Break-glass merge PR #1596

This commit is contained in:
Martin Vogel authored and GitHub committed 2026-08-14 01:51:40 +02:00
commit d150ebe4fc
26 files changed
+4197 -752

No files matched your search

+286 -54
View File
@@ -12,6 +12,14 @@ on:
description: 'Generate build provenance attestations for release artifacts'
type: boolean
default: false
scan_candidates:
description: 'VirusTotal-scan both candidates before tuple-local selection (release must keep true)'
type: boolean
default: true
secrets:
VIRUS_TOTAL_SCANNER_API_KEY:
description: 'VirusTotal API key used for candidate selection'
required: false
permissions:
contents: read
id-token: write
@@ -74,37 +82,34 @@ jobs:
scripts/build.sh --with-ui "CC=$CC" "CXX=$CXX"
fi
- name: Ad-hoc sign macOS release binary
if: startsWith(matrix.os, 'macos')
run: codesign --sign - --force build/c/codebase-memory-mcp
- name: Frontend integrity scan
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
run: scripts/security-ui.sh
# Archive layout/name live in the ONE canonical script the local
# artifact-flow smoke lane also runs (venue-parity contract).
- name: Archive release binary (canonical package-release.sh)
env:
VERSION: ${{ inputs.version }}
run: scripts/package-release.sh ${{ matrix.goos }} ${{ matrix.goarch }}
- name: Derive immutable stripped and unstripped release candidates
run: |
scripts/ci/prepare-release-candidates.sh \
"${{ matrix.goos }}" "${{ matrix.goarch }}" \
--binary build/c/codebase-memory-mcp \
--out-dir release-candidates
scripts/gen-third-party-notices.sh \
"release-candidates/${{ matrix.goos }}-${{ matrix.goarch }}/THIRD_PARTY_NOTICES.md"
- name: Attest release binary provenance
- name: Attest release candidate provenance
if: ${{ inputs.attest }}
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
# *.mcpb matches nothing on the glibc-dynamic linux targets — the
# canonical packager builds bundles only where static/one-click holds.
subject-path: |
codebase-memory-mcp-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
*.mcpb
release-candidates/${{ matrix.goos }}-${{ matrix.goarch }}/candidate-provenance.tsv
release-candidates/${{ matrix.goos }}-${{ matrix.goarch }}/stripped/*
release-candidates/${{ matrix.goos }}-${{ matrix.goarch }}/unstripped/*
release-candidates/${{ matrix.goos }}-${{ matrix.goarch }}/THIRD_PARTY_NOTICES.md
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}
path: |
*.tar.gz
*.mcpb
name: release-candidates-${{ matrix.goos }}-${{ matrix.goarch }}
path: release-candidates/${{ matrix.goos }}-${{ matrix.goarch }}
if-no-files-found: error
build-windows:
runs-on: windows-latest
@@ -120,6 +125,8 @@ jobs:
mingw-w64-clang-x86_64-clang
mingw-w64-clang-x86_64-zlib
make
python
unzip
zip
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
@@ -137,28 +144,30 @@ jobs:
scripts/build.sh --with-ui CC=clang CXX=clang++
fi
# Archive layout/name live in the ONE canonical script the local
# artifact-flow smoke lane also runs (venue-parity contract).
- name: Archive release binary (canonical package-release.sh)
- name: Derive immutable stripped and unstripped release candidates
shell: msys2 {0}
env:
VERSION: ${{ inputs.version }}
run: scripts/package-release.sh windows amd64 CC=clang CXX=clang++
run: |
scripts/ci/prepare-release-candidates.sh windows amd64 \
--binary build/c/codebase-memory-mcp.exe \
--out-dir release-candidates
scripts/gen-third-party-notices.sh \
release-candidates/windows-amd64/THIRD_PARTY_NOTICES.md
- name: Attest release binary provenance
- name: Attest release candidate provenance
if: ${{ inputs.attest }}
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: |
codebase-memory-mcp-windows-amd64.zip
*.mcpb
release-candidates/windows-amd64/candidate-provenance.tsv
release-candidates/windows-amd64/stripped/*
release-candidates/windows-amd64/unstripped/*
release-candidates/windows-amd64/THIRD_PARTY_NOTICES.md
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binaries-windows-amd64
path: |
*.zip
*.mcpb
name: release-candidates-windows-amd64
path: release-candidates/windows-amd64
if-no-files-found: error
build-windows-arm64:
# Native ARM64 Windows binary via the CLANGARM64 toolchain on the
@@ -178,6 +187,8 @@ jobs:
mingw-w64-clang-aarch64-clang
mingw-w64-clang-aarch64-zlib
make
python
unzip
zip
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
@@ -195,26 +206,30 @@ jobs:
scripts/build.sh --with-ui CC=clang CXX=clang++
fi
- name: Archive release binary (canonical package-release.sh)
- name: Derive immutable stripped and unstripped release candidates
shell: msys2 {0}
env:
VERSION: ${{ inputs.version }}
run: scripts/package-release.sh windows arm64 CC=clang CXX=clang++
run: |
scripts/ci/prepare-release-candidates.sh windows arm64 \
--binary build/c/codebase-memory-mcp.exe \
--out-dir release-candidates
scripts/gen-third-party-notices.sh \
release-candidates/windows-arm64/THIRD_PARTY_NOTICES.md
- name: Attest release binary provenance
- name: Attest release candidate provenance
if: ${{ inputs.attest }}
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: |
codebase-memory-mcp-windows-arm64.zip
*.mcpb
release-candidates/windows-arm64/candidate-provenance.tsv
release-candidates/windows-arm64/stripped/*
release-candidates/windows-arm64/unstripped/*
release-candidates/windows-arm64/THIRD_PARTY_NOTICES.md
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binaries-windows-arm64
path: |
*.zip
*.mcpb
name: release-candidates-windows-arm64
path: release-candidates/windows-arm64
if-no-files-found: error
build-linux-portable:
# Fully static Linux binaries (gcc -static on Ubuntu).
@@ -254,22 +269,239 @@ jobs:
file build/c/codebase-memory-mcp
ldd build/c/codebase-memory-mcp 2>&1 | grep -q "not a dynamic executable" || ldd build/c/codebase-memory-mcp 2>&1 | grep -q "statically linked"
- name: Archive release binary (canonical package-release.sh)
env:
VERSION: ${{ inputs.version }}
run: scripts/package-release.sh linux ${{ matrix.arch }}-portable
- name: Derive immutable stripped and unstripped release candidates
run: |
scripts/ci/prepare-release-candidates.sh \
linux "${{ matrix.arch }}-portable" \
--binary build/c/codebase-memory-mcp \
--out-dir release-candidates
scripts/gen-third-party-notices.sh \
"release-candidates/linux-${{ matrix.arch }}-portable/THIRD_PARTY_NOTICES.md"
- name: Attest release binary provenance
- name: Attest release candidate provenance
if: ${{ inputs.attest }}
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: |
codebase-memory-mcp-linux-${{ matrix.arch }}-portable.tar.gz
*.mcpb
release-candidates/linux-${{ matrix.arch }}-portable/candidate-provenance.tsv
release-candidates/linux-${{ matrix.arch }}-portable/stripped/*
release-candidates/linux-${{ matrix.arch }}-portable/unstripped/*
release-candidates/linux-${{ matrix.arch }}-portable/THIRD_PARTY_NOTICES.md
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binaries-linux-${{ matrix.arch }}-portable
path: |
*.tar.gz
*.mcpb
name: release-candidates-linux-${{ matrix.arch }}-portable
path: release-candidates/linux-${{ matrix.arch }}-portable
if-no-files-found: error
# One blocking join owns the security-sensitive transition from candidates
# to public artifacts. Each target tuple contributes exactly three behavior-
# equivalent candidates (unstripped/debug-stripped/stripped); the selector may
# only choose within that tuple. A hard result on ANY sibling blocks
# the all-target release rather than selecting around a real detection.
select-package:
needs: [build-unix, build-windows, build-windows-arm64, build-linux-portable]
runs-on: ubuntu-latest
timeout-minutes: 330
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download all native candidate pairs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: release-candidates-*
path: ${{ runner.temp }}/release-candidate-artifacts
- name: Stage the exact candidate scan set
run: |
python3 scripts/ci/stage-release-candidates.py \
"$RUNNER_TEMP/release-candidate-artifacts" \
release-candidate-scan \
--expect-targets 8 \
--expect-candidates 24
# Staging copied and re-hashed every byte into the immutable object
# set. Drop the redundant artifact-download tree before the VT wait.
test -n "$RUNNER_TEMP"
rm -rf -- "$RUNNER_TEMP/release-candidate-artifacts"
test ! -e "$RUNNER_TEMP/release-candidate-artifacts"
- name: Submit all 24 candidates to VirusTotal
if: ${{ inputs.scan_candidates }}
uses: crazy-max/ghaction-virustotal@936d8c5c00afe97d3d9a1af26d017cfdf26800a2 # v5.0.0
id: virustotal-candidates
with:
vt_api_key: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
files: release-candidate-scan/objects/*
request_rate: 4
# The established gate tolerates only one Microsoft malicious label
# ending !ml and rejects every other malicious/suspicious combination.
# Every candidate still must complete with the minimum engine coverage.
- name: Wait for complete VirusTotal candidate results
if: ${{ inputs.scan_candidates }}
env:
VT_API_KEY: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
VT_ANALYSIS: ${{ steps.virustotal-candidates.outputs.analysis }}
VT_EXPECTED_SCAN_SET: release-candidate-scan/scan-set.tsv
VT_ASSOCIATIONS: release-candidate-scan/associations.tsv
VT_RESULTS_PATH: release-candidate-scan/vt-results.tsv
VT_POLL_TIMEOUT_SECONDS: 14400
MIN_ENGINES: 50
run: scripts/ci/check-virustotal.sh
- name: Select one scanned candidate inside every target tuple
if: ${{ inputs.scan_candidates }}
run: |
python3 scripts/ci/select-release-candidates.py \
--candidates release-candidate-scan/candidates.tsv \
--objects-dir release-candidate-scan/objects \
--results release-candidate-scan/vt-results.tsv \
--out-dir release-selection
# This is intentionally reachable only from dry-run.yml's explicit
# skip_virustotal input. release.yml pins scan_candidates=true.
- name: Mark unscanned dry run and default to stripped candidates
if: ${{ !inputs.scan_candidates }}
run: |
echo '::warning::VirusTotal candidate scan explicitly skipped; selecting stripped candidates as unscanned-dry-run.'
python3 scripts/ci/select-release-candidates.py \
--candidates release-candidate-scan/candidates.tsv \
--objects-dir release-candidate-scan/objects \
--default-stripped \
--out-dir release-selection
- name: Reclaim rejected and redundant candidate bytes
run: |
# release-selection owns fresh, hash-verified copies of the eight
# chosen executables. No later release step may consume the 24-object
# scan pool, so discard it before packaging to bound runner disk use.
rm -rf -- release-candidate-scan/objects
test ! -e release-candidate-scan/objects
- name: Package the selected exact bytes
env:
VERSION: ${{ inputs.version }}
run: |
for target in \
linux-amd64 linux-arm64 \
linux-amd64-portable linux-arm64-portable \
darwin-amd64 darwin-arm64 \
windows-amd64 windows-arm64; do
goos="${target%%-*}"
goarch="${target#*-}"
selected_name=codebase-memory-mcp
test "$goos" != windows || selected_name=codebase-memory-mcp.exe
selected="release-selection/selected/$target/$selected_name"
notices="release-candidate-scan/notices/$target/THIRD_PARTY_NOTICES.md"
test -f "$selected"
test -f "$notices"
# The expected hash comes from the selector's provenance manifest,
# not from the file being checked. package-release.sh independently
# re-hashes the file and therefore exercises manifest -> file ->
# archive/MCPB binding instead of comparing a file to itself.
selected_sha="$(awk -F '\t' -v wanted="$target" '!header { for (i = 1; i <= NF; i++) { if ($i == "target") target_col = i; if ($i == "selected_sha256") sha_col = i } if (target_col && sha_col) header = 1; next } $target_col == wanted { count++; selected = $sha_col } END { if (count != 1 || length(selected) != 64 || selected ~ /[^0-9a-f]/) exit 3; print selected }' release-selection/release-selection.tsv)"
scripts/package-release.sh "$goos" "$goarch" \
--selected-binary "$selected" \
--expected-sha256 "$selected_sha" \
--third-party-notices "$notices" \
--out-dir "release-containers/$target"
done
- name: Verify every packaged executable matches its selected hash
run: |
python3 scripts/ci/verify-release-selection.py \
--selection release-selection/release-selection.tsv \
--archive-dir release-containers
- name: Prepare durable candidate-selection evidence
if: ${{ always() }}
run: |
mkdir -p selection-evidence
test ! -f release-candidate-scan/candidates.tsv || \
cp release-candidate-scan/candidates.tsv selection-evidence/release-candidates.tsv
test ! -f release-candidate-scan/vt-results.tsv || \
cp release-candidate-scan/vt-results.tsv selection-evidence/virustotal-candidate-results.tsv
test ! -f release-selection/release-selection.tsv || \
cp release-selection/release-selection.tsv selection-evidence/release-selection.tsv
- name: Attest selected containers and decision evidence
if: ${{ inputs.attest }}
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: |
release-containers/**/*.tar.gz
release-containers/**/*.zip
release-containers/**/*.mcpb
selection-evidence/*.tsv
- name: Preserve candidate selection evidence
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-selection-evidence
path: selection-evidence
if-no-files-found: error
# Keep the established artifact names so the reusable smoke workflow and
# all callers consume only the selected, byte-verified containers.
- name: Upload linux-amd64 containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binaries-linux-amd64
path: release-containers/linux-amd64/*
if-no-files-found: error
- name: Upload linux-arm64 containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binaries-linux-arm64
path: release-containers/linux-arm64/*
if-no-files-found: error
- name: Upload linux-amd64-portable containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binaries-linux-amd64-portable
path: release-containers/linux-amd64-portable/*
if-no-files-found: error
- name: Upload linux-arm64-portable containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binaries-linux-arm64-portable
path: release-containers/linux-arm64-portable/*
if-no-files-found: error
- name: Upload darwin-amd64 containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binaries-darwin-amd64
path: release-containers/darwin-amd64/*
if-no-files-found: error
- name: Upload darwin-arm64 containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binaries-darwin-arm64
path: release-containers/darwin-arm64/*
if-no-files-found: error
- name: Upload windows-amd64 containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binaries-windows-amd64
path: release-containers/windows-amd64/*
if-no-files-found: error
- name: Upload windows-arm64 containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binaries-windows-arm64
path: release-containers/windows-arm64/*
if-no-files-found: error
+124 -3
View File
@@ -20,6 +20,10 @@ on:
description: 'Version string for build'
type: string
default: ''
use_release_artifacts:
description: 'Soak the already selected binaries-* artifacts instead of rebuilding quick-lane binaries'
type: boolean
default: false
permissions:
contents: read
@@ -64,12 +68,33 @@ jobs:
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git
- name: Build
if: ${{ !inputs.use_release_artifacts }}
run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
- name: Download selected release container
if: ${{ inputs.use_release_artifacts }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}
path: ${{ runner.temp }}/cbm-selected-container
- name: Extract selected release executable
if: ${{ inputs.use_release_artifacts }}
run: |
mkdir -p "$RUNNER_TEMP/cbm-selected-artifact"
tar -xzf "$RUNNER_TEMP/cbm-selected-container/codebase-memory-mcp-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz" \
-C "$RUNNER_TEMP/cbm-selected-artifact" codebase-memory-mcp
chmod +x "$RUNNER_TEMP/cbm-selected-artifact/codebase-memory-mcp"
"$RUNNER_TEMP/cbm-selected-artifact/codebase-memory-mcp" --version
# The soak SEQUENCE and its guards live in the ONE canonical entry
# scripts/soak-legs.sh — never in workflow YAML. Leg selection is a
# workflow input so a long #581-only run needs no separate workflow.
- name: Soak legs (${{ inputs.legs }}, ${{ inputs.duration_minutes }} min each)
run: scripts/soak-legs.sh --legs "${{ inputs.legs }}" build/c/codebase-memory-mcp ${{ inputs.duration_minutes }}
env:
SELECTED_BINARY: ${{ runner.temp }}/cbm-selected-artifact/codebase-memory-mcp
USE_RELEASE_ARTIFACTS: ${{ inputs.use_release_artifacts }}
run: |
binary=build/c/codebase-memory-mcp
test "$USE_RELEASE_ARTIFACTS" != true || binary="$SELECTED_BINARY"
scripts/soak-legs.sh --legs "${{ inputs.legs }}" "$binary" ${{ inputs.duration_minutes }}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -104,6 +129,7 @@ jobs:
make
git
coreutils
unzip
# Same protected root the test legs and the VM soak use. Without it the
# soak ran under the runner's inherited LocalAppData\Temp ACLs while the
# local soak ran hardened — the two venues were soaking different shapes.
@@ -115,8 +141,24 @@ jobs:
-ProtectDir (Join-Path $env:GITHUB_WORKSPACE "build\c")
"CBM_CI_TEMP_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Build
if: ${{ !inputs.use_release_artifacts }}
shell: msys2 {0}
run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=clang CXX=clang++
- name: Download selected release container
if: ${{ inputs.use_release_artifacts }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: binaries-windows-amd64
path: ${{ runner.temp }}/cbm-selected-container
- name: Extract selected release executable
if: ${{ inputs.use_release_artifacts }}
shell: msys2 {0}
run: |
ARTIFACT_DIR="$(cygpath -u "$RUNNER_TEMP")/cbm-selected-artifact"
mkdir -p "$ARTIFACT_DIR"
unzip -q "$(cygpath -u "$RUNNER_TEMP")/cbm-selected-container/codebase-memory-mcp-windows-amd64.zip" \
codebase-memory-mcp.exe -d "$ARTIFACT_DIR"
"$ARTIFACT_DIR/codebase-memory-mcp.exe" --version
# Both release-gating legs via the ONE canonical soak entry; only the
# protected-TEMP provisioning is venue text. soak-legs.sh resolves .exe.
- name: Soak legs (${{ inputs.legs }}, ${{ inputs.duration_minutes }} min each)
@@ -125,7 +167,11 @@ jobs:
export TEMP="$(cygpath -m "$CBM_CI_TEMP_ROOT")"
export TMP="$TEMP"
export TMPDIR="$(cygpath -u "$CBM_CI_TEMP_ROOT")"
scripts/soak-legs.sh --legs "${{ inputs.legs }}" build/c/codebase-memory-mcp ${{ inputs.duration_minutes }}
binary=build/c/codebase-memory-mcp
if [ "${{ inputs.use_release_artifacts }}" = true ]; then
binary="$(cygpath -u "$RUNNER_TEMP")/cbm-selected-artifact/codebase-memory-mcp.exe"
fi
scripts/soak-legs.sh --legs "${{ inputs.legs }}" "$binary" ${{ inputs.duration_minutes }}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -161,6 +207,7 @@ jobs:
make
git
coreutils
unzip
# Same protected root the test legs and the VM soak use. Without it the
# soak ran under the runner's inherited LocalAppData\Temp ACLs while the
# local soak ran hardened — the two venues were soaking different shapes.
@@ -172,8 +219,24 @@ jobs:
-ProtectDir (Join-Path $env:GITHUB_WORKSPACE "build\c")
"CBM_CI_TEMP_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Build
if: ${{ !inputs.use_release_artifacts }}
shell: msys2 {0}
run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=clang CXX=clang++
- name: Download selected release container
if: ${{ inputs.use_release_artifacts }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: binaries-windows-arm64
path: ${{ runner.temp }}/cbm-selected-container
- name: Extract selected release executable
if: ${{ inputs.use_release_artifacts }}
shell: msys2 {0}
run: |
ARTIFACT_DIR="$(cygpath -u "$RUNNER_TEMP")/cbm-selected-artifact"
mkdir -p "$ARTIFACT_DIR"
unzip -q "$(cygpath -u "$RUNNER_TEMP")/cbm-selected-container/codebase-memory-mcp-windows-arm64.zip" \
codebase-memory-mcp.exe -d "$ARTIFACT_DIR"
"$ARTIFACT_DIR/codebase-memory-mcp.exe" --version
# Both release-gating legs via the ONE canonical soak entry; only the
# protected-TEMP provisioning is venue text. soak-legs.sh resolves .exe.
- name: Soak legs (${{ inputs.legs }}, ${{ inputs.duration_minutes }} min each)
@@ -182,7 +245,11 @@ jobs:
export TEMP="$(cygpath -m "$CBM_CI_TEMP_ROOT")"
export TMP="$TEMP"
export TMPDIR="$(cygpath -u "$CBM_CI_TEMP_ROOT")"
scripts/soak-legs.sh --legs "${{ inputs.legs }}" build/c/codebase-memory-mcp ${{ inputs.duration_minutes }}
binary=build/c/codebase-memory-mcp
if [ "${{ inputs.use_release_artifacts }}" = true ]; then
binary="$(cygpath -u "$RUNNER_TEMP")/cbm-selected-artifact/codebase-memory-mcp.exe"
fi
scripts/soak-legs.sh --legs "${{ inputs.legs }}" "$binary" ${{ inputs.duration_minutes }}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -198,7 +265,61 @@ jobs:
path: soak-results-query-leak/
retention-days: 14
# Portable Linux is a separate immutable product tuple. It participates when
# a caller supplies selected release artifacts; the source-building nightly
# retains its historical matrix and has no upstream binaries-* artifacts.
soak-quick-linux-portable:
if: ${{ inputs.use_release_artifacts }}
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
timeout-minutes: 560
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install soak dependencies
run: sudo apt-get update && sudo apt-get install -y python3 git
- name: Download selected portable release container
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: binaries-linux-${{ matrix.arch }}-portable
path: ${{ runner.temp }}/cbm-selected-container
- name: Extract selected portable executable
run: |
mkdir -p "$RUNNER_TEMP/cbm-selected-artifact"
tar -xzf "$RUNNER_TEMP/cbm-selected-container/codebase-memory-mcp-linux-${{ matrix.arch }}-portable.tar.gz" \
-C "$RUNNER_TEMP/cbm-selected-artifact" codebase-memory-mcp
chmod +x "$RUNNER_TEMP/cbm-selected-artifact/codebase-memory-mcp"
"$RUNNER_TEMP/cbm-selected-artifact/codebase-memory-mcp" --version
- name: Soak selected portable legs (${{ inputs.legs }}, ${{ inputs.duration_minutes }} min each)
run: |
scripts/soak-legs.sh --legs "${{ inputs.legs }}" \
"$RUNNER_TEMP/cbm-selected-artifact/codebase-memory-mcp" \
${{ inputs.duration_minutes }}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: soak-quick-linux-${{ matrix.arch }}-portable
path: soak-results/
retention-days: 14
- name: Upload query-leak metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: soak-query-leak-linux-${{ matrix.arch }}-portable
path: soak-results-query-leak/
retention-days: 14
soak-asan:
# Supplemental source-built instrumentation. This can find memory bugs but
# is not evidence that the selected release bytes were soaked; the quick
# jobs above own that release gate when use_release_artifacts=true.
if: ${{ inputs.run_asan }}
strategy:
fail-fast: false
+16 -92
View File
@@ -1,30 +1,25 @@
# Manual trigger: test everything before pushing a release.
# Each step can be skipped for faster iteration.
#
# Pipeline: lint → test → build → smoke/soak → virustotal (sequential chain)
# Pipeline: lint → test → build + candidate scan/select → smoke/soak
# Security: security-static + codeql-gate (independent island)
#
# Security does NOT block lint/test/build/smoke/soak. All jobs must pass
# for the overall workflow to be green.
#
# VirusTotal runs LAST and scans the same artifacts the release scans, so a
# detection surfaces here instead of mid-release. It is the one dry-run job
# whose verdict does not depend on our code: the same unchanged bytes can come
# back clean one day and flagged the next, which is precisely why finding out
# during a dry run is worth the wait.
# Candidate VirusTotal scanning and tuple-local selection are part of the
# reusable build boundary. Smoke and soak therefore start only after all 16
# stripped/unstripped candidates have completed and the selected exact bytes
# have been packaged under the canonical artifact names.
#
# The skips are independent because they answer different questions:
# skip_builds no artifacts at all "do lint and tests pass?"
# skip_smoke build + scan, no smoke "does this diff scan red?"
# skip_virustotal build + smoke + soak, no scan "do the archives pass CI?"
# skip_builds no artifacts at all "do lint and tests pass?"
# skip_smoke build + scan/select, no smoke "does this diff scan red?"
# skip_virustotal default-stripped + smoke, no scan "do the archives pass CI?"
#
# skip_virustotal exists because the scan costs API quota and polls for up to
# two hours, which is pure noise while iterating on a build or smoke failure.
#
# skip_smoke is the opposite trip: go straight from build to VirusTotal when the
# question is only whether a diff turns the scan red. Smoke is in the VirusTotal
# job's needs, so without this a smoke failure would withhold exactly the answer
# such a run was dispatched to get.
# skip_virustotal is a conspicuous dry-run-only bypass for API-quota-sensitive
# iteration. It records unscanned-dry-run and deterministically chooses stripped
# within each tuple. release.yml has no corresponding bypass.
name: Dry Run
on:
@@ -43,11 +38,11 @@ on:
type: boolean
default: false
skip_smoke:
description: 'Skip smoke (build straight to VirusTotal)'
description: 'Skip smoke (candidate scan/select still runs inside build)'
type: boolean
default: false
skip_virustotal:
description: 'Skip VirusTotal scan (still builds + smokes the archives)'
description: 'Skip candidate scan; mark unscanned and default to stripped'
type: boolean
default: false
soak_level:
@@ -91,6 +86,8 @@ jobs:
uses: ./.github/workflows/_build.yml
with:
attest: false
scan_candidates: ${{ !inputs.skip_virustotal }}
secrets: inherit
# ── Smoke test every binary ────────────────────────────────────
# Run unless builds were skipped or a build leg FAILED. Every build leg
@@ -111,77 +108,4 @@ jobs:
with:
duration_minutes: 10
run_asan: ${{ inputs.soak_level == 'full' }}
# ── VirusTotal (final step) ────────────────────────────────────
# Mirrors the release workflow's scan so a dry run answers the question the
# release will ask, before we tag anything. The only difference is where the
# archives come from: the release pulls them off a published release, a dry
# run has none, so they come from the build job's artifacts — the SAME files,
# produced by the same canonical package-release.sh.
#
# Runs after smoke and soak deliberately. It is the slowest job (polling can
# take up to two hours) and the one most likely to be waiting on someone
# else's queue, so it must not delay the feedback that is actually about our
# code.
#
# It needs smoke and soak only so it runs LAST, not because it depends on
# them: a skipped smoke or soak leaves the scan reachable (result 'skipped',
# which is not 'failure'), while a FAILED one withholds it — if the artifacts
# are broken, a verdict on them is not worth two hours of polling.
virustotal:
if: ${{ inputs.skip_builds != true && inputs.skip_virustotal != true && !cancelled() && needs.build.result == 'success' && needs.smoke.result != 'failure' && needs.soak.result != 'failure' }}
needs: [build, smoke, soak]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: binaries-*
merge-multiple: true
path: ${{ runner.temp }}/release-archives
# Same extraction the release verify job performs. VirusTotal receives
# only extracted files and unpacked UI assets, never .tar.gz/.zip containers.
- name: Extract archive artifacts
run: |
scripts/ci/extract-release-archives.sh "$RUNNER_TEMP/release-archives" binaries \
--expect-archives=8 \
--expect-binaries=8 \
--expect-runtime-files=24
- name: VirusTotal scan
uses: crazy-max/ghaction-virustotal@936d8c5c00afe97d3d9a1af26d017cfdf26800a2 # v5.0.0
id: virustotal
with:
vt_api_key: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
files: binaries/objects/*
request_rate: 4
# Same ZERO-tolerance gate as the release. Every extracted object must arrive
# naturally clean; any malicious or suspicious verdict fails this run.
- name: Wait for VirusTotal results
env:
VT_API_KEY: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
VT_ANALYSIS: ${{ steps.virustotal.outputs.analysis }}
VT_EXPECTED_SCAN_SET: binaries/scan-set.tsv
VT_ASSOCIATIONS: binaries/associations.tsv
VT_RESULTS_PATH: binaries/vt-results.tsv
MIN_ENGINES: 50
run: scripts/ci/check-virustotal.sh
- name: Preserve VirusTotal scope and result evidence
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: virustotal-evidence-dry-run-${{ github.run_id }}
path: |
binaries/associations.tsv
binaries/scan-set.tsv
binaries/vt-results.tsv
if-no-files-found: warn
use_release_artifacts: true
+93 -26
View File
@@ -22,9 +22,9 @@ on:
type: boolean
default: false
soak_level:
description: 'Soak: full (quick+asan), quick (10min), none'
description: 'Soak: full (quick+asan) or quick (10min)'
type: choice
options: ['full', 'quick', 'none']
options: ['full', 'quick']
default: 'quick'
skip_perf:
description: "Skip performance tests (use when no pipeline logic changed)"
@@ -78,6 +78,10 @@ jobs:
with:
version: ${{ inputs.version }}
attest: true
# Release builds have no scan bypass: both candidates in all eight
# immutable target tuples must complete before smoke or soak starts.
scan_candidates: true
secrets: inherit
# ── 4. Smoke test every binary ──────────────────────────────────
# !cancelled() && !failure(): GitHub propagates "skipped" TRANSITIVELY down the
@@ -96,26 +100,27 @@ jobs:
# ── 5. Soak tests ──────────────────────────────────────────────
soak:
if: ${{ !cancelled() && !failure() && inputs.soak_level != 'none' }}
if: ${{ !cancelled() && !failure() }}
needs: [build]
uses: ./.github/workflows/_soak.yml
with:
duration_minutes: 10
run_asan: ${{ inputs.soak_level == 'full' }}
version: ${{ inputs.version }}
use_release_artifacts: true
# ── 6. Create DRAFT release ────────────────────────────────────
# Requires smoke to have actually SUCCEEDED, not merely "not failed". The bare
# !cancelled() && !failure() form is fail-OPEN: a skipped smoke is neither
# cancelled nor failed, so a draft was one condition away from being cut from
# binaries nobody had run. soak may legitimately be 'skipped' (soak_level=none),
# so that one is enumerated explicitly rather than lumped in.
# binaries nobody had run. Selected release bytes must also finish soak; a
# skipped soak is not an acceptable production-release state.
release-draft:
needs: [smoke, soak]
if: >-
${{ !cancelled() && !failure()
&& needs.smoke.result == 'success'
&& (needs.soak.result == 'success' || needs.soak.result == 'skipped') }}
&& needs.soak.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write
@@ -124,15 +129,40 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- name: Download only selected release containers
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: binaries-*
merge-multiple: true
- name: List artifacts
run: ls -la *.tar.gz *.zip *.mcpb
# Candidate artifacts are deliberately excluded from the container
# download above. Preserve only the three reviewable manifests that bind
# candidate bytes, VT results, and the tuple-local decision.
- name: Download candidate selection evidence separately
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-selection-evidence
path: selection-evidence
- name: Verify and stage the exact evidence set
run: |
test "$(find selection-evidence -maxdepth 1 -type f | wc -l | tr -d ' ')" = 3
for f in release-candidates.tsv virustotal-candidate-results.tsv release-selection.tsv; do
test -f "selection-evidence/$f"
cp "selection-evidence/$f" "$f"
done
- name: List selected artifacts and decision evidence
run: ls -la *.tar.gz *.zip *.mcpb *.tsv
- name: Generate checksums
run: sha256sum *.tar.gz *.zip *.mcpb > checksums.txt
run: |
sha256sum \
*.tar.gz *.zip *.mcpb \
release-candidates.tsv \
virustotal-candidate-results.tsv \
release-selection.tsv \
> checksums.txt
- name: Attest checksum provenance
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
@@ -164,7 +194,12 @@ jobs:
- name: Sign artifacts
run: |
for f in *.tar.gz *.zip *.mcpb checksums.txt; do
for f in \
*.tar.gz *.zip *.mcpb \
release-candidates.tsv \
virustotal-candidate-results.tsv \
release-selection.tsv \
checksums.txt; do
cosign sign-blob --yes --bundle "${f}.bundle" "$f"
done
@@ -197,6 +232,9 @@ jobs:
*.tar.gz
*.zip
*.mcpb
release-candidates.tsv
virustotal-candidate-results.tsv
release-selection.tsv
checksums.txt
sbom.json
*.bundle
@@ -206,9 +244,9 @@ jobs:
# ── 7. Verify + Publish (requires security gate) ───────────────
verify:
needs: [release-draft, security]
# Explicit condition: a skipped ancestor (soak with soak_level=none) must
# not skip the publish chain — only real failures/cancellations block it.
if: ${{ !cancelled() && !failure() && needs.release-draft.result == 'success' }}
# Explicit condition: a deliberately skipped test ancestor must not skip
# the publish chain; the draft itself proves smoke and soak both succeeded.
if: ${{ !cancelled() && !failure() && needs.security.result == 'success' && needs.release-draft.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write
@@ -225,6 +263,11 @@ jobs:
ARCHIVE_DIR="$RUNNER_TEMP/release-archives"
mkdir -p "$ARCHIVE_DIR" binaries
gh release download "$VERSION" --dir "$ARCHIVE_DIR" --repo "$GITHUB_REPOSITORY" --pattern '*.tar.gz' --pattern '*.zip' --pattern '*.mcpb'
gh release download "$VERSION" --repo "$GITHUB_REPOSITORY" --pattern 'release-selection.tsv' --output "$RUNNER_TEMP/release-selection.tsv"
python3 scripts/ci/verify-release-selection.py \
--selection "$RUNNER_TEMP/release-selection.tsv" \
--require-policy virustotal-v2 \
--archive-dir "$ARCHIVE_DIR"
# 14 = 8 archives + 6 MCPB bundles; runtime 42 = 8×3 archive
# sidecars + 6×3 bundle members (manifest.json, LICENSE, notices).
# The bundle binaries dedupe to the archive objects byte-for-byte.
@@ -233,6 +276,14 @@ jobs:
--expect-binaries=14 \
--expect-runtime-files=42
- name: Download the existing candidate VirusTotal evidence
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
gh release download "$VERSION" --repo "$GITHUB_REPOSITORY" --pattern 'release-candidates.tsv' --output "$RUNNER_TEMP/release-candidates.tsv"
gh release download "$VERSION" --repo "$GITHUB_REPOSITORY" --pattern 'virustotal-candidate-results.tsv' --output "$RUNNER_TEMP/virustotal-candidate-results.tsv"
- name: Security audits on all unique extracted release objects
run: |
# Audit every distinct extracted member/UI-asset byte sequence. The
@@ -247,7 +298,21 @@ jobs:
scripts/security-strings.sh "$f"
done
- name: VirusTotal scan
# Full-surface VirusTotal pass over the SHIPPED containers.
#
# The candidate scan upstream covers executables only. Everything else we
# actually ship — install.sh, install.ps1, LICENSE, THIRD_PARTY_NOTICES.md,
# the MCPB manifest.json and the unpacked UI assets, 42 runtime files
# across 14 containers — is extracted here and gets scanned here. Dropping
# it would have narrowed the promise made in README/SECURITY.md, and
# install.sh and install.ps1 are the highest-consequence non-executable
# bytes we publish: users pipe them straight into a shell.
#
# The selected executables are re-submitted too. That is deliberate and
# nearly free: VirusTotal is content-addressed, so identical bytes return
# the analysis it already holds instead of re-running 70+ engines — the
# same property that made the analysis-id equality check untenable.
- name: VirusTotal scan of every extracted release object
uses: crazy-max/ghaction-virustotal@936d8c5c00afe97d3d9a1af26d017cfdf26800a2 # v5.0.0
id: virustotal
with:
@@ -255,8 +320,8 @@ jobs:
files: binaries/objects/*
request_rate: 4
# ZERO tolerance: every extracted object must arrive naturally clean. Any
# malicious or suspicious verdict blocks the release with no exception.
# Same policy as the candidate gate: a single Microsoft `!ml` is tolerated
# and disclosed, anything else blocks. Engine count is evidence, not a gate.
- name: Wait for VirusTotal results
env:
VT_API_KEY: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
@@ -267,7 +332,7 @@ jobs:
MIN_ENGINES: 50
run: scripts/ci/check-virustotal.sh
- name: Preserve VirusTotal scope and result evidence
- name: Preserve extracted-object VirusTotal evidence
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@@ -287,15 +352,17 @@ jobs:
VT_RESULTS_PATH: binaries/vt-results.tsv
run: bash scripts/ci/publish-vt-evidence.sh
# Renders extracted-file scan results plus archive SHA-256 provenance. It
# runs only after the exact-set zero-tolerance gate passed.
- name: Append VirusTotal scan links to release notes
# Candidate bytes were scanned before smoke/soak and the verifier above
# proved every packaged executable has the selected candidate SHA-256.
# Re-submitting the same executable bytes here would add wait time but no
# new coverage, so publish the existing per-candidate verdicts instead.
- name: Append candidate VirusTotal results to release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
VT_EXPECTED_SCAN_SET: binaries/scan-set.tsv
VT_ASSOCIATIONS: binaries/associations.tsv
VT_RESULTS_PATH: binaries/vt-results.tsv
VT_CANDIDATES: ${{ runner.temp }}/release-candidates.tsv
VT_RESULTS_PATH: ${{ runner.temp }}/virustotal-candidate-results.tsv
RELEASE_SELECTION: ${{ runner.temp }}/release-selection.tsv
run: bash scripts/ci/append-vt-notes.sh
# ── 8. Publish package wrappers (npm + PyPI) ──────────────────
@@ -510,8 +577,8 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Legacy ui-* aliases so 0.9.x updaters stop 404ing (#1538). Runs after
# verify: same bytes as archives the VirusTotal gate already cleared, and
# publishing earlier would duplicate the scan set and provenance manifest.
# verify: aliases contain the same selected executable bytes already
# hash-bound to the pre-smoke VirusTotal verdicts.
- name: Publish legacy ui-* archive aliases
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+8 -4
View File
@@ -20,7 +20,7 @@ High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-si
> **Research** — The design and benchmarks behind this project are described in the preprint [*Codebase-Memory: Tree-Sitter-Based Knowledge Graphs for LLM Code Exploration via MCP*](https://arxiv.org/abs/2603.27277) (arXiv:2603.27277). Evaluated across 31 real-world repositories: 83% answer quality, 10× fewer tokens, 2.1× fewer tool calls vs. file-by-file exploration.
> **Security & Trust** — This tool reads your codebase and writes to your agent configuration files. That is what it is designed to do. If you prefer to audit before running, the [full source is here](https://github.com/DeusData/codebase-memory-mcp). Release archives are signed and checksummed; every distinct extracted member and unpacked UI asset is submitted to VirusTotal, while the downloadable `.tar.gz`/`.zip` containers themselves are not. The release notes record the measured engine count and exact 0/N result; publication requires at least 50 decisive engine results with zero malicious and zero suspicious verdicts. All processing happens 100% locally; your code never leaves your machine. Found a security issue? We want to know — see [SECURITY.md](SECURITY.md). Security is Priority #1 for us.
> **Security & Trust** — This tool reads your codebase and writes to your agent configuration files. That is what it is designed to do. If you prefer to audit before running, the [full source is here](https://github.com/DeusData/codebase-memory-mcp). For each release product, three behaviourally identical executable candidates (unstripped, debug-stripped, stripped) are submitted to VirusTotal before testing; the selected candidate is then packaged with its SHA-256 unchanged. Release notes link every measured candidate result. Publication permits only the narrowly documented single-Microsoft `!ml` tolerance in [SECURITY.md](SECURITY.md#our-release-policy). All processing happens 100% locally; your code never leaves your machine. Found a security issue? We want to know — see [SECURITY.md](SECURITY.md). Security is Priority #1 for us.
<p align="center">
<img src="docs/graph-ui-screenshot.png" alt="Graph visualization UI showing the codebase-memory-mcp knowledge graph" width="800">
@@ -391,12 +391,16 @@ scripts/test.sh --suites <name> # one suite, incremental, seconds
build/c/test-runner --list-suites # what is available
```
`scripts/test.sh` is the same entry the CI gates run, so a local pass means the same thing a CI pass does. Packaging a release archive locally uses the same canonical script the release pipeline calls:
`scripts/test.sh` is the same entry the CI gates run, so a local pass means the same thing a CI pass does. The canonical local artifact-flow check builds both stripped/unstripped candidates, defaults to the stripped candidate for this explicitly unscanned local run, packages those exact bytes, extracts the archive, and smokes it:
```bash
scripts/package-release.sh <linux|darwin|windows> <amd64|arm64>
scripts/ci/smoke-artifact.sh <linux|darwin|windows> <amd64|arm64>
```
`scripts/package-release.sh` is intentionally a lower-level immutable boundary:
it accepts only an already-final `--selected-binary` plus its
`--expected-sha256`; it never builds, strips, signs, or relinks the executable.
### Manual MCP Configuration
<details>
@@ -790,7 +794,7 @@ internal/cbm/ Vendored tree-sitter grammars (158 languages) + AST extrac
Every release is verified through a multi-layer pipeline before publication:
- **VirusTotal** — every distinct extracted release file and unpacked UI asset is scanned; downloadable `.tar.gz`/`.zip` containers are not submitted (minimum 50 decisive engines; zero malicious and zero suspicious required). Release notes retain archive SHA-256 provenance and link durable public association, exact-scan-set, and per-object result files containing every measured engine count and VirusTotal report URL.
- **VirusTotal** — all 24 executable candidates (unstripped, debug-stripped and stripped) across the eight release products are scanned before smoke/soak (clean is preferred and only the single-Microsoft `!ml` tolerance documented in [SECURITY.md](SECURITY.md#our-release-policy) may pass; the number of engines that returned a decisive result is recorded as evidence but is VirusTotal fleet availability, not a pass condition). The selected executable is packaged without changing its SHA-256, and release notes link every candidate result and the selection evidence. Every distinct object extracted from the shipped containers — `install.sh`, `install.ps1`, `LICENSE`, `THIRD_PARTY_NOTICES.md`, the MCPB `manifest.json` and the unpacked UI assets — is then scanned as well, so the full published surface is covered, not just the executables.
- **SLSA Level 3** — cryptographic build provenance generated by the trusted GitHub Actions build workflow; verify with `gh attestation verify <file> --repo DeusData/codebase-memory-mcp --signer-workflow DeusData/codebase-memory-mcp/.github/workflows/_build.yml`
- **Sigstore cosign** — keyless signatures on all artifacts; bundles included in every release
- **SHA-256 checksums** — `checksums.txt` published with every release; verified by both install scripts before extraction
+45 -6
View File
@@ -126,7 +126,7 @@ Releases are created as **drafts** (invisible to users) and only published after
2. **Sigstore cosign signing** — keyless digital signatures verifiable by anyone
3. **SBOM** — Software Bill of Materials (SPDX) listing all vendored dependencies
4. **SHA-256 checksums** — published with every release
5. **VirusTotal scanning** — every distinct extracted member and unpacked UI asset is scanned; downloadable `.tar.gz`/`.zip` containers are not submitted. Each completed analysis must contain at least 50 decisive engine results, zero malicious verdicts, and zero suspicious verdicts. Release notes retain archive SHA-256 provenance and link durable public association, exact-scan-set, per-object-result, and evidence-checksum assets.
5. **VirusTotal scanning** — three behaviourally identical executable candidates — unstripped, debug-stripped and stripped — are derived from one linker output for every release product and scanned before smoke/soak. Zero malicious and zero suspicious is preferred; the only tolerated result is exactly one Microsoft malicious label ending in `!ml`, which is disclosed. The selected executable is packaged without changing its SHA-256. After packaging, every distinct object extracted from the 14 shipped containers is scanned under the same policy — `install.sh`, `install.ps1`, `LICENSE`, `THIRD_PARTY_NOTICES.md`, the MCPB `manifest.json` and the unpacked UI assets — so the covered surface is everything we publish, not only the executables. Release notes link every candidate result and the selection evidence.
6. **OpenSSF Scorecard** — repository security health score
Scope of the SLSA claim: this is a build provenance claim for release
@@ -137,9 +137,10 @@ not mean the source code is vulnerability-free or that maintainers cannot change
source. Consumers should verify the signer workflow, not only repository
ownership.
If any antivirus engine flags any scanned release object, the release stays as a
draft and is not published until the issue is investigated and resolved. There
is no exception path in this release gate.
If a scanned executable candidate has any result outside the one narrowly reviewed
Microsoft `!ml` tolerance, the release stays as a draft and is not published.
There is no manual bypass around that release gate; its sole machine-enforced
tolerance and selection table are specified in [Our release policy](#our-release-policy).
### Code-Level Defenses
@@ -167,7 +168,7 @@ cosign verify-blob --bundle <file>.bundle <file>
# SHA-256 checksum
sha256sum -c checksums.txt
# VirusTotal (follow the durable per-object results link in the release notes)
# VirusTotal (follow the durable per-candidate result links in the release notes)
# https://www.virustotal.com/
```
@@ -255,7 +256,7 @@ Every release ships the material needed to check our artifacts independently.
Use the commands in [Verification](#verification) above: SLSA Build Level 3
provenance ties the archive to the workflow run that produced it, Sigstore cosign
verifies the signature, and `checksums.txt` pins the bytes. The release notes
carry a durable per-object VirusTotal link for every scanned file, including any
carry a durable VirusTotal link for every executable candidate, including any
tolerated detection — we publish those results whether or not they are clean.
You can also rebuild from source and compare: `scripts/build.sh --with-ui`
@@ -269,6 +270,44 @@ label, any other vendor, or any "suspicious" verdict blocks the release. That
rule is enforced in `scripts/ci/check-virustotal.sh` and pinned by
`tests/test_vt_gate_policy_contract.sh`.
Before smoke and soak testing, each of the eight supported platform/link-mode
products is built in two conventional forms from the same linked executable:
stripped and unstripped. Both candidates are scanned, and selection is confined
to that product tuple; for example, a portable static Linux build can never be
substituted for the ordinary dynamically linked Linux build.
The selection policy is deterministic:
| Stripped candidate | Unstripped candidate | Selected candidate |
|---|---|---|
| Clean | Clean | Stripped |
| Clean | One tolerated Microsoft `!ml` | Stripped |
| One tolerated Microsoft `!ml` | Clean | Unstripped |
| One tolerated Microsoft `!ml` | One tolerated Microsoft `!ml` | Stripped |
Any other verdict, an incomplete scan, a missing sibling, or a provenance/hash
mismatch blocks the complete release. The selected bytes are then packaged
without stripping, signing, relinking, or any other content mutation, and the
resulting archives—not rejected candidates—are what smoke and soak testing execute.
After packaging, every archive and MCPB executable is extracted and its SHA-256
must still equal the selected candidate. This identity check makes a second
VirusTotal submission of the same bytes unnecessary.
This is not obfuscation: stripping is a standard release transformation, both
forms and their SHA-256 provenance are recorded, and no binary is modified in
response to an engine result. If the unstripped form is selected, it is larger
and can expose compiler symbol names and build-time path metadata (but not source
file contents by itself). Stripping also changes intentionally observable
developer-facing surfaces such as symbolized backtraces, debugger visibility,
and executable-symbol lookup; the two forms are therefore not claimed to be
byte-for-byte or introspection-equivalent. They are conventional copy/strip
outputs of one linker result, and the actually selected form—not its sibling—is
subjected to the full smoke/soak gate. That trade-off is visible in the
published selection evidence.
Policy identifier: `cbm-vt-candidate-selection-v1`. It is reviewed at least
every 90 days; last review: 2026-08-13, next review due: 2026-11-11.
### If you find something real
We would genuinely rather be wrong in public than confidently wrong. If you find
+5 -4
View File
@@ -165,7 +165,7 @@
"name": "Does codebase-memory-mcp send my code anywhere?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. All indexing and querying happen 100% locally on your machine. There is no embedded LLM and no API key — your MCP client acts as the intelligence layer. Release archives are signed and checksummed; every distinct extracted member and unpacked UI asset is submitted to VirusTotal, while downloadable .tar.gz/.zip containers are not. Release notes record measured 0/N results, with at least 50 decisive engines and zero malicious or suspicious verdicts required."
"text": "No. All indexing and querying happen 100% locally on your machine. There is no embedded LLM and no API key — your MCP client acts as the intelligence layer. Before release testing, three behaviourally identical executable candidates (unstripped, debug-stripped, stripped) for every platform and link mode are submitted to VirusTotal. The selected candidate is packaged with its SHA-256 unchanged, and release notes link every measured result. Publication permits only the documented single-Microsoft !ml tolerance."
}
},
{
@@ -791,9 +791,10 @@
<h3>Does codebase-memory-mcp send my code anywhere?</h3>
<p>No. All indexing and querying happen 100% locally. There is no embedded LLM and no API key. Release
archives are signed and checksummed; every distinct extracted member and unpacked UI asset is submitted
to VirusTotal, while downloadable <code>.tar.gz</code>/<code>.zip</code> containers are not. Release notes record the scanned files' measured 0/N
results; at least 50 decisive engines and zero malicious or suspicious verdicts are required.</p>
archives are signed and checksummed. Before testing, three behaviourally identical executable candidates
(unstripped, debug-stripped, stripped) for every platform and link mode are submitted to VirusTotal; the selected candidate is packaged with its
SHA-256 unchanged. Release notes link every measured result. Publication permits only the
documented single-Microsoft <code>!ml</code> tolerance.</p>
<h3>Does it support semantic or natural-language code search?</h3>
<p>Yes. Alongside structural and full-text search, <code>search_graph</code>'s <code>semantic_query</code>
+5 -1
View File
@@ -15,4 +15,8 @@ CI and the local infrastructure — both of which the venue-parity contract
| `generate-sbom.py` | The release SPDX SBOM (vendored versions reviewable here, diffable by vendoring PRs — was inline YAML). | `release.yml` |
| `require-all-green.sh` | The aggregate gate: fail unless every needed job succeeded or legitimately skipped (was inline YAML). | `pr.yml ci-ok` |
| `verify-shard-union.sh` | Prove sharded test legs lost nothing: shard count agreement, indices 1..n, identical suite lists, union of slices == full list (was inline YAML). | `_test.yml` shard-completeness |
| `check-virustotal.sh` | Exact extracted-file VirusTotal lookups. | `release.yml` |
| `prepare-release-candidates.sh` | Copy one linker output into stripped/unstripped candidates, finalize signatures, composition-check them without execution, and record their hashes. | `_build.yml`, local artifact smoke |
| `stage-release-candidates.py` | Admit exactly eight candidate artifacts / sixteen byte-distinct binaries into the content-addressed VirusTotal scan set. | `_build.yml` |
| `select-release-candidates.py` | Apply the reviewed tuple-local VT truth table, or the explicit dry-run stripped default, and atomically copy one content-bound binary per target. | `_build.yml` |
| `verify-release-selection.py` | Recompute the selection policy and prove every executable member in all 14 public containers equals its selected SHA-256. | `_build.yml`, `release.yml` final draft verification |
| `check-virustotal.sh` | Poll and validate the exact candidate scan set, enforce engine coverage and the narrow documented Microsoft `!ml` policy, and emit content-bound results evidence. | `_build.yml` |
+216 -213
View File
@@ -1,22 +1,22 @@
#!/usr/bin/env bash
# Idempotently publish the exact VirusTotal scope and measured result evidence
# after check-virustotal.sh has passed.
# Publish the already-completed candidate scans and tuple-local selections.
set -euo pipefail
: "${GH_TOKEN:?append-vt-notes: GH_TOKEN is required}"
: "${VERSION:?append-vt-notes: VERSION is required}"
: "${GITHUB_REPOSITORY:?append-vt-notes: GITHUB_REPOSITORY is required}"
VT_ASSOCIATIONS="${VT_ASSOCIATIONS:-binaries/associations.tsv}"
VT_RESULTS_PATH="${VT_RESULTS_PATH:-binaries/vt-results.tsv}"
VT_CANDIDATES="${VT_CANDIDATES:-release-candidates.tsv}"
VT_RESULTS_PATH="${VT_RESULTS_PATH:-virustotal-candidate-results.tsv}"
RELEASE_SELECTION="${RELEASE_SELECTION:-release-selection.tsv}"
WORK="$(mktemp -d "${TMPDIR:-/tmp}/cbm-vt-notes.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT
gh release view "$VERSION" \
--json body --jq '.body // ""' --repo "$GITHUB_REPOSITORY" > "$WORK/current.md"
gh release view "$VERSION" --json body --jq '.body // ""' \
--repo "$GITHUB_REPOSITORY" > "$WORK/current.md"
python3 - "$VT_ASSOCIATIONS" "$VT_RESULTS_PATH" "$WORK/current.md" "$WORK/updated.md" \
"$GITHUB_REPOSITORY" "$VERSION" <<'PY'
python3 - "$VT_CANDIDATES" "$VT_RESULTS_PATH" "$RELEASE_SELECTION" \
"$WORK/current.md" "$WORK/updated.md" "$GITHUB_REPOSITORY" "$VERSION" <<'PY'
from __future__ import annotations
import csv
@@ -24,262 +24,265 @@ import pathlib
import re
import sys
import urllib.parse
from typing import Dict, List, Sequence, Tuple
START = "<!-- cbm-security-verification:start -->"
END = "<!-- cbm-security-verification:end -->"
ASSOCIATION_FIELDS = (
"association_type",
"archive",
"archive_sha256",
"variant",
"kind",
"member",
"asset_path",
"mime",
"scan_path",
"object_sha256",
"size",
TARGETS = (
"linux-amd64",
"linux-arm64",
"linux-amd64-portable",
"linux-arm64-portable",
"darwin-amd64",
"darwin-arm64",
"windows-amd64",
"windows-arm64",
)
VARIANTS = ("unstripped", "debug-stripped", "stripped")
FIELD_KEY = {variant: variant.replace("-", "_") for variant in VARIANTS}
CANDIDATE_FIELDS = (
"target", "variant", "relative_path", "source_sha256", "pre_sign_sha256",
"sha256", "size", "format", "architecture", "linkage", "transform",
"signature", "strip_tool", "strip_version", "pair_verification", "scan_path",
)
RESULT_FIELDS = (
"scan_path",
"sha256",
"size",
"association_count",
"completed_engines",
"total_engines",
"malicious",
"suspicious",
"analysis_id",
"microsoft_category",
"microsoft_engine_version",
"microsoft_engine_update",
"virustotal_url",
"scan_path", "sha256", "size", "association_count", "completed_engines",
"total_engines", "malicious", "suspicious", "analysis_id",
"microsoft_category", "microsoft_result", "policy_classification",
"microsoft_engine_version", "microsoft_engine_update", "virustotal_url",
)
SELECTION_FIELDS = (
"target", "selected_variant", "selected_path", "selected_sha256",
"selected_size", "decision", "unstripped_sha256", "unstripped_scan_path",
"unstripped_classification", "unstripped_analysis_id",
"unstripped_virustotal_url",
"debug_stripped_sha256", "debug_stripped_scan_path",
"debug_stripped_classification", "debug_stripped_analysis_id",
"debug_stripped_virustotal_url",
"stripped_sha256", "stripped_scan_path",
"stripped_classification", "stripped_analysis_id", "stripped_virustotal_url",
)
SHA256 = re.compile(r"[0-9a-f]{64}\Z")
def fail(message: str) -> None:
raise SystemExit(f"append-vt-notes: {message}")
def read_tsv(
path: pathlib.Path,
marker: str,
fields: Sequence[str],
) -> Tuple[Dict[str, int], List[Dict[str, str]]]:
def read_tsv(path: pathlib.Path, marker: str, fields: tuple[str, ...]):
if path.is_symlink() or not path.is_file() or path.stat().st_size > 16 * 1024 * 1024:
fail(f"missing, unsafe or oversized evidence file: {path}")
lines = path.read_text(encoding="utf-8").splitlines()
text = path.read_text(encoding="utf-8")
if "\x00" in text or "\r" in text:
fail(f"forbidden control bytes in evidence file: {path}")
lines = text.splitlines()
if not lines or lines[0] != f"# {marker}":
fail(f"wrong evidence marker in {path}")
metadata: Dict[str, int] = {}
metadata: dict[str, str] = {}
cursor = 1
while cursor < len(lines) and lines[cursor].startswith("# "):
key, separator, raw_value = lines[cursor][2:].partition("=")
if not separator or not key or key in metadata or not raw_value.isdigit():
key, separator, value = lines[cursor][2:].partition("=")
if not separator or not key or not value or key in metadata:
fail(f"malformed evidence metadata in {path}")
metadata[key] = int(raw_value)
metadata[key] = value
cursor += 1
reader = csv.DictReader(lines[cursor:], delimiter="\t")
if tuple(reader.fieldnames or ()) != tuple(fields):
if tuple(reader.fieldnames or ()) != fields:
fail(f"unexpected TSV header in {path}")
rows = list(reader)
if any(None in row or any(row.get(field) is None for field in fields) for row in rows):
fail(f"TSV row has missing or surplus cells in {path}")
if not rows:
fail(f"empty evidence rows in {path}")
fail(f"missing or surplus TSV cells in {path}")
return metadata, rows
association_path = pathlib.Path(sys.argv[1])
candidates_path = pathlib.Path(sys.argv[1])
results_path = pathlib.Path(sys.argv[2])
current_path = pathlib.Path(sys.argv[3])
updated_path = pathlib.Path(sys.argv[4])
repository = sys.argv[5]
version = sys.argv[6]
selection_path = pathlib.Path(sys.argv[3])
current_path = pathlib.Path(sys.argv[4])
updated_path = pathlib.Path(sys.argv[5])
repository = sys.argv[6]
version = sys.argv[7]
if re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repository) is None or not version:
fail("unsafe repository or version for public evidence links")
fail("unsafe repository or version")
candidate_meta, candidates = read_tsv(
candidates_path, "cbm-release-candidates-v1", CANDIDATE_FIELDS
)
result_meta, results = read_tsv(
results_path, "cbm-virustotal-results-v2", RESULT_FIELDS
)
selection_meta, selections = read_tsv(
selection_path, "cbm-release-selection-v1", SELECTION_FIELDS
)
expected_pairs = [(target, variant) for target in TARGETS for variant in VARIANTS]
if candidate_meta != {"targets": str(len(TARGETS)), "candidates": str(len(TARGETS) * len(VARIANTS))}:
fail("candidate metadata does not bind the canonical target/variant matrix")
if [(row["target"], row["variant"]) for row in candidates] != expected_pairs:
fail("candidate rows are not the canonical target/variant matrix")
expected_scans = str(len(TARGETS) * len(VARIANTS))
if result_meta.get("scan_objects") != expected_scans or result_meta.get("associations") != expected_scans:
fail("VirusTotal results do not cover every candidate")
if selection_meta != {"policy": "virustotal-v2", "targets": str(len(TARGETS)), "candidates": str(len(TARGETS) * len(VARIANTS))}:
fail("selection was not produced by the scanned release policy")
if [row["target"] for row in selections] != list(TARGETS):
fail("selection rows are not the canonical target matrix")
candidate_by_pair = {(row["target"], row["variant"]): row for row in candidates}
candidate_by_path = {row["scan_path"]: row for row in candidates}
expected_candidates = len(TARGETS) * len(VARIANTS)
if len(candidate_by_pair) != expected_candidates or len(candidate_by_path) != expected_candidates:
fail("candidate evidence contains duplicates")
results_by_path: dict[str, dict[str, str]] = {}
for result in results:
candidate = candidate_by_path.get(result["scan_path"])
if candidate is None or result["scan_path"] in results_by_path:
fail("VirusTotal evidence contains an unknown or duplicate candidate")
for field in ("size", "completed_engines", "total_engines", "malicious", "suspicious"):
if not result[field].isdigit():
fail(f"malformed numeric result for {result['scan_path']}")
if (
result["sha256"] != candidate["sha256"]
or result["size"] != candidate["size"]
or result["association_count"] != "1"
or SHA256.fullmatch(result["sha256"]) is None
or result["virustotal_url"]
!= f"https://www.virustotal.com/gui/file/{result['sha256']}/detection"
):
fail(f"VirusTotal result is not bound to candidate bytes: {result['scan_path']}")
completed = int(result["completed_engines"])
total = int(result["total_engines"])
if completed < 50 or total < completed:
fail(f"incomplete VirusTotal result: {result['scan_path']}")
classification = result["policy_classification"]
clean = (
classification == "clean"
and result["malicious"] == "0"
and result["suspicious"] == "0"
and result["microsoft_category"] in {"undetected", "harmless"}
and result["microsoft_result"] == ""
)
microsoft_ml = (
classification == "microsoft-ml"
and result["malicious"] == "1"
and result["suspicious"] == "0"
and result["microsoft_category"] == "malicious"
and result["microsoft_result"].endswith("!ml")
)
if not clean and not microsoft_ml:
fail(f"blocked VirusTotal result reached release notes: {result['scan_path']}")
if not result["analysis_id"] or not result["microsoft_engine_version"] or not result["microsoft_engine_update"]:
fail(f"incomplete VirusTotal evidence: {result['scan_path']}")
results_by_path[result["scan_path"]] = result
if set(results_by_path) != set(candidate_by_path):
fail("VirusTotal results are missing candidates")
selection_by_target: dict[str, dict[str, str]] = {}
for selection in selections:
target = selection["target"]
pair = {variant: candidate_by_pair[target, variant] for variant in VARIANTS}
pair_results = {variant: results_by_path[pair[variant]["scan_path"]] for variant in VARIANTS}
classes = {v: pair_results[v]["policy_classification"] for v in VARIANTS}
order = ("stripped", "debug-stripped", "unstripped")
clean = [v for v in order if classes[v] == "clean"]
expected_variant = clean[0] if clean else "stripped"
selected = pair[expected_variant]
if (
selection["selected_variant"] != expected_variant
or selection["selected_sha256"] != selected["sha256"]
or selection["selected_size"] != selected["size"]
):
fail(f"selection contradicts candidate results: {target}")
for variant in VARIANTS:
result = pair_results[variant]
if (
selection[f"{FIELD_KEY[variant]}_sha256"] != pair[variant]["sha256"]
or selection[f"{FIELD_KEY[variant]}_scan_path"] != pair[variant]["scan_path"]
or selection[f"{FIELD_KEY[variant]}_classification"] != result["policy_classification"]
or selection[f"{FIELD_KEY[variant]}_analysis_id"] != result["analysis_id"]
or selection[f"{FIELD_KEY[variant]}_virustotal_url"] != result["virustotal_url"]
):
fail(f"selection evidence is not bound to both candidate verdicts: {target}")
selection_by_target[target] = selection
asset_base = (
f"https://github.com/{repository}/releases/download/"
f"{urllib.parse.quote(version, safe='')}"
)
association_meta, associations = read_tsv(
association_path,
"cbm-release-scan-associations-v3",
ASSOCIATION_FIELDS,
def verdict_cell(target: str, variant: str) -> str:
result = results_by_path[candidate_by_pair[target, variant]["scan_path"]]
label = "clean" if result["policy_classification"] == "clean" else "Microsoft `!ml`"
return f"[{label}]({result['virustotal_url']})"
ml_count = sum(
result["policy_classification"] == "microsoft-ml" for result in results
)
result_meta, results = read_tsv(
results_path,
"cbm-virustotal-results-v1",
RESULT_FIELDS,
)
if association_meta.get("associations") != len(associations):
fail("association manifest count does not match its rows")
if result_meta.get("scan_objects") != len(results):
fail("VirusTotal result count does not match its rows")
if result_meta.get("associations") != len(associations):
fail("VirusTotal results do not cover the association manifest")
results_by_path: Dict[str, Dict[str, str]] = {}
association_counts: Dict[str, int] = {}
executable_paths = {
association["scan_path"]
for association in associations
if association["kind"] == "binary"
}
for association in associations:
scan_path = association["scan_path"]
association_counts[scan_path] = association_counts.get(scan_path, 0) + 1
tolerated_paths: List[str] = []
for result in results:
scan_path = result["scan_path"]
if scan_path in results_by_path:
fail(f"duplicate VirusTotal result path: {scan_path}")
for numeric in (
"size",
"association_count",
"completed_engines",
"total_engines",
"malicious",
"suspicious",
):
if not result[numeric].isdigit():
fail(f"malformed numeric result field {numeric}: {scan_path}")
# Mirrors the gate's release policy: a single Microsoft machine-learning
# verdict is tolerated and disclosed in the notes; anything else must never
# have reached publication.
tolerated = (
int(result["malicious"]) == 1
and int(result["suspicious"]) == 0
and result["microsoft_category"] == "malicious"
)
if (int(result["malicious"]) != 0 or int(result["suspicious"]) != 0) and not tolerated:
fail(f"detected object reached release-note publication: {scan_path}")
if tolerated:
tolerated_paths.append(scan_path)
if (
re.fullmatch(r"[0-9a-f]{64}", result["sha256"]) is None
or result["virustotal_url"]
!= f"https://www.virustotal.com/gui/file/{result['sha256']}/detection"
):
fail(f"result SHA/link is malformed or mismatched: {scan_path}")
if int(result["association_count"]) != association_counts.get(scan_path):
fail(f"result association count mismatch: {scan_path}")
if scan_path in executable_paths:
if result["microsoft_category"] not in {"undetected", "harmless", "malicious"}:
fail(f"executable result lacks a decisive Microsoft verdict: {scan_path}")
if not result["microsoft_engine_version"] or not result["microsoft_engine_update"]:
fail(f"executable result lacks Microsoft version evidence: {scan_path}")
results_by_path[scan_path] = result
if set(results_by_path) != set(association_counts):
fail("VirusTotal result paths differ from association paths")
for association in associations:
result = results_by_path[association["scan_path"]]
if (
result["sha256"] != association["object_sha256"]
or result["size"] != association["size"]
):
fail(f"result hash/size differs from association: {association['scan_path']}")
member_rows = [row for row in associations if row["association_type"] == "member"]
if len(member_rows) != len(associations):
fail("association manifest contains a non-extracted scan target")
archive_hashes: Dict[str, str] = {}
for association in associations:
archive = association["archive"]
archive_sha256 = association["archive_sha256"]
if not archive or re.fullmatch(r"[0-9a-f]{64}", archive_sha256) is None:
fail("member association lacks valid archive SHA provenance")
previous_sha256 = archive_hashes.get(archive)
if previous_sha256 is not None and previous_sha256 != archive_sha256:
fail(f"conflicting archive SHA provenance: {archive}")
archive_hashes[archive] = archive_sha256
if len(archive_hashes) != association_meta.get("archives"):
fail("distinct archive provenance count is inconsistent")
engine_counts = [int(row["completed_engines"]) for row in results]
minimum = min(engine_counts)
maximum = max(engine_counts)
policy = result_meta.get("min_engines_policy")
if policy is None or minimum < policy:
fail("result engine counts do not satisfy their recorded policy")
engine_range = str(minimum) if minimum == maximum else f"{minimum}–{maximum}"
engine_counts = [int(result["completed_engines"]) for result in results]
engine_range = str(min(engine_counts)) if min(engine_counts) == max(engine_counts) else f"{min(engine_counts)}–{max(engine_counts)}"
section = [
START,
"## Security Verification",
"",
(
f"VirusTotal completed **{len(results)} distinct extracted byte objects** covering "
f"**{len(associations)} exact extracted archive members**."
f"Before smoke and soak testing, VirusTotal completed **{len(TARGETS) * len(VARIANTS)} executable scans**: "
"stripped and unstripped candidates for each of the **8 release products**. "
f"Every scan had at least 50 decisive engines (observed range: {engine_range})."
),
(
f"The extraction manifest binds those associations to "
f"**{len(archive_hashes)} downloadable "
f"{'archive' if len(archive_hashes) == 1 else 'archives'}** by SHA-256 provenance. "
"Downloadable .tar.gz/.zip release containers were not submitted to VirusTotal."
"All candidates were clean."
if ml_count == 0
else f"**{ml_count} candidate(s)** had only the documented single Microsoft machine-learning `!ml` result; no other decisive engine reported malicious or suspicious."
),
"",
(
f"Every scanned object returned **0 malicious and 0 suspicious** verdicts with "
f"{engine_range} decisive engine results (required minimum: {policy})."
if not tolerated_paths
else (
f"{len(results) - len(tolerated_paths)} of {len(results)} scanned objects returned "
f"**0 malicious and 0 suspicious** verdicts with {engine_range} decisive engine "
f"results (required minimum: {policy})."
)
),
(
f"Microsoft returned a decisive clean verdict for all "
f"{len(executable_paths)} executable objects."
if not tolerated_paths
else (
f"**{len(tolerated_paths)} object(s) carry a single Microsoft machine-learning "
f"detection** (`!ml`), which this project treats as a known false positive and "
f"publishes rather than hides. Every other engine returned clean. See "
f"[Antivirus False Positives](https://github.com/{repository}"
f"/blob/main/SECURITY.md#antivirus-false-positives) for the evidence and for how to "
f"verify these artifacts yourself: "
+ ", ".join(f"`{path}`" for path in sorted(tolerated_paths))
)
),
"",
(
"Durable public evidence: "
f"[associations]({asset_base}/virustotal-associations.tsv), "
f"[exact scan set]({asset_base}/virustotal-scan-set.tsv), "
f"[per-extracted-object results and report links]({asset_base}/virustotal-results.tsv), "
f"[evidence checksums]({asset_base}/virustotal-evidence-checksums.txt)."
),
"",
"Archive SHA-256 provenance (from the extraction manifest):",
"",
"| Downloadable archive | SHA-256 provenance |",
"|---|---|",
"| Product | Stripped candidate | Unstripped candidate | Shipped |",
"|---|---|---|---|",
]
for archive, archive_sha256 in sorted(archive_hashes.items()):
section.append(f"| `{archive}` | `{archive_sha256}` |")
section.append(END)
for target in TARGETS:
selection = selection_by_target[target]
section.append(
f"| `{target}` | {verdict_cell(target, 'stripped')} | "
f"{verdict_cell(target, 'unstripped')} | `{selection['selected_variant']}` (`{selection['selected_sha256']}`) |"
)
section.extend(
[
"",
(
"Selection is tuple-local and defaults to stripped. The selected executable SHA-256 "
"was verified again after packaging; archive containers were not redundantly submitted "
"to VirusTotal. Their hashes remain available in `checksums.txt`."
),
"",
(
"Durable evidence: "
f"[candidate provenance]({asset_base}/release-candidates.tsv), "
f"[candidate VirusTotal results]({asset_base}/virustotal-candidate-results.tsv), "
f"[selection decisions]({asset_base}/release-selection.tsv)."
),
END,
]
)
replacement = "\n".join(section)
current = current_path.read_text(encoding="utf-8")
start_count = current.count(START)
end_count = current.count(END)
if start_count != end_count or start_count > 1:
if current.count(START) != current.count(END) or current.count(START) > 1:
fail("existing release notes contain malformed verification markers")
if start_count == 1:
if START in current:
if current.index(START) >= current.index(END):
fail("existing release-note verification markers are reversed")
pattern = re.compile(re.escape(START) + r".*?" + re.escape(END), re.DOTALL)
updated, replacements = pattern.subn(replacement, current, count=1)
if replacements != 1:
fail("existing release-note verification section could not be replaced")
fail("existing verification markers are reversed")
updated, count = re.subn(
re.escape(START) + r".*?" + re.escape(END),
replacement,
current,
count=1,
flags=re.DOTALL,
)
if count != 1:
fail("could not replace existing verification section")
else:
updated = current.rstrip() + ("\n\n" if current.strip() else "") + replacement + "\n"
updated_path.write_text(updated, encoding="utf-8")
PY
gh release edit "$VERSION" \
--notes-file "$WORK/updated.md" --repo "$GITHUB_REPOSITORY"
gh release edit "$VERSION" --notes-file "$WORK/updated.md" --repo "$GITHUB_REPOSITORY"
+7 -6
View File
@@ -67,9 +67,10 @@ UPDATER_NEEDLES=(
# SQLite loadable extensions = arbitrary code execution through a database
# file. The amalgamation is built with SQLITE_OMIT_LOAD_EXTENSION; if that ever
# drops out, dlopen()/LoadLibrary() re-enters the store layer.
# NOTE: release binaries are stripped, so the two API symbol names only appear
# when the surface is *exported*. The dlopen error text is the needle that
# still works on a stripped build — it is compiled in only when the feature is.
# NOTE: the stripped release candidate can erase non-exported symbol names, so
# the two API names are not a sufficient proof on their own. The dlopen error
# text still works after stripping — it is compiled in only with the feature.
# When the unstripped candidate is inspected, the symbol checks are stronger.
SQLITE_LOADEXT_NEEDLES=(
'sqlite3_load_extension'
'sqlite3_enable_load_extension'
@@ -343,9 +344,9 @@ check_file() {
for needle in "${SQLITE_LOADEXT_NEEDLES[@]}"; do
assert_absent "$file" "$token" A4-no-sqlite-loadext "$needle"
done
# Positive control for A4: the three needles above are all *absent* on a
# stripped build even when the feature is compiled in, so on its own that
# assertion can pass vacuously. SQLite's own compile-option table names
# Positive control for A4: the symbol needles can be absent on a stripped
# candidate even when the feature is compiled in, so that candidate's
# absence assertion can pass vacuously. SQLite's own compile-option table names
# every OMIT_* it was built with — when that table is present (it ships
# unless SQLITE_OMIT_COMPILEOPTION_DIAGS is set) it proves the omission
# positively rather than by absence.
+66 -12
View File
@@ -3,7 +3,8 @@
# content-bound scan set produced by extract-release-archives.sh.
#
# Required: VT_API_KEY, VT_ANALYSIS, VT_EXPECTED_SCAN_SET, VT_ASSOCIATIONS
# Workflow policy: MIN_ENGINES=50, zero malicious, zero suspicious.
# Workflow policy: MIN_ENGINES=50, clean or exactly one disclosed Microsoft
# machine-learning (`!ml`) verdict; every other detection blocks.
# Optional: VT_RESULTS_PATH, VT_REQUEST_INTERVAL_SECONDS,
# VT_POLL_TIMEOUT_SECONDS, VT_CURL_TIMEOUT_SECONDS.
set -euo pipefail
@@ -75,6 +76,8 @@ RESULT_FIELDS = (
"suspicious",
"analysis_id",
"microsoft_category",
"microsoft_result",
"policy_classification",
"microsoft_engine_version",
"microsoft_engine_update",
"virustotal_url",
@@ -124,8 +127,10 @@ class CompletedResult:
malicious: int
suspicious: int
microsoft_category: str
microsoft_result: str
microsoft_engine_version: str
microsoft_engine_update: str
detections: Tuple[Tuple[str, str, str, str, str], ...]
def required_env(name: str) -> str:
@@ -421,6 +426,27 @@ def parse_completed(document: object, submission: Submission) -> Tuple[str, Opti
response_id = data.get("id")
if not isinstance(response_id, str) or ANALYSIS_ID_RE.fullmatch(response_id) is None:
raise GateError(f"VirusTotal response has an invalid analysis id: {submission.expected.scan_path}")
# The id is recorded as evidence, NOT required to equal the submitted one.
#
# VirusTotal is content-addressed, and it recognising our bytes is the
# behaviour we WANT, not a problem to defend against. The evidence this
# pipeline publishes is the hash-keyed file report — append-vt-notes.sh
# asserts the URL is exactly .../gui/file/<sha256>/detection — so what we
# gate on and what a reader sees when they look that SHA-256 up themselves
# are the same report. Demanding a freshly minted analysis id would have
# contradicted the evidence we publish alongside it.
#
# It also does not work in practice: requiring equality blocked a real
# dry-run on the 282 MB linux-arm64 candidate, exactly the kind of large,
# previously seen artifact VirusTotal answers for from its own record, so it
# would have recurred on most releases.
#
# What must hold is that the verdict describes THESE bytes, and the
# completed branch below enforces that against file_info.sha256 and size —
# a strictly stronger binding than an id. It is also what keeps a tuple's
# two candidates apart: stripped and unstripped differ in hash by
# construction, so neither can be read as the other whatever ids VirusTotal
# hands out (the wrong-hash and wrong-size contract cases pin this).
attributes = data.get("attributes")
if not isinstance(attributes, dict):
raise GateError("VirusTotal response has no analysis attributes")
@@ -460,6 +486,7 @@ def parse_completed(document: object, submission: Submission) -> Tuple[str, Opti
detections: List[Tuple[str, str, str, str, str]] = []
results = attributes.get("results")
microsoft_category = ""
microsoft_result = ""
microsoft_engine_version = ""
microsoft_engine_update = ""
detail_detections = {"malicious": 0, "suspicious": 0}
@@ -480,6 +507,7 @@ def parse_completed(document: object, submission: Submission) -> Tuple[str, Opti
detections.append((one_line(engine), label, str(category), version, updated))
if engine == "Microsoft":
microsoft_category = str(category)
microsoft_result = one_line(result.get("result") or "")
microsoft_engine_version = required_one_line_string(
result.get("engine_version"), "Microsoft.engine_version"
)
@@ -517,8 +545,10 @@ def parse_completed(document: object, submission: Submission) -> Tuple[str, Opti
malicious,
suspicious,
microsoft_category,
microsoft_result,
microsoft_engine_version,
microsoft_engine_update,
tuple(detections),
),
detections,
)
@@ -565,7 +595,7 @@ def write_results(
temporary = pathlib.Path(temporary_name)
try:
with os.fdopen(fd, "w", encoding="utf-8", newline="") as handle:
handle.write("# cbm-virustotal-results-v1\n")
handle.write("# cbm-virustotal-results-v2\n")
handle.write(f"# scan_objects={len(results)}\n")
handle.write(f"# associations={associations}\n")
handle.write(f"# min_engines_policy={min_engines}\n")
@@ -592,6 +622,8 @@ def write_results(
"suspicious": item.suspicious,
"analysis_id": item.submission.analysis_id,
"microsoft_category": item.microsoft_category,
"microsoft_result": item.microsoft_result,
"policy_classification": classify_result(item, min_engines),
"microsoft_engine_version": item.microsoft_engine_version,
"microsoft_engine_update": item.microsoft_engine_update,
"virustotal_url": f"https://www.virustotal.com/gui/file/{expected.sha256}/detection",
@@ -619,13 +651,35 @@ def write_results(
# Everything else still fails the release: two or more engines, any label that
# is not `!ml` (a signature hit is a real finding), any non-Microsoft engine,
# any suspicious verdict, and every infrastructure error.
def is_tolerated_detection(result, detections) -> bool:
if result.suspicious or result.malicious != 1 or len(detections) != 1:
def is_tolerated_detection(result: CompletedResult) -> bool:
if result.suspicious or result.malicious != 1 or len(result.detections) != 1:
return False
engine, label, category, _version, _updated = detections[0]
engine, label, category, _version, _updated = result.detections[0]
return engine == "Microsoft" and category == "malicious" and label.endswith("!ml")
# Classification depends ONLY on what engines found, never on how many answered.
#
# "hard" means an engine actually found something we will not ship: two or more
# engines, any non-Microsoft engine, any label that is not `!ml`, or anything
# suspicious.
#
# How many engines returned a decisive result is NOT a policy input. It is a
# property of VirusTotal's fleet on the day, which we cannot influence: these
# binaries are ~300 MB and many engines skip or time out at that size, so the
# count varies run to run (observed on one run: one object at 48, the other
# fifteen spread 59-68). A 50-engine floor turned that variance into a release
# blocker — it failed an 8-target release on a windows-arm64 candidate with
# ZERO detections whose own sibling scanned clean at 66. Gating on it makes
# shipping a lottery decided by someone else's infrastructure, so the count is
# recorded as evidence and nothing more.
def classify_result(result: CompletedResult, min_engines: int) -> str:
del min_engines # retained for signature stability; not a policy input
if result.malicious or result.suspicious:
return "microsoft-ml" if is_tolerated_detection(result) else "hard"
return "clean"
def main() -> None:
api_key = required_env("VT_API_KEY")
raw_analysis = required_env("VT_ANALYSIS")
@@ -689,16 +743,16 @@ def main() -> None:
print(f" {submission.expected.scan_path}: {status}; will retry round-robin")
continue
completed.append(result)
tolerated = is_tolerated_detection(result, detections)
tolerated = is_tolerated_detection(result)
if result.completed_engines < min_engines:
message = (
f"{submission.expected.scan_path} completed with only "
# Reported, never fatal. See classify_result: engine count is
# VirusTotal's fleet availability, not a property of our binary.
print(
f"NOTE: {submission.expected.scan_path} was judged by "
f"{result.completed_engines}/{result.total_engines} decisive engines "
f"(< {min_engines})"
f"(below the {min_engines} reference); verdict still applies"
)
failures.append(message)
print(f"BLOCKED: {message}")
elif result.malicious or result.suspicious:
if result.malicious or result.suspicious:
message = (
f"{submission.expected.scan_path} flagged "
f"({result.malicious} malicious, {result.suspicious} suspicious / "
+271
View File
@@ -0,0 +1,271 @@
#!/usr/bin/env bash
# Derive the three final, immutable release candidates for one existing product
# tuple from a single linker output. Strip/signing happens here and nowhere
# after this boundary.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
usage() {
cat <<'EOF'
Usage: scripts/ci/prepare-release-candidates.sh <goos> <goarch> \
--binary FILE --out-dir DIR
Derive three final executables - unstripped, debug-stripped and stripped - for
exactly one of the eight release product tuples. All three come from the same
linker output
and are finalized before hashing and atomic publication. Runtime testing starts
only after VirusTotal selection, using the selected candidate.
Environment:
STRIP exact strip executable override
CODESIGN exact codesign executable override (Darwin only)
EOF
}
GOOS=""
GOARCH=""
BINARY=""
OUT_DIR=""
expect=""
for arg in "$@"; do
case "$expect" in
binary) BINARY="$arg"; expect=""; continue ;;
out-dir) OUT_DIR="$arg"; expect=""; continue ;;
esac
case "$arg" in
-h | --help) usage; exit 0 ;;
--binary) expect=binary ;;
--binary=*) BINARY="${arg#--binary=}" ;;
--out-dir) expect=out-dir ;;
--out-dir=*) OUT_DIR="${arg#--out-dir=}" ;;
-*) echo "prepare-release-candidates: unknown option '$arg'" >&2; exit 2 ;;
*)
if [ -z "$GOOS" ]; then GOOS="$arg"
elif [ -z "$GOARCH" ]; then GOARCH="$arg"
else echo "prepare-release-candidates: unexpected argument '$arg'" >&2; exit 2
fi
;;
esac
done
[ -z "$expect" ] || { echo "prepare-release-candidates: --$expect needs a value" >&2; exit 2; }
[ -n "$GOOS" ] && [ -n "$GOARCH" ] && [ -n "$BINARY" ] && [ -n "$OUT_DIR" ] || {
usage >&2
exit 2
}
TARGET="$GOOS-$GOARCH"
case "$TARGET" in
linux-amd64 | linux-arm64 | linux-amd64-portable | linux-arm64-portable | \
darwin-amd64 | darwin-arm64 | windows-amd64 | windows-arm64) ;;
*) echo "prepare-release-candidates: unsupported release target '$TARGET'" >&2; exit 2 ;;
esac
[ -f "$BINARY" ] && [ ! -L "$BINARY" ] || {
echo "prepare-release-candidates: input is not a regular non-symlink file: $BINARY" >&2
exit 2
}
command -v python3 >/dev/null 2>&1 || {
echo "prepare-release-candidates: python3 is required" >&2
exit 2
}
BINARY="$(cd "$(dirname "$BINARY")" && pwd -P)/$(basename "$BINARY")"
mkdir -p "$OUT_DIR"
OUT_DIR="$(cd "$OUT_DIR" && pwd -P)"
PUBLISHED="$OUT_DIR/$TARGET"
[ ! -e "$PUBLISHED" ] && [ ! -L "$PUBLISHED" ] || {
echo "prepare-release-candidates: refusing to overwrite $PUBLISHED" >&2
exit 2
}
sha256_file() {
python3 - "$1" <<'PY'
import hashlib
import pathlib
import sys
digest = hashlib.sha256()
with pathlib.Path(sys.argv[1]).open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
print(digest.hexdigest())
PY
}
one_line() {
LC_ALL=C tr '\t\r\n' ' ' | sed 's/ */ /g; s/^ //; s/ $//'
}
resolve_strip() {
if [ -n "${STRIP:-}" ]; then
[ -x "$STRIP" ] || command -v "$STRIP" >/dev/null 2>&1 || {
echo "prepare-release-candidates: explicit STRIP is not executable: $STRIP" >&2
return 1
}
command -v "$STRIP" 2>/dev/null || printf '%s\n' "$STRIP"
return
fi
local candidate
for candidate in llvm-strip strip; do
if command -v "$candidate" >/dev/null 2>&1; then
command -v "$candidate"
return
fi
done
echo "prepare-release-candidates: no strip tool is available" >&2
return 1
}
tool_version() {
local tool="$1" value=""
value="$({ "$tool" --version || "$tool" -V || true; } 2>&1 | head -n 1 | one_line)"
[ -n "$value" ] || value="$(basename "$tool") (version unavailable)"
printf '%s\n' "$value"
}
STRIP_TOOL="$(resolve_strip)" || exit 2
STRIP_VERSION="$(tool_version "$STRIP_TOOL")"
SOURCE_SHA="$(sha256_file "$BINARY")"
WORK="$(mktemp -d "$OUT_DIR/.${TARGET}.candidates.XXXXXX")"
cleanup() {
local status=$?
trap - EXIT
rm -rf "$WORK"
exit "$status"
}
trap cleanup EXIT
STAGE="$WORK/$TARGET"
BINARY_NAME=codebase-memory-mcp
[ "$GOOS" = windows ] && BINARY_NAME=codebase-memory-mcp.exe
mkdir -p "$STAGE/unstripped" "$STAGE/debug-stripped" "$STAGE/stripped"
UNSTRIPPED="$STAGE/unstripped/$BINARY_NAME"
DEBUG_STRIPPED="$STAGE/debug-stripped/$BINARY_NAME"
STRIPPED="$STAGE/stripped/$BINARY_NAME"
cp "$BINARY" "$UNSTRIPPED"
cp "$BINARY" "$DEBUG_STRIPPED"
cp "$BINARY" "$STRIPPED"
chmod 0755 "$UNSTRIPPED" "$DEBUG_STRIPPED" "$STRIPPED"
STRIP_INVOCATION="$(basename "$STRIP_TOOL") --strip-all"
if ! "$STRIP_TOOL" --strip-all "$STRIPPED" 2>/dev/null; then
# Apple's strip rejects GNU/LLVM --strip-all. Plain Apple strip is the
# measured equivalent; no weaker flag combination is allowed. Restore the
# pristine linker output first because a failed tool may have written.
if [ "$GOOS" != darwin ]; then
echo "prepare-release-candidates: $STRIP_TOOL --strip-all failed" >&2
exit 2
fi
cp "$BINARY" "$STRIPPED"
chmod 0755 "$STRIPPED"
"$STRIP_TOOL" "$STRIPPED"
STRIP_INVOCATION="$(basename "$STRIP_TOOL")"
fi
# Third candidate: debug information removed, symbol table kept. Behaviourally
# identical to the other two — same linker output, nothing but metadata differs
# — but a distinct byte image, so VirusTotal scans it as its own file.
#
# That is the entire point. Release-run evidence shows the single tolerated
# Microsoft `!ml` verdict landing on stripped and unstripped candidates of the
# SAME build essentially at random (they disagreed on 4 of 8 targets, in both
# directions), so each variant is an independent draw. With two candidates one
# target came back flagged on both and had no clean binary to ship; a third
# independent draw makes that outcome substantially rarer.
DEBUG_STRIP_INVOCATION="$(basename "$STRIP_TOOL") --strip-debug"
if ! "$STRIP_TOOL" --strip-debug "$DEBUG_STRIPPED" 2>/dev/null; then
# Apple's strip rejects --strip-debug; -S is its measured equivalent.
if [ "$GOOS" != darwin ]; then
echo "prepare-release-candidates: $STRIP_TOOL --strip-debug failed" >&2
exit 2
fi
cp "$BINARY" "$DEBUG_STRIPPED"
chmod 0755 "$DEBUG_STRIPPED"
"$STRIP_TOOL" -S "$DEBUG_STRIPPED"
DEBUG_STRIP_INVOCATION="$(basename "$STRIP_TOOL") -S"
fi
UNSTRIPPED_PRE_SIGN_SHA="$(sha256_file "$UNSTRIPPED")"
DEBUG_STRIPPED_PRE_SIGN_SHA="$(sha256_file "$DEBUG_STRIPPED")"
STRIPPED_PRE_SIGN_SHA="$(sha256_file "$STRIPPED")"
SIGNATURE=not-applicable
if [ "$GOOS" = darwin ]; then
CODESIGN_TOOL="${CODESIGN:-codesign}"
command -v "$CODESIGN_TOOL" >/dev/null 2>&1 || {
echo "prepare-release-candidates: codesign is required for Darwin candidates" >&2
exit 2
}
CODESIGN_TOOL="$(command -v "$CODESIGN_TOOL")"
for candidate in "$UNSTRIPPED" "$DEBUG_STRIPPED" "$STRIPPED"; do
"$CODESIGN_TOOL" --sign - --force --timestamp=none "$candidate"
"$CODESIGN_TOOL" --verify --strict "$candidate"
done
SIGNATURE=adhoc-verified
fi
UNSTRIPPED_SHA="$(sha256_file "$UNSTRIPPED")"
DEBUG_STRIPPED_SHA="$(sha256_file "$DEBUG_STRIPPED")"
STRIPPED_SHA="$(sha256_file "$STRIPPED")"
# All three must differ: identical candidates would be one draw wearing three
# hats, and the selector would believe it had alternatives it does not have.
if [ "$UNSTRIPPED_SHA" = "$STRIPPED_SHA" ] || \
[ "$UNSTRIPPED_SHA" = "$DEBUG_STRIPPED_SHA" ] || \
[ "$DEBUG_STRIPPED_SHA" = "$STRIPPED_SHA" ]; then
echo "prepare-release-candidates: strip did not produce byte-distinct candidates" >&2
exit 2
fi
for candidate in "$UNSTRIPPED" "$DEBUG_STRIPPED" "$STRIPPED"; do
bash "$ROOT/scripts/ci/check-binary-composition.sh" "$candidate" >/dev/null
done
[ "$(sha256_file "$BINARY")" = "$SOURCE_SHA" ] || {
echo "prepare-release-candidates: linker output changed during derivation" >&2
exit 2
}
[ "$(sha256_file "$UNSTRIPPED")" = "$UNSTRIPPED_SHA" ] && \
[ "$(sha256_file "$DEBUG_STRIPPED")" = "$DEBUG_STRIPPED_SHA" ] && \
[ "$(sha256_file "$STRIPPED")" = "$STRIPPED_SHA" ] || {
echo "prepare-release-candidates: a final candidate changed during validation" >&2
exit 2
}
case "$GOOS" in
linux) FORMAT=elf ;;
darwin) FORMAT=macho ;;
windows) FORMAT=pe ;;
esac
ARCHITECTURE="${GOARCH%-portable}"
case "$TARGET" in
linux-*-portable) LINKAGE=portable ;;
linux-*) LINKAGE=dynamic ;;
*) LINKAGE=native ;;
esac
UNSTRIPPED_SIZE="$(wc -c < "$UNSTRIPPED" | tr -d '[:space:]')"
DEBUG_STRIPPED_SIZE="$(wc -c < "$DEBUG_STRIPPED" | tr -d '[:space:]')"
STRIPPED_SIZE="$(wc -c < "$STRIPPED" | tr -d '[:space:]')"
MANIFEST="$STAGE/candidate-provenance.tsv"
{
printf '# cbm-release-candidate-provenance-v1\n'
printf 'target\tvariant\trelative_path\tsource_sha256\tpre_sign_sha256\tsha256\tsize\tformat\tarchitecture\tlinkage\ttransform\tsignature\tstrip_tool\tstrip_version\tpair_verification\n'
printf '%s\tunstripped\tunstripped/%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\tcopy\t%s\t%s\t%s\tsame-linker-output-v1\n' \
"$TARGET" "$BINARY_NAME" "$SOURCE_SHA" "$UNSTRIPPED_PRE_SIGN_SHA" \
"$UNSTRIPPED_SHA" "$UNSTRIPPED_SIZE" "$FORMAT" "$ARCHITECTURE" \
"$LINKAGE" "$SIGNATURE" "$STRIP_INVOCATION" "$STRIP_VERSION"
printf '%s\tdebug-stripped\tdebug-stripped/%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\tstrip-debug\t%s\t%s\t%s\tsame-linker-output-v1\n' \
"$TARGET" "$BINARY_NAME" "$SOURCE_SHA" "$DEBUG_STRIPPED_PRE_SIGN_SHA" \
"$DEBUG_STRIPPED_SHA" "$DEBUG_STRIPPED_SIZE" "$FORMAT" "$ARCHITECTURE" \
"$LINKAGE" "$SIGNATURE" "$DEBUG_STRIP_INVOCATION" "$STRIP_VERSION"
printf '%s\tstripped\tstripped/%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\tstrip\t%s\t%s\t%s\tsame-linker-output-v1\n' \
"$TARGET" "$BINARY_NAME" "$SOURCE_SHA" "$STRIPPED_PRE_SIGN_SHA" \
"$STRIPPED_SHA" "$STRIPPED_SIZE" "$FORMAT" "$ARCHITECTURE" \
"$LINKAGE" "$SIGNATURE" "$STRIP_INVOCATION" "$STRIP_VERSION"
} > "$MANIFEST"
chmod 0555 "$UNSTRIPPED" "$DEBUG_STRIPPED" "$STRIPPED"
chmod 0444 "$MANIFEST"
mv "$STAGE" "$PUBLISHED"
echo "prepare-release-candidates: published $TARGET unstripped/debug-stripped/stripped candidates"
+3 -4
View File
@@ -9,10 +9,9 @@
# Publishing the same bytes under the legacy names makes already-released
# updaters work again with no action from the user.
#
# Runs AFTER the VirusTotal gate: the aliases are copies of archives the gate
# already cleared, and uploading them earlier would duplicate every object in
# the scan set and in the provenance manifest. They are likewise absent from
# checksums.txt, which covers the canonical names current installers request.
# Runs after verification: each alias is a copy of a container whose executable
# was hash-bound to the selected, pre-smoke VirusTotal candidate. They are absent
# from checksums.txt, which covers the canonical names current installers request.
#
# Usage: publish-legacy-aliases.sh <version-tag> <repository>
set -euo pipefail
+606
View File
@@ -0,0 +1,606 @@
#!/usr/bin/env python3
"""Select one content-bound candidate inside every release target tuple."""
from __future__ import annotations
import argparse
import csv
import hashlib
import os
import pathlib
import re
import shutil
import stat
import sys
import tempfile
from collections.abc import Iterable, Sequence
class ContractError(Exception):
"""Candidate or VirusTotal evidence is incomplete or incoherent."""
TARGETS = (
"linux-amd64",
"linux-arm64",
"linux-amd64-portable",
"linux-arm64-portable",
"darwin-amd64",
"darwin-arm64",
"windows-amd64",
"windows-arm64",
)
VARIANTS = ("unstripped", "debug-stripped", "stripped")
PROVENANCE_FIELDS = (
"target",
"variant",
"relative_path",
"source_sha256",
"pre_sign_sha256",
"sha256",
"size",
"format",
"architecture",
"linkage",
"transform",
"signature",
"strip_tool",
"strip_version",
"pair_verification",
)
CANDIDATE_FIELDS = (*PROVENANCE_FIELDS, "scan_path")
RESULT_FIELDS = (
"scan_path",
"sha256",
"size",
"association_count",
"completed_engines",
"total_engines",
"malicious",
"suspicious",
"analysis_id",
"microsoft_category",
"microsoft_result",
"policy_classification",
"microsoft_engine_version",
"microsoft_engine_update",
"virustotal_url",
)
SELECTION_FIELDS = (
"target",
"selected_variant",
"selected_path",
"selected_sha256",
"selected_size",
"decision",
"unstripped_sha256",
"unstripped_scan_path",
"unstripped_classification",
"unstripped_analysis_id",
"unstripped_virustotal_url",
"debug_stripped_sha256",
"debug_stripped_scan_path",
"debug_stripped_classification",
"debug_stripped_analysis_id",
"debug_stripped_virustotal_url",
"stripped_sha256",
"stripped_scan_path",
"stripped_classification",
"stripped_analysis_id",
"stripped_virustotal_url",
)
SHA256_RE = re.compile(r"\A[0-9a-f]{64}\Z")
ANALYSIS_ID_RE = re.compile(r"\A[A-Za-z0-9_+/-]+={0,2}\Z")
MAX_MANIFEST_BYTES = 16 * 1024 * 1024
MAX_CANDIDATE_BYTES = 512 * 1024 * 1024
MIN_ENGINES = 50
def parse_arguments(argv: Sequence[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Select one VT-approved candidate per canonical target tuple."
)
parser.add_argument("--candidates", required=True, type=pathlib.Path)
parser.add_argument("--objects-dir", required=True, type=pathlib.Path)
parser.add_argument("--out-dir", required=True, type=pathlib.Path)
policy = parser.add_mutually_exclusive_group(required=True)
policy.add_argument("--results", type=pathlib.Path)
policy.add_argument("--default-stripped", action="store_true")
return parser.parse_args(argv[1:])
def regular_status(path: pathlib.Path, *, ceiling: int, label: str) -> os.stat_result:
try:
status = path.lstat()
except FileNotFoundError as error:
raise ContractError(f"missing {label}: {path}") from error
if not stat.S_ISREG(status.st_mode):
raise ContractError(f"{label} is not a regular non-symlink file: {path}")
if status.st_size > ceiling:
raise ContractError(f"{label} exceeds the {ceiling}-byte ceiling: {path}")
return status
def parse_tsv(
path: pathlib.Path,
*,
marker: str,
fields: Sequence[str],
) -> tuple[dict[str, str], list[dict[str, str]]]:
regular_status(path, ceiling=MAX_MANIFEST_BYTES, label="evidence manifest")
try:
text = path.read_text(encoding="utf-8")
except UnicodeError as error:
raise ContractError(f"evidence manifest is not UTF-8: {path}") from error
if "\x00" in text or "\r" in text:
raise ContractError(f"evidence manifest contains forbidden control bytes: {path}")
lines = text.splitlines()
if not lines or lines[0] != f"# {marker}":
raise ContractError(f"evidence marker is missing: {marker}")
metadata: dict[str, str] = {}
cursor = 1
while cursor < len(lines) and lines[cursor].startswith("# "):
raw = lines[cursor][2:]
key, separator, value = raw.partition("=")
if (
not separator
or not key
or not value
or key in metadata
or any(ord(character) < 32 for character in raw)
):
raise ContractError(f"malformed or duplicate evidence metadata: {raw!r}")
metadata[key] = value
cursor += 1
reader = csv.DictReader(lines[cursor:], delimiter="\t")
if tuple(reader.fieldnames or ()) != tuple(fields):
raise ContractError(f"evidence TSV header is malformed or unexpected: {path}")
rows = list(reader)
if any(
None in row
or any(row.get(field) is None for field in fields)
or any(
any(ord(character) < 32 for character in value)
for value in row.values()
if value
)
for row in rows
):
raise ContractError(f"evidence TSV has missing or surplus cells: {path}")
return metadata, rows
def expected_properties(target: str) -> tuple[str, str, str, str]:
binary = "codebase-memory-mcp.exe" if target.startswith("windows-") else "codebase-memory-mcp"
file_format = "pe" if target.startswith("windows-") else "macho" if target.startswith("darwin-") else "elf"
architecture = target.split("-")[1]
linkage = (
"portable"
if target.endswith("-portable")
else "dynamic"
if target.startswith("linux-")
else "native"
)
return binary, file_format, architecture, linkage
def sha256_file(path: pathlib.Path, *, expected_size: int) -> str:
status = regular_status(path, ceiling=MAX_CANDIDATE_BYTES, label="candidate object")
if status.st_size != expected_size:
raise ContractError(f"candidate object size changed: {path}")
digest = hashlib.sha256()
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(path, flags)
try:
opened = os.fstat(descriptor)
if not stat.S_ISREG(opened.st_mode) or (opened.st_dev, opened.st_ino) != (
status.st_dev,
status.st_ino,
):
raise ContractError(f"candidate object changed while being opened: {path}")
with os.fdopen(descriptor, "rb", closefd=False) as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
finally:
os.close(descriptor)
return digest.hexdigest()
def validate_candidate_row(row: dict[str, str], *, target: str, variant: str) -> None:
binary, file_format, architecture, linkage = expected_properties(target)
exact = {
"target": target,
"variant": variant,
"relative_path": f"{variant}/{binary}",
"format": file_format,
"architecture": architecture,
"linkage": linkage,
"transform": {
"unstripped": "copy",
"debug-stripped": "strip-debug",
"stripped": "strip",
}[variant],
"signature": "adhoc-verified" if target.startswith("darwin-") else "not-applicable",
"pair_verification": "same-linker-output-v1",
}
for field, expected in exact.items():
if row[field] != expected:
raise ContractError(f"candidate {target}/{variant} has invalid {field}")
for field in ("source_sha256", "pre_sign_sha256", "sha256"):
if SHA256_RE.fullmatch(row[field]) is None:
raise ContractError(f"candidate {target}/{variant} has invalid {field}")
if not row["size"].isdigit() or not 0 < int(row["size"]) <= MAX_CANDIDATE_BYTES:
raise ContractError(f"candidate {target}/{variant} has invalid size")
scan_path = pathlib.PurePosixPath(row["scan_path"])
if (
scan_path.is_absolute()
or len(scan_path.parts) != 2
or scan_path.parts[0] != "objects"
or scan_path.parts[1] != row["sha256"]
or scan_path.as_posix() != row["scan_path"]
):
raise ContractError(f"candidate {target}/{variant} has invalid scan path")
for field in ("strip_tool", "strip_version"):
if not row[field] or any(ord(character) < 32 for character in row[field]):
raise ContractError(f"candidate {target}/{variant} has invalid {field}")
def load_candidates(
manifest: pathlib.Path, objects_dir: pathlib.Path
) -> tuple[list[dict[str, str]], dict[str, pathlib.Path]]:
metadata, rows = parse_tsv(
manifest,
marker="cbm-release-candidates-v1",
fields=CANDIDATE_FIELDS,
)
if metadata != {"targets": str(len(TARGETS)), "candidates": str(len(TARGETS) * len(VARIANTS))}:
raise ContractError("candidate metadata does not bind the exact canonical matrix")
expected_pairs = [(target, variant) for target in TARGETS for variant in VARIANTS]
if [(row["target"], row["variant"]) for row in rows] != expected_pairs:
raise ContractError("candidate rows are not the exact canonical target/variant matrix")
try:
objects_status = objects_dir.lstat()
except FileNotFoundError as error:
raise ContractError(f"candidate object directory is missing: {objects_dir}") from error
if not stat.S_ISDIR(objects_status.st_mode):
raise ContractError(f"candidate object path is not a regular directory: {objects_dir}")
object_paths: dict[str, pathlib.Path] = {}
seen_hashes: set[str] = set()
for row, (target, variant) in zip(rows, expected_pairs):
validate_candidate_row(row, target=target, variant=variant)
if row["sha256"] in seen_hashes:
raise ContractError("candidate object hashes are not unique")
seen_hashes.add(row["sha256"])
path = objects_dir / pathlib.PurePosixPath(row["scan_path"]).name
actual_sha256 = sha256_file(path, expected_size=int(row["size"]))
if actual_sha256 != row["sha256"]:
raise ContractError(f"candidate object hash changed: {row['scan_path']}")
object_paths[row["scan_path"]] = path
actual_names: set[str] = set()
for path in objects_dir.iterdir():
if path.is_symlink() or not path.is_file():
raise ContractError(f"unexpected non-regular candidate object: {path}")
actual_names.add(path.name)
expected_names = {path.name for path in object_paths.values()}
if actual_names != expected_names or len(actual_names) != len(rows):
raise ContractError("candidate object directory is missing objects or contains surplus entries")
for target in TARGETS:
pair = [row for row in rows if row["target"] == target]
if len({row["source_sha256"] for row in pair}) != 1:
raise ContractError(f"candidate siblings do not share source provenance: {target}")
if pair[0]["pre_sign_sha256"] != pair[0]["source_sha256"]:
raise ContractError(
f"unstripped pre-sign bytes do not match source provenance: {target}"
)
if not target.startswith("darwin-") and any(
row["pre_sign_sha256"] != row["sha256"] for row in pair
):
raise ContractError(
f"non-Darwin candidate changed between pre-sign and final bytes: {target}"
)
if pair[0]["sha256"] == pair[1]["sha256"]:
raise ContractError(f"candidate siblings are not byte-distinct: {target}")
return rows, object_paths
def parse_nonnegative(row: dict[str, str], field: str, *, scan_path: str) -> int:
value = row[field]
if not value.isdigit():
raise ContractError(f"VT result has invalid {field}: {scan_path}")
return int(value)
def validate_result_row(row: dict[str, str], candidate: dict[str, str]) -> None:
scan_path = candidate["scan_path"]
if (
row["scan_path"] != scan_path
or row["sha256"] != candidate["sha256"]
or row["size"] != candidate["size"]
or row["association_count"] != "1"
):
raise ContractError(f"VT result is not bound to candidate object: {scan_path}")
if SHA256_RE.fullmatch(row["sha256"]) is None:
raise ContractError(f"VT result has invalid SHA-256: {scan_path}")
completed = parse_nonnegative(row, "completed_engines", scan_path=scan_path)
total = parse_nonnegative(row, "total_engines", scan_path=scan_path)
malicious = parse_nonnegative(row, "malicious", scan_path=scan_path)
suspicious = parse_nonnegative(row, "suspicious", scan_path=scan_path)
# `total < completed` stays: that is an incoherent response. A low decisive
# count does NOT — how many engines answered is VirusTotal fleet
# availability on the day, not a property of this binary, and gating on it
# made an 8-target release fail on a candidate with zero detections.
if total < completed:
raise ContractError(f"VT result has incoherent engine coverage: {scan_path}")
if ANALYSIS_ID_RE.fullmatch(row["analysis_id"]) is None:
raise ContractError(f"VT result has invalid analysis id: {scan_path}")
for field in ("microsoft_engine_version", "microsoft_engine_update"):
if not row[field] or any(ord(character) < 32 for character in row[field]):
raise ContractError(f"VT result has incomplete Microsoft evidence: {scan_path}")
expected_url = f"https://www.virustotal.com/gui/file/{row['sha256']}/detection"
if row["virustotal_url"] != expected_url:
raise ContractError(f"VT result URL is not content-bound: {scan_path}")
classification = row["policy_classification"]
if classification == "clean":
coherent = (
malicious == 0
and suspicious == 0
and row["microsoft_category"] in {"undetected", "harmless"}
and row["microsoft_result"] == ""
)
elif classification == "microsoft-ml":
coherent = (
malicious == 1
and suspicious == 0
and row["microsoft_category"] == "malicious"
and bool(row["microsoft_result"])
and row["microsoft_result"].endswith("!ml")
)
else:
raise ContractError(f"VT result is a hard or unknown policy block: {scan_path}")
if not coherent:
raise ContractError(f"VT classification contradicts its result details: {scan_path}")
def load_results(
path: pathlib.Path, candidates: Sequence[dict[str, str]]
) -> dict[str, dict[str, str]]:
metadata, rows = parse_tsv(
path,
marker="cbm-virustotal-results-v2",
fields=RESULT_FIELDS,
)
expected_count = len(candidates)
required_metadata = {
"scan_objects": expected_count,
"associations": expected_count,
"min_engines_policy": MIN_ENGINES,
}
for key, expected in required_metadata.items():
if metadata.get(key) != str(expected):
raise ContractError(f"VT results metadata has invalid {key}")
if set(metadata) != {
"scan_objects",
"associations",
"min_engines_policy",
"min_completed_engines",
"max_completed_engines",
}:
raise ContractError("VT results metadata fields are incomplete or unexpected")
if len(rows) != expected_count:
raise ContractError("VT results do not contain exactly one row per candidate")
by_path: dict[str, dict[str, str]] = {}
analysis_ids: set[str] = set()
candidate_by_path = {row["scan_path"]: row for row in candidates}
for row in rows:
candidate = candidate_by_path.get(row["scan_path"])
if candidate is None or row["scan_path"] in by_path:
raise ContractError(f"VT results contain a surplus or duplicate row: {row['scan_path']}")
validate_result_row(row, candidate)
if row["analysis_id"] in analysis_ids:
raise ContractError("VT results reuse one analysis id for multiple objects")
analysis_ids.add(row["analysis_id"])
by_path[row["scan_path"]] = row
if set(by_path) != set(candidate_by_path):
raise ContractError("VT results are missing candidate objects")
completed = [int(row["completed_engines"]) for row in rows]
if (
not metadata["min_completed_engines"].isdigit()
or not metadata["max_completed_engines"].isdigit()
or int(metadata["min_completed_engines"]) != min(completed)
or int(metadata["max_completed_engines"]) != max(completed)
):
raise ContractError("VT results engine-range metadata does not match its rows")
return by_path
def write_tsv(
path: pathlib.Path,
*,
metadata: Iterable[tuple[str, object]],
rows: Iterable[dict[str, object]],
) -> None:
with path.open("x", encoding="utf-8", newline="") as handle:
handle.write("# cbm-release-selection-v1\n")
for key, value in metadata:
handle.write(f"# {key}={value}\n")
writer = csv.DictWriter(
handle,
fieldnames=SELECTION_FIELDS,
delimiter="\t",
lineterminator="\n",
extrasaction="raise",
)
writer.writeheader()
writer.writerows(rows)
handle.flush()
os.fsync(handle.fileno())
def copy_selected(
source: pathlib.Path,
destination: pathlib.Path,
*,
expected_sha256: str,
expected_size: int,
) -> None:
if sha256_file(source, expected_size=expected_size) != expected_sha256:
raise ContractError(f"candidate object changed before selection copy: {source}")
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
before = regular_status(source, ceiling=MAX_CANDIDATE_BYTES, label="candidate object")
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(source, flags)
try:
opened = os.fstat(descriptor)
if not stat.S_ISREG(opened.st_mode) or (opened.st_dev, opened.st_ino) != (
before.st_dev,
before.st_ino,
):
raise ContractError(f"candidate object changed while being opened: {source}")
digest = hashlib.sha256()
total = 0
with os.fdopen(descriptor, "rb", closefd=False) as input_handle, destination.open(
"xb"
) as output_handle:
while True:
chunk = input_handle.read(1024 * 1024)
if not chunk:
break
total += len(chunk)
if total > expected_size:
raise ContractError(f"candidate object grew during selection: {source}")
digest.update(chunk)
output_handle.write(chunk)
output_handle.flush()
os.fsync(output_handle.fileno())
finally:
os.close(descriptor)
if total != expected_size or digest.hexdigest() != expected_sha256:
raise ContractError(f"candidate object changed during selection copy: {source}")
destination.chmod(0o555)
if sha256_file(destination, expected_size=expected_size) != expected_sha256:
raise ContractError(f"selected copy is not content-bound: {destination}")
def main(argv: Sequence[str]) -> None:
args = parse_arguments(argv)
output = args.out_dir
if output.name in ("", ".", ".."):
raise ContractError(f"unsafe output directory: {output}")
if os.path.lexists(output):
raise ContractError(f"refusing to overwrite output path: {output}")
candidates, object_paths = load_candidates(args.candidates, args.objects_dir)
results = load_results(args.results, candidates) if args.results else None
policy = "virustotal-v2" if results is not None else "unscanned-dry-run"
candidate_by_tuple = {
(row["target"], row["variant"]): row for row in candidates
}
output.parent.mkdir(parents=True, exist_ok=True)
temporary = pathlib.Path(
tempfile.mkdtemp(prefix=f".{output.name}.select-", dir=str(output.parent))
)
staged = temporary / "bundle"
staged.mkdir(mode=0o700)
selection_rows: list[dict[str, object]] = []
try:
for target in TARGETS:
pair = {
variant: candidate_by_tuple[target, variant] for variant in VARIANTS
}
if results is None:
classifications = {
variant: "unscanned-dry-run" for variant in VARIANTS
}
selected_variant = "stripped"
decision = "stripped-unscanned-dry-run"
else:
classifications = {
variant: results[pair[variant]["scan_path"]]["policy_classification"]
for variant in VARIANTS
}
# Preference order is fixed and content-independent: the
# smallest artifact first, then progressively more metadata.
# A `hard` classification never reaches here - the gate fails
# the release before selection - so every candidate below is
# either clean or a tolerated single Microsoft `!ml`.
#
# The variants are behaviourally identical, so this picks on
# verdict alone: the first clean one in preference order, and
# only if EVERY candidate drew the tolerated verdict do we ship
# a flagged one (still the smallest).
order = ("stripped", "debug-stripped", "unstripped")
clean = [v for v in order if classifications[v] == "clean"]
if clean:
selected_variant = clean[0]
if selected_variant == "stripped":
decision = "stripped-preferred"
else:
others = "-".join(
f"{v}:{classifications[v]}" for v in order if v != selected_variant
)
decision = f"{selected_variant}-clean-after-{others}"
else:
selected_variant = "stripped"
decision = "stripped-all-candidates-microsoft-ml"
selected = pair[selected_variant]
binary_name = expected_properties(target)[0]
selected_relative = f"selected/{target}/{binary_name}"
copy_selected(
object_paths[selected["scan_path"]],
staged / selected_relative,
expected_sha256=selected["sha256"],
expected_size=int(selected["size"]),
)
evidence: dict[str, object] = {
"target": target,
"selected_variant": selected_variant,
"selected_path": selected_relative,
"selected_sha256": selected["sha256"],
"selected_size": selected["size"],
"decision": decision,
}
for variant in VARIANTS:
# Field prefixes use underscores; the variant name is the
# on-disk directory and keeps its hyphen ("debug-stripped").
key = variant.replace("-", "_")
candidate = pair[variant]
result = results[candidate["scan_path"]] if results is not None else None
evidence[f"{key}_sha256"] = candidate["sha256"]
evidence[f"{key}_scan_path"] = candidate["scan_path"]
evidence[f"{key}_classification"] = classifications[variant]
evidence[f"{key}_analysis_id"] = result["analysis_id"] if result else ""
evidence[f"{key}_virustotal_url"] = result["virustotal_url"] if result else ""
selection_rows.append(evidence)
write_tsv(
staged / "release-selection.tsv",
metadata=(
("policy", policy),
("targets", len(TARGETS)),
("candidates", len(candidates)),
),
rows=selection_rows,
)
if os.path.lexists(output):
raise ContractError(f"output path appeared during selection: {output}")
os.replace(staged, output)
finally:
shutil.rmtree(temporary, ignore_errors=True)
print(f"selected one candidate for each of {len(TARGETS)} targets under {policy}: {output}")
try:
main(sys.argv)
except (ContractError, OSError, UnicodeError, ValueError) as error:
print(f"select-release-candidates: {error}", file=sys.stderr)
raise SystemExit(1)
+51 -9
View File
@@ -5,7 +5,8 @@
# packaging/archive-layout bug class could first appear in a release dry run.
#
# This driver reproduces the release flow end to end with local bytes:
# scripts/build.sh → scripts/package-release.sh → extract →
# build → derive stripped/unstripped pair → default-select stripped →
# byte-preserving package → extract →
# the SAME canonical wrapper the remote venue runs, in artifact mode
# (CBM_SMOKE_ARTIFACT_DIR), whose completeness checks make a broken or
# incomplete archive a loud failure.
@@ -19,14 +20,15 @@ usage() {
cat <<'EOF'
Usage: scripts/ci/smoke-artifact.sh <goos> <goarch> [VAR=VAL ...]
Build → package (scripts/package-release.sh) → extract → smoke the EXTRACTED
artifact through the canonical wrapper, exactly like the release venue:
Build → derive both release candidates → select stripped (local, unscanned) →
package immutable bytes → extract → smoke the EXTRACTED artifact through the
canonical wrapper, exactly like the release venue:
unix: scripts/smoke-local.sh with CBM_SMOKE_ARTIFACT_DIR
windows: test-infrastructure/vm/vm-smoke.sh with CBM_SMOKE_ARTIFACT_DIR
(run inside the VM/CI msys2 shell)
Make passthrough (VAR=VAL): CC= CXX= STATIC=1 ... forwarded to build steps.
Environment: BUILD_DIR (default build/c) — build tree used for the archive.
Environment: BUILD_DIR (default build/c) — build tree containing linker output.
On failure the work directory is preserved for post-mortem (path printed).
EOF
}
@@ -34,7 +36,6 @@ EOF
GOOS=""
GOARCH=""
BUILD_ARGS=()
expect_value=""
for arg in "$@"; do
case "$arg" in
-h | --help) usage; exit 0 ;;
@@ -67,9 +68,6 @@ export BUILD_DIR
scripts/build.sh ${UI_FLAG[@]+"${UI_FLAG[@]}"} \
BUILD_DIR="$BUILD_DIR" ${BUILD_ARGS[@]+"${BUILD_ARGS[@]}"}
if [ "$GOOS" = "darwin" ]; then
codesign --sign - --force "$BUILD_DIR/codebase-memory-mcp"
fi
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cbm-smoke-artifact.XXXXXX")"
cleanup() {
@@ -84,8 +82,52 @@ cleanup() {
}
trap cleanup EXIT
SOURCE_BINARY="$BUILD_DIR/codebase-memory-mcp"
if [ "$GOOS" = "windows" ] && [ -f "${SOURCE_BINARY}.exe" ]; then
SOURCE_BINARY="${SOURCE_BINARY}.exe"
fi
[ -f "$SOURCE_BINARY" ] || {
echo "smoke-artifact: build completed without expected binary: $SOURCE_BINARY" >&2
exit 2
}
CANDIDATE_ROOT="$WORK_DIR/candidates"
scripts/ci/prepare-release-candidates.sh "$GOOS" "$GOARCH" \
--binary "$SOURCE_BINARY" --out-dir "$CANDIDATE_ROOT"
SELECTED_NAME="codebase-memory-mcp"
[ "$GOOS" = "windows" ] && SELECTED_NAME="codebase-memory-mcp.exe"
SELECTED_BINARY="$CANDIDATE_ROOT/${GOOS}-${GOARCH}/stripped/$SELECTED_NAME"
[ -f "$SELECTED_BINARY" ] || {
echo "smoke-artifact: candidate derivation did not produce $SELECTED_BINARY" >&2
exit 2
}
PROVENANCE="$CANDIDATE_ROOT/${GOOS}-${GOARCH}/candidate-provenance.tsv"
SELECTED_SHA256="$(python3 - "$PROVENANCE" <<'PY'
import csv
import pathlib
import sys
with pathlib.Path(sys.argv[1]).open(encoding="utf-8", newline="") as handle:
lines = handle.read().splitlines()
rows = list(csv.DictReader(lines[1:], delimiter="\t"))
matches = [row for row in rows if row.get("variant") == "stripped"]
if len(matches) != 1:
raise SystemExit("smoke-artifact: provenance does not contain exactly one stripped row")
print(matches[0]["sha256"])
PY
)"
echo "=== smoke-artifact: unscanned-local-smoke default selected stripped $GOOS-$GOARCH ($SELECTED_SHA256) ==="
# Generate notices while the build's graph-ui/node_modules tree is available;
# the release build similarly carries this file alongside candidate artifacts.
NOTICES="$WORK_DIR/THIRD_PARTY_NOTICES.md"
scripts/gen-third-party-notices.sh "$NOTICES"
scripts/package-release.sh "$GOOS" "$GOARCH" \
--out-dir "$WORK_DIR" ${BUILD_ARGS[@]+"${BUILD_ARGS[@]}"}
--selected-binary "$SELECTED_BINARY" \
--expected-sha256 "$SELECTED_SHA256" \
--third-party-notices "$NOTICES" \
--out-dir "$WORK_DIR"
NAME="codebase-memory-mcp-${GOOS}-${GOARCH}"
EXTRACT_DIR="$WORK_DIR/extract"
+488
View File
@@ -0,0 +1,488 @@
#!/usr/bin/env python3
"""Validate and atomically stage the exact release-candidate scan set."""
from __future__ import annotations
import argparse
import csv
import hashlib
import os
import pathlib
import re
import shutil
import stat
import sys
import tempfile
from collections.abc import Iterable, Sequence
class ContractError(Exception):
"""The candidate boundary is incomplete, unsafe, or incoherent."""
TARGETS = (
"linux-amd64",
"linux-arm64",
"linux-amd64-portable",
"linux-arm64-portable",
"darwin-amd64",
"darwin-arm64",
"windows-amd64",
"windows-arm64",
)
VARIANTS = ("unstripped", "debug-stripped", "stripped")
PROVENANCE_FIELDS = (
"target",
"variant",
"relative_path",
"source_sha256",
"pre_sign_sha256",
"sha256",
"size",
"format",
"architecture",
"linkage",
"transform",
"signature",
"strip_tool",
"strip_version",
"pair_verification",
)
CANDIDATE_FIELDS = (*PROVENANCE_FIELDS, "scan_path")
SCAN_SET_FIELDS = (
"scan_path",
"sha256",
"size",
"association_count",
"association_kinds",
)
ASSOCIATION_FIELDS = (
"association_type",
"archive",
"archive_sha256",
"variant",
"kind",
"member",
"asset_path",
"mime",
"scan_path",
"object_sha256",
"size",
)
SHA256_RE = re.compile(r"\A[0-9a-f]{64}\Z")
MAX_MANIFEST_BYTES = 1024 * 1024
MAX_CANDIDATE_BYTES = 512 * 1024 * 1024
MAX_NOTICE_BYTES = 16 * 1024 * 1024
def parse_arguments(argv: Sequence[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Stage the exact eight-target, sixteen-candidate VT scan set."
)
parser.add_argument("source_dir", type=pathlib.Path)
parser.add_argument("output_dir", type=pathlib.Path)
parser.add_argument("--expect-targets", type=int, required=True)
parser.add_argument("--expect-candidates", type=int, required=True)
args = parser.parse_args(argv[1:])
if args.expect_targets != len(TARGETS):
parser.error(f"--expect-targets must be exactly {len(TARGETS)}")
if args.expect_candidates != len(TARGETS) * len(VARIANTS):
parser.error(
f"--expect-candidates must be exactly {len(TARGETS) * len(VARIANTS)}"
)
return args
def require_regular(path: pathlib.Path, *, ceiling: int, label: str) -> os.stat_result:
try:
status = path.lstat()
except FileNotFoundError as error:
raise ContractError(f"missing {label}: {path}") from error
if not stat.S_ISREG(status.st_mode):
raise ContractError(f"{label} is not a regular non-symlink file: {path}")
if status.st_size > ceiling:
raise ContractError(f"{label} exceeds the {ceiling}-byte ceiling: {path}")
return status
def ensure_clean_source_tree(source: pathlib.Path) -> list[pathlib.Path]:
try:
root_status = source.lstat()
except FileNotFoundError as error:
raise ContractError(f"candidate source directory does not exist: {source}") from error
if not stat.S_ISDIR(root_status.st_mode):
raise ContractError(f"candidate source is not a regular directory: {source}")
manifests: list[pathlib.Path] = []
for directory, names, files in os.walk(source, followlinks=False):
parent = pathlib.Path(directory)
for name in names:
child = parent / name
if child.is_symlink():
raise ContractError(f"symlinked directory in candidate source: {child}")
for name in files:
child = parent / name
if child.is_symlink():
raise ContractError(f"symlinked file in candidate source: {child}")
if name == "candidate-provenance.tsv":
manifests.append(child)
return sorted(manifests, key=lambda item: item.as_posix())
def read_provenance(path: pathlib.Path) -> list[dict[str, str]]:
require_regular(path, ceiling=MAX_MANIFEST_BYTES, label="candidate provenance")
try:
text = path.read_text(encoding="utf-8")
except UnicodeError as error:
raise ContractError(f"candidate provenance is not UTF-8: {path}") from error
if "\x00" in text or "\r" in text:
raise ContractError(f"candidate provenance contains forbidden control bytes: {path}")
lines = text.splitlines()
if not lines or lines[0] != "# cbm-release-candidate-provenance-v1":
raise ContractError(f"candidate provenance marker is missing: {path}")
if len(lines) < 2 or lines[1].startswith("# "):
raise ContractError(f"candidate provenance has unexpected metadata: {path}")
reader = csv.DictReader(lines[1:], delimiter="\t")
if tuple(reader.fieldnames or ()) != PROVENANCE_FIELDS:
raise ContractError(f"candidate provenance header changed: {path}")
rows = list(reader)
if len(rows) != len(VARIANTS):
raise ContractError(f"candidate provenance must contain exactly two rows: {path}")
if any(
None in row
or any(row.get(field) is None for field in PROVENANCE_FIELDS)
or any(
any(ord(character) < 32 for character in value)
for value in row.values()
if value
)
for row in rows
):
raise ContractError(f"candidate provenance has missing or surplus cells: {path}")
return rows
def expected_properties(target: str) -> tuple[str, str, str, str]:
binary = "codebase-memory-mcp.exe" if target.startswith("windows-") else "codebase-memory-mcp"
file_format = "pe" if target.startswith("windows-") else "macho" if target.startswith("darwin-") else "elf"
architecture = target.split("-")[1]
linkage = (
"portable"
if target.endswith("-portable")
else "dynamic"
if target.startswith("linux-")
else "native"
)
return binary, file_format, architecture, linkage
def validate_row_shape(row: dict[str, str], *, target: str, variant: str) -> pathlib.PurePosixPath:
binary, file_format, architecture, linkage = expected_properties(target)
expected_relative = f"{variant}/{binary}"
relative = pathlib.PurePosixPath(row["relative_path"])
if (
relative.is_absolute()
or relative.as_posix() != expected_relative
or any(part in ("", ".", "..") for part in relative.parts)
):
raise ContractError(
f"candidate {target}/{variant} has unsafe or noncanonical relative path"
)
exact = {
"target": target,
"variant": variant,
"format": file_format,
"architecture": architecture,
"linkage": linkage,
"transform": {
"unstripped": "copy",
"debug-stripped": "strip-debug",
"stripped": "strip",
}[variant],
"signature": "adhoc-verified" if target.startswith("darwin-") else "not-applicable",
"pair_verification": "same-linker-output-v1",
}
for field, expected in exact.items():
if row[field] != expected:
raise ContractError(
f"candidate {target}/{variant} has invalid {field}: {row[field]!r}"
)
for field in ("source_sha256", "pre_sign_sha256", "sha256"):
if SHA256_RE.fullmatch(row[field]) is None:
raise ContractError(f"candidate {target}/{variant} has invalid {field}")
if not row["size"].isdigit() or not 0 < int(row["size"]) <= MAX_CANDIDATE_BYTES:
raise ContractError(f"candidate {target}/{variant} has invalid size")
for field in ("strip_tool", "strip_version"):
value = row[field]
if not value or any(ord(character) < 32 for character in value):
raise ContractError(f"candidate {target}/{variant} has invalid {field}")
return relative
def validate_artifact_inventory(root: pathlib.Path, *, target: str) -> None:
binary = expected_properties(target)[0]
expected_root = {
"candidate-provenance.tsv",
"THIRD_PARTY_NOTICES.md",
*VARIANTS,
}
actual_root = {path.name for path in root.iterdir()}
if actual_root != expected_root:
raise ContractError(
f"candidate artifact inventory is not exact for {target}: "
f"missing={sorted(expected_root - actual_root)}, "
f"surplus={sorted(actual_root - expected_root)}"
)
for variant in VARIANTS:
variant_dir = root / variant
try:
mode = variant_dir.lstat().st_mode
except FileNotFoundError as error:
raise ContractError(f"missing candidate variant directory: {target}/{variant}") from error
if not stat.S_ISDIR(mode):
raise ContractError(f"candidate variant path is not a directory: {target}/{variant}")
if {path.name for path in variant_dir.iterdir()} != {binary}:
raise ContractError(
f"candidate variant inventory is not exact: {target}/{variant}"
)
def ingest_file(
source: pathlib.Path,
destination: pathlib.Path,
*,
expected_sha256: str,
expected_size: int,
ceiling: int,
label: str,
) -> None:
before = require_regular(source, ceiling=ceiling, label=label)
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
digest = hashlib.sha256()
total = 0
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(source, flags)
try:
opened = os.fstat(descriptor)
if not stat.S_ISREG(opened.st_mode) or (opened.st_dev, opened.st_ino) != (
before.st_dev,
before.st_ino,
):
raise ContractError(f"{label} changed while being opened: {source}")
with os.fdopen(descriptor, "rb", closefd=False) as input_handle, destination.open(
"xb"
) as output_handle:
while True:
chunk = input_handle.read(1024 * 1024)
if not chunk:
break
total += len(chunk)
if total > ceiling:
raise ContractError(f"{label} exceeds the {ceiling}-byte ceiling: {source}")
digest.update(chunk)
output_handle.write(chunk)
output_handle.flush()
os.fsync(output_handle.fileno())
finally:
os.close(descriptor)
if total != expected_size or digest.hexdigest() != expected_sha256:
raise ContractError(f"{label} does not match its provenance: {source}")
destination.chmod(0o600)
def write_tsv(
path: pathlib.Path,
*,
marker: str,
metadata: Iterable[tuple[str, object]],
fields: Sequence[str],
rows: Iterable[dict[str, object]],
) -> None:
with path.open("x", encoding="utf-8", newline="") as handle:
handle.write(f"# {marker}\n")
for key, value in metadata:
handle.write(f"# {key}={value}\n")
writer = csv.DictWriter(
handle,
fieldnames=fields,
delimiter="\t",
lineterminator="\n",
extrasaction="raise",
)
writer.writeheader()
writer.writerows(rows)
handle.flush()
os.fsync(handle.fileno())
def publish_atomically(staged: pathlib.Path, output: pathlib.Path) -> None:
if os.path.lexists(output):
raise ContractError(f"refusing to overwrite output path: {output}")
os.replace(staged, output)
def main(argv: Sequence[str]) -> None:
args = parse_arguments(argv)
source = args.source_dir
output = args.output_dir
if output.name in ("", ".", ".."):
raise ContractError(f"unsafe output directory: {output}")
if os.path.lexists(output):
raise ContractError(f"refusing to overwrite output path: {output}")
manifests = ensure_clean_source_tree(source)
if len(manifests) != args.expect_targets:
raise ContractError(
f"candidate matrix has {len(manifests)} provenance manifests; expected {args.expect_targets}"
)
by_target: dict[str, tuple[pathlib.Path, list[dict[str, str]]]] = {}
for manifest in manifests:
rows = read_provenance(manifest)
targets = {row["target"] for row in rows}
if len(targets) != 1:
raise ContractError(f"candidate provenance mixes targets: {manifest}")
target = next(iter(targets))
if target not in TARGETS or target in by_target:
raise ContractError(f"unexpected or duplicate candidate target: {target}")
if tuple(row["variant"] for row in rows) != VARIANTS:
raise ContractError(f"candidate variants are not in canonical order: {manifest}")
validate_artifact_inventory(manifest.parent, target=target)
by_target[target] = (manifest.parent, rows)
if tuple(target for target in TARGETS if target in by_target) != TARGETS:
missing = [target for target in TARGETS if target not in by_target]
raise ContractError(f"candidate matrix is missing canonical targets: {missing}")
output.parent.mkdir(parents=True, exist_ok=True)
temporary = pathlib.Path(
tempfile.mkdtemp(prefix=f".{output.name}.stage-", dir=str(output.parent))
)
staged = temporary / "bundle"
staged.mkdir(mode=0o700)
try:
candidate_rows: list[dict[str, object]] = []
association_rows: list[dict[str, object]] = []
seen_hashes: set[str] = set()
for target in TARGETS:
artifact_root, rows = by_target[target]
row_by_variant = {row["variant"]: row for row in rows}
source_hashes = {row["source_sha256"] for row in rows}
if len(source_hashes) != 1:
raise ContractError(f"candidate siblings do not share source provenance: {target}")
if rows[0]["pre_sign_sha256"] != rows[0]["source_sha256"]:
raise ContractError(
f"unstripped pre-sign bytes do not match linker-output provenance: {target}"
)
if not target.startswith("darwin-") and any(
row["pre_sign_sha256"] != row["sha256"] for row in rows
):
raise ContractError(
f"non-Darwin candidate changed between pre-sign and final bytes: {target}"
)
for variant in VARIANTS:
row = row_by_variant[variant]
relative = validate_row_shape(row, target=target, variant=variant)
candidate = artifact_root.joinpath(*relative.parts)
sha256 = row["sha256"]
if sha256 in seen_hashes:
raise ContractError(f"candidate hashes are not unique across the exact scan set: {sha256}")
seen_hashes.add(sha256)
scan_path = f"objects/{sha256}"
object_path = staged / scan_path
ingest_file(
candidate,
object_path,
expected_sha256=sha256,
expected_size=int(row["size"]),
ceiling=MAX_CANDIDATE_BYTES,
label=f"candidate {target}/{variant}",
)
candidate_rows.append({**row, "scan_path": scan_path})
association_rows.append(
{
"association_type": "member",
"archive": target,
"archive_sha256": row["source_sha256"],
"variant": variant,
"kind": "binary",
"member": row["relative_path"],
"asset_path": "",
"mime": "",
"scan_path": scan_path,
"object_sha256": sha256,
"size": row["size"],
}
)
if rows[0]["sha256"] == rows[1]["sha256"]:
raise ContractError(f"candidate siblings are not byte-distinct: {target}")
notice_source = artifact_root / "THIRD_PARTY_NOTICES.md"
notice_status = require_regular(
notice_source, ceiling=MAX_NOTICE_BYTES, label=f"third-party notice for {target}"
)
notice_destination = staged / "notices" / target / "THIRD_PARTY_NOTICES.md"
notice_destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
notice_sha = hashlib.sha256(notice_source.read_bytes()).hexdigest()
ingest_file(
notice_source,
notice_destination,
expected_sha256=notice_sha,
expected_size=notice_status.st_size,
ceiling=MAX_NOTICE_BYTES,
label=f"third-party notice for {target}",
)
notice_destination.chmod(0o444)
if len(candidate_rows) != args.expect_candidates:
raise ContractError(
f"candidate matrix has {len(candidate_rows)} rows; expected {args.expect_candidates}"
)
write_tsv(
staged / "candidates.tsv",
marker="cbm-release-candidates-v1",
metadata=(("targets", len(TARGETS)), ("candidates", len(candidate_rows))),
fields=CANDIDATE_FIELDS,
rows=candidate_rows,
)
write_tsv(
staged / "scan-set.tsv",
marker="cbm-release-scan-set-v2",
metadata=(("scan_objects", len(candidate_rows)), ("associations", len(candidate_rows))),
fields=SCAN_SET_FIELDS,
rows=(
{
"scan_path": row["scan_path"],
"sha256": row["sha256"],
"size": row["size"],
"association_count": 1,
"association_kinds": "binary",
}
for row in candidate_rows
),
)
write_tsv(
staged / "associations.tsv",
marker="cbm-release-scan-associations-v3",
metadata=(
("archives", len(TARGETS)),
("associations", len(candidate_rows)),
("scan_objects", len(candidate_rows)),
),
fields=ASSOCIATION_FIELDS,
rows=association_rows,
)
for object_path in (staged / "objects").iterdir():
object_path.chmod(0o444)
publish_atomically(staged, output)
finally:
shutil.rmtree(temporary, ignore_errors=True)
print(
f"staged exact {len(TARGETS)}-target/{len(candidate_rows)}-candidate scan set: {output}"
)
try:
main(sys.argv)
except (ContractError, OSError, UnicodeError, ValueError) as error:
print(f"stage-release-candidates: {error}", file=sys.stderr)
raise SystemExit(1)
+435
View File
@@ -0,0 +1,435 @@
#!/usr/bin/env python3
"""Reconcile every canonical public release container to selected bytes."""
from __future__ import annotations
import argparse
import csv
import hashlib
import os
import pathlib
import re
import stat
import sys
import tarfile
import zipfile
from collections.abc import Sequence
from typing import BinaryIO, Optional
class ContractError(Exception):
"""The release-container namespace or selected-byte binding is invalid."""
TARGETS = (
"linux-amd64",
"linux-arm64",
"linux-amd64-portable",
"linux-arm64-portable",
"darwin-amd64",
"darwin-arm64",
"windows-amd64",
"windows-arm64",
)
VARIANTS = ("unstripped", "debug-stripped", "stripped")
FIELD_KEY = {variant: variant.replace("-", "_") for variant in VARIANTS}
SELECTION_FIELDS = (
"target",
"selected_variant",
"selected_path",
"selected_sha256",
"selected_size",
"decision",
"unstripped_sha256",
"unstripped_scan_path",
"unstripped_classification",
"unstripped_analysis_id",
"unstripped_virustotal_url",
"debug_stripped_sha256",
"debug_stripped_scan_path",
"debug_stripped_classification",
"debug_stripped_analysis_id",
"debug_stripped_virustotal_url",
"stripped_sha256",
"stripped_scan_path",
"stripped_classification",
"stripped_analysis_id",
"stripped_virustotal_url",
)
SHA256_RE = re.compile(r"\A[0-9a-f]{64}\Z")
ANALYSIS_ID_RE = re.compile(r"\A[A-Za-z0-9_+/-]+={0,2}\Z")
MAX_MANIFEST_BYTES = 16 * 1024 * 1024
MAX_ARCHIVE_BYTES = 1024 * 1024 * 1024
MAX_MEMBER_BYTES = 512 * 1024 * 1024
MAX_TOTAL_MEMBER_BYTES = 2 * 1024 * 1024 * 1024
MAX_MEMBERS = 128
def archive_specs() -> dict[str, tuple[str, str]]:
specs: dict[str, tuple[str, str]] = {}
for target in TARGETS:
binary = "codebase-memory-mcp.exe" if target.startswith("windows-") else "codebase-memory-mcp"
suffix = ".zip" if target.startswith("windows-") else ".tar.gz"
specs[f"codebase-memory-mcp-{target}{suffix}"] = (target, binary)
if (
target.startswith(("darwin-", "windows-"))
or target.endswith("-portable")
):
specs[f"codebase-memory-mcp-{target}.mcpb"] = (
target,
f"server/{binary}",
)
return specs
ARCHIVES = archive_specs()
def parse_arguments(argv: Sequence[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Bind all fourteen canonical release containers to release-selection.tsv."
)
parser.add_argument("--selection", required=True, type=pathlib.Path)
parser.add_argument("--archive-dir", required=True, type=pathlib.Path)
parser.add_argument(
"--require-policy",
choices=("virustotal-v2", "unscanned-dry-run"),
help="reject a selection produced under any other policy",
)
return parser.parse_args(argv[1:])
def regular_status(path: pathlib.Path, *, ceiling: int, label: str) -> os.stat_result:
try:
status = path.lstat()
except FileNotFoundError as error:
raise ContractError(f"missing {label}: {path}") from error
if not stat.S_ISREG(status.st_mode):
raise ContractError(f"{label} is not a regular non-symlink file: {path}")
if status.st_size > ceiling:
raise ContractError(f"{label} exceeds the {ceiling}-byte ceiling: {path}")
return status
def load_selection(
path: pathlib.Path, *, require_policy: Optional[str] = None
) -> dict[str, dict[str, str]]:
regular_status(path, ceiling=MAX_MANIFEST_BYTES, label="release selection")
try:
text = path.read_text(encoding="utf-8")
except UnicodeError as error:
raise ContractError("release selection is not UTF-8") from error
if "\x00" in text or "\r" in text:
raise ContractError("release selection contains forbidden control bytes")
lines = text.splitlines()
if not lines or lines[0] != "# cbm-release-selection-v1":
raise ContractError("release selection marker is missing")
metadata: dict[str, str] = {}
cursor = 1
while cursor < len(lines) and lines[cursor].startswith("# "):
raw = lines[cursor][2:]
key, separator, value = raw.partition("=")
if not separator or not key or not value or key in metadata:
raise ContractError(f"malformed or duplicate release-selection metadata: {raw!r}")
metadata[key] = value
cursor += 1
if set(metadata) != {"policy", "targets", "candidates"}:
raise ContractError("release selection metadata fields are incomplete or unexpected")
if metadata["policy"] not in {"virustotal-v2", "unscanned-dry-run"}:
raise ContractError("release selection has an invalid policy")
if require_policy is not None and metadata["policy"] != require_policy:
raise ContractError(
f"release selection policy is {metadata['policy']!r}; required {require_policy!r}"
)
if metadata["targets"] != str(len(TARGETS)) or metadata["candidates"] != str(
len(TARGETS) * len(VARIANTS)
):
raise ContractError("release selection metadata does not bind the canonical matrix")
reader = csv.DictReader(lines[cursor:], delimiter="\t")
if tuple(reader.fieldnames or ()) != SELECTION_FIELDS:
raise ContractError("release selection header is malformed or unexpected")
rows = list(reader)
if any(
None in row
or any(row.get(field) is None for field in SELECTION_FIELDS)
or any(
any(ord(character) < 32 for character in value)
for value in row.values()
if value
)
for row in rows
):
raise ContractError("release selection has missing or surplus cells")
if [row["target"] for row in rows] != list(TARGETS):
raise ContractError("release selection is not the exact canonical target matrix")
selections: dict[str, dict[str, str]] = {}
analysis_ids: set[str] = set()
for row in rows:
target = row["target"]
binary = "codebase-memory-mcp.exe" if target.startswith("windows-") else "codebase-memory-mcp"
classifications = {
variant: row[f"{FIELD_KEY[variant]}_classification"] for variant in VARIANTS
}
if metadata["policy"] == "virustotal-v2":
if any(
classification not in {"clean", "microsoft-ml"}
for classification in classifications.values()
):
raise ContractError(f"release selection contains a hard classification: {target}")
for variant in VARIANTS:
sha256 = row[f"{FIELD_KEY[variant]}_sha256"]
analysis_id = row[f"{FIELD_KEY[variant]}_analysis_id"]
expected_url = f"https://www.virustotal.com/gui/file/{sha256}/detection"
if ANALYSIS_ID_RE.fullmatch(analysis_id) is None:
raise ContractError(f"release selection lacks VT analysis evidence: {target}")
if analysis_id in analysis_ids:
raise ContractError("release selection reuses one VT analysis id")
analysis_ids.add(analysis_id)
if row[f"{FIELD_KEY[variant]}_virustotal_url"] != expected_url:
raise ContractError(f"release selection has unbound VT URL: {target}")
# Re-derived here independently of the selector: smallest artifact
# first, first CLEAN one wins, and only if every candidate drew the
# tolerated verdict do we ship the smallest flagged one.
order = ("stripped", "debug-stripped", "unstripped")
clean = [v for v in order if classifications[v] == "clean"]
if clean:
expected_variant = clean[0]
if expected_variant == "stripped":
expected_decision = "stripped-preferred"
else:
others = "-".join(
f"{v}:{classifications[v]}" for v in order if v != expected_variant
)
expected_decision = f"{expected_variant}-clean-after-{others}"
else:
expected_variant = "stripped"
expected_decision = "stripped-all-candidates-microsoft-ml"
else:
if any(
classification != "unscanned-dry-run"
for classification in classifications.values()
):
raise ContractError(f"dry-run selection disguises unscanned evidence: {target}")
if any(
row[f"{FIELD_KEY[variant]}_{suffix}"]
for variant in VARIANTS
for suffix in ("analysis_id", "virustotal_url")
):
raise ContractError(f"dry-run selection contains invented VT evidence: {target}")
expected_variant = "stripped"
expected_decision = "stripped-unscanned-dry-run"
if (
row["selected_variant"] != expected_variant
or row["decision"] != expected_decision
):
raise ContractError(f"release selection contradicts the policy truth table: {target}")
selected_variant = expected_variant
if row["selected_path"] != f"selected/{target}/{binary}":
raise ContractError(f"release selection has unsafe selected path: {target}")
if (
SHA256_RE.fullmatch(row["selected_sha256"]) is None
or row["selected_sha256"] != row[f"{FIELD_KEY[selected_variant]}_sha256"]
or not row["selected_size"].isdigit()
or not 0 < int(row["selected_size"]) <= MAX_MEMBER_BYTES
):
raise ContractError(f"release selection has invalid selected content: {target}")
for variant in VARIANTS:
sha256 = row[f"{FIELD_KEY[variant]}_sha256"]
scan_path = pathlib.PurePosixPath(row[f"{FIELD_KEY[variant]}_scan_path"])
if (
SHA256_RE.fullmatch(sha256) is None
or scan_path.as_posix() != f"objects/{sha256}"
):
raise ContractError(f"release selection has invalid sibling binding: {target}")
if row["unstripped_sha256"] == row["stripped_sha256"]:
raise ContractError(f"release selection siblings are not byte-distinct: {target}")
selections[target] = row
return selections
def safe_member_name(name: str) -> bool:
if not name or "\\" in name or "\x00" in name or name.startswith("/"):
return False
pure = pathlib.PurePosixPath(name)
return (
not pure.is_absolute()
and pure.as_posix() == name
and all(part not in ("", ".", "..") for part in pure.parts)
)
def hash_stream(stream: BinaryIO, *, declared_size: int, label: str) -> tuple[str, int]:
if declared_size < 0 or declared_size > MAX_MEMBER_BYTES:
raise ContractError(f"archive member has an invalid size: {label}")
digest = hashlib.sha256()
total = 0
while True:
chunk = stream.read(min(1024 * 1024, declared_size - total + 1))
if not chunk:
break
total += len(chunk)
if total > declared_size or total > MAX_MEMBER_BYTES:
raise ContractError(f"archive member exceeded its declared size: {label}")
digest.update(chunk)
if total != declared_size:
raise ContractError(f"archive member size does not match metadata: {label}")
return digest.hexdigest(), total
def verify_tar(
path: pathlib.Path, *, required_member: str, expected_sha256: str,
expected_size: int, require_executable_mode: bool
) -> None:
matches = 0
try:
with tarfile.open(path, mode="r:gz") as archive:
members = archive.getmembers()
if not members or len(members) > MAX_MEMBERS:
raise ContractError(f"tar member count is invalid: {path.name}")
if sum(member.size for member in members) > MAX_TOTAL_MEMBER_BYTES:
raise ContractError(f"tar exceeds total member-size ceiling: {path.name}")
seen: set[str] = set()
for member in members:
if not safe_member_name(member.name) or member.name in seen:
raise ContractError(f"tar has unsafe or duplicate member: {path.name}:{member.name}")
seen.add(member.name)
if member.issym() or member.islnk() or member.isdev() or member.isfifo():
raise ContractError(f"tar contains a special member: {path.name}:{member.name}")
if member.size > MAX_MEMBER_BYTES:
raise ContractError(f"tar member exceeds size ceiling: {path.name}:{member.name}")
if member.name != required_member:
continue
if not member.isfile():
raise ContractError(f"required tar executable is not regular: {path.name}")
if require_executable_mode and stat.S_IMODE(member.mode) != 0o755:
raise ContractError(
f"required tar executable has wrong mode: {path.name}:{member.name}"
)
matches += 1
handle = archive.extractfile(member)
if handle is None:
raise ContractError(f"required tar executable is unreadable: {path.name}")
with handle:
actual_sha256, actual_size = hash_stream(
handle, declared_size=member.size, label=f"{path.name}:{member.name}"
)
if actual_sha256 != expected_sha256 or actual_size != expected_size:
raise ContractError(f"tar executable does not match selection: {path.name}")
except (tarfile.TarError, EOFError) as error:
raise ContractError(f"malformed tar archive: {path.name}") from error
if matches != 1:
raise ContractError(f"tar must contain exactly one required executable: {path.name}")
def zip_entry_kind(info: zipfile.ZipInfo) -> str:
mode = (info.external_attr >> 16) & 0xFFFF
if mode and stat.S_IFMT(mode) not in (0, stat.S_IFREG, stat.S_IFDIR):
return "special"
return "directory" if info.is_dir() else "regular"
def verify_zip(
path: pathlib.Path, *, required_member: str, expected_sha256: str,
expected_size: int, require_executable_mode: bool
) -> None:
matches = 0
try:
with zipfile.ZipFile(path, mode="r") as archive:
members = archive.infolist()
if not members or len(members) > MAX_MEMBERS:
raise ContractError(f"zip member count is invalid: {path.name}")
if sum(member.file_size for member in members) > MAX_TOTAL_MEMBER_BYTES:
raise ContractError(f"zip exceeds total member-size ceiling: {path.name}")
seen: set[str] = set()
for member in members:
name = member.filename
if not safe_member_name(name) or name in seen:
raise ContractError(f"zip has unsafe or duplicate member: {path.name}:{name}")
seen.add(name)
if zip_entry_kind(member) == "special":
raise ContractError(f"zip contains a special member: {path.name}:{name}")
if member.file_size > MAX_MEMBER_BYTES:
raise ContractError(f"zip member exceeds size ceiling: {path.name}:{name}")
if name != required_member:
continue
if zip_entry_kind(member) != "regular":
raise ContractError(f"required zip executable is not regular: {path.name}")
mode = (member.external_attr >> 16) & 0xFFFF
if require_executable_mode and stat.S_IMODE(mode) != 0o755:
raise ContractError(
f"required zip executable has wrong mode: {path.name}:{name}"
)
matches += 1
with archive.open(member, mode="r") as handle:
actual_sha256, actual_size = hash_stream(
handle, declared_size=member.file_size, label=f"{path.name}:{name}"
)
if actual_sha256 != expected_sha256 or actual_size != expected_size:
raise ContractError(f"zip executable does not match selection: {path.name}")
except (zipfile.BadZipFile, EOFError, RuntimeError) as error:
raise ContractError(f"malformed zip archive: {path.name}") from error
if matches != 1:
raise ContractError(f"zip must contain exactly one required executable: {path.name}")
def discover_archives(root: pathlib.Path) -> dict[str, pathlib.Path]:
try:
root_status = root.lstat()
except FileNotFoundError as error:
raise ContractError(f"archive directory does not exist: {root}") from error
if not stat.S_ISDIR(root_status.st_mode):
raise ContractError(f"archive path is not a regular directory: {root}")
discovered: dict[str, pathlib.Path] = {}
archive_like: list[pathlib.Path] = []
for directory, names, files in os.walk(root, followlinks=False):
parent = pathlib.Path(directory)
for name in names:
child = parent / name
if child.is_symlink():
raise ContractError(f"symlinked directory in archive tree: {child}")
for name in files:
child = parent / name
if child.is_symlink():
raise ContractError(f"symlinked file in archive tree: {child}")
if name.endswith((".tar.gz", ".zip", ".mcpb")):
archive_like.append(child)
if name in discovered:
raise ContractError(f"duplicate archive basename: {name}")
discovered[name] = child
actual_names = set(discovered)
expected_names = set(ARCHIVES)
if actual_names != expected_names or len(archive_like) != len(expected_names):
missing = sorted(expected_names - actual_names)
surplus = sorted(actual_names - expected_names)
raise ContractError(
f"archive namespace is not the exact canonical set; missing={missing}, surplus={surplus}"
)
return discovered
def main(argv: Sequence[str]) -> None:
args = parse_arguments(argv)
selections = load_selection(args.selection, require_policy=args.require_policy)
archives = discover_archives(args.archive_dir)
for name, (target, member) in ARCHIVES.items():
path = archives[name]
regular_status(path, ceiling=MAX_ARCHIVE_BYTES, label="release archive")
selection = selections[target]
verify = verify_tar if name.endswith(".tar.gz") else verify_zip
verify(
path,
required_member=member,
expected_sha256=selection["selected_sha256"],
expected_size=int(selection["selected_size"]),
require_executable_mode=not target.startswith("windows-"),
)
print(f"verified all {len(ARCHIVES)} canonical containers against selected bytes")
try:
main(sys.argv)
except (ContractError, OSError, UnicodeError, ValueError) as error:
print(f"verify-release-selection: {error}", file=sys.stderr)
raise SystemExit(1)
+242 -154
View File
@@ -1,13 +1,10 @@
#!/usr/bin/env bash
# package-release.sh — THE canonical release-archive step. Every venue that
# turns built binaries into a release archive (release/_build.yml, the local
# artifact-flow smoke lane) runs this file; workflows provide only
# checkout/toolchain/upload around it. Archive names and contents are defined
# HERE, nowhere else, so a local artifact smoke provably exercises the same
# bytes-layout the release publishes.
# package-release.sh — canonical, byte-preserving release packager.
#
# This script ARCHIVES what scripts/build.sh already produced; it never builds
# or synthesizes another executable.
# Candidate derivation (including strip and macOS signing) happens before this
# boundary. This script accepts one already-final executable plus its expected
# SHA-256 and only copies that byte sequence into the release containers. A
# hash mismatch at any boundary is fatal.
set -euo pipefail
@@ -16,57 +13,70 @@ cd "$ROOT"
usage() {
cat <<'EOF'
Usage: scripts/package-release.sh <goos> <goarch> [--out-dir DIR] [VAR=VAL ...]
Usage: scripts/package-release.sh <goos> <goarch> \
--selected-binary FILE --expected-sha256 HEX [OPTIONS]
The canonical release-archive step: identical in the release build and the
local artifact-flow smoke lane.
Package an already-final release executable without changing its bytes.
goos linux | darwin | windows
goarch arch label used verbatim in the archive name (amd64, arm64,
arm64-portable, ...)
--out-dir where to place the archive (default: repository root).
Required:
--selected-binary FILE selected candidate to package
--expected-sha256 HEX SHA-256 of FILE (64 hexadecimal characters)
Make passthrough (VAR=VAL, forwarded to the build):
CC= CXX= compiler override, e.g. CC=clang CXX=clang++.
Options:
--third-party-notices FILE pre-generated notices file (otherwise generated)
--out-dir DIR output directory (default: repository root)
Environment:
BUILD_DIR build tree to archive from (default build/c).
VERSION release version stamped into the MCPB manifest (v-prefix
accepted; defaults to 0.0.0-dev outside a release build).
VERSION MCPB manifest version (v-prefix accepted;
default: 0.0.0-dev)
Archive contents (defined here, canonical) — ONE executable, no sidecars:
Targets are the eight existing release products:
linux/{amd64,arm64,amd64-portable,arm64-portable}
darwin/{amd64,arm64}
windows/{amd64,arm64}
Archive contents — one executable, no sidecars:
unix: codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md (.tar.gz)
windows: codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md (.zip)
MCPB bundle (.mcpb, a zip) — darwin/windows targets plus the STATIC linux
builds; same staged binary, for MCP-Registry one-click-install hosts:
manifest.json server/codebase-memory-mcp[.exe] server/LICENSE
server/THIRD_PARTY_NOTICES.md
Only one build variant ships: the binary carries the graph UI and the agent
integration templates inside itself, so an extracted archive is immediately
complete — no adjacent data file has to resolve for `install` to work.
MCPB bundle (.mcpb): every darwin/windows target and static linux target.
The executable member in every produced container is verified against
--expected-sha256 before anything is published.
EOF
}
GOOS=""
GOARCH=""
SELECTED_BINARY=""
EXPECTED_SHA256=""
THIRD_PARTY_NOTICES=""
OUT_DIR="$ROOT"
MAKE_ARGS=()
expect_value=""
for arg in "$@"; do
case "$expect_value" in
selected-binary) SELECTED_BINARY="$arg"; expect_value=""; continue ;;
expected-sha256) EXPECTED_SHA256="$arg"; expect_value=""; continue ;;
third-party-notices) THIRD_PARTY_NOTICES="$arg"; expect_value=""; continue ;;
out-dir) OUT_DIR="$arg"; expect_value=""; continue ;;
esac
case "$arg" in
-h | --help) usage; exit 0 ;;
--selected-binary) expect_value="selected-binary" ;;
--selected-binary=*) SELECTED_BINARY="${arg#--selected-binary=}" ;;
--expected-sha256) expect_value="expected-sha256" ;;
--expected-sha256=*) EXPECTED_SHA256="${arg#--expected-sha256=}" ;;
--third-party-notices) expect_value="third-party-notices" ;;
--third-party-notices=*) THIRD_PARTY_NOTICES="${arg#--third-party-notices=}" ;;
--out-dir) expect_value="out-dir" ;;
--out-dir=*) OUT_DIR="${arg#--out-dir=}" ;;
-*)
echo "package-release: unknown option '$arg'. Please consult --help." >&2
exit 2
;;
*=*) MAKE_ARGS+=("$arg") ;;
*=*)
echo "package-release: build variables are not accepted; package already-final bytes." >&2
exit 2
;;
*)
if [ -z "$GOOS" ]; then GOOS="$arg"
elif [ -z "$GOARCH" ]; then GOARCH="$arg"
@@ -77,140 +87,189 @@ for arg in "$@"; do
;;
esac
done
[ -n "$GOOS" ] && [ -n "$GOARCH" ] || { usage >&2; exit 2; }
case "$GOOS" in
linux | darwin | windows) ;;
*) echo "package-release: goos must be linux, darwin or windows." >&2; exit 2 ;;
esac
[ -n "$expect_value" ] && { echo "package-release: --$expect_value needs a value." >&2; exit 2; }
BUILD_DIR="${BUILD_DIR:-build/c}"
OUT_DIR="$(mkdir -p "$OUT_DIR" && cd "$OUT_DIR" && pwd)"
NAME="codebase-memory-mcp-${GOOS}-${GOARCH}"
# Ship every release binary stripped. Production already builds without -g, but
# the linker still keeps a ~536 KB .symtab, so releases carried their full
# symbol table to users: bigger downloads and a free map of the internals, with
# nothing gained. Nothing symbolizes at runtime (mem_profile.c is not in the
# production build and never calls backtrace_symbols), so this costs no
# diagnostics.
#
# A historical unstripped linux-amd64 artifact was the sole Microsoft detection
# in release run 30398064336, while related stripped artifacts later scanned
# clean. That is useful release evidence but not controlled feature attribution:
# engine state and other bytes can differ between observations. Independently
# of the opaque verdict, stripping removes an unnecessary symbol surface and
# does not change program behavior.
#
# macOS is ad-hoc signed by the build workflow BEFORE this script runs, and
# stripping invalidates that signature, so Mach-O is re-signed here. Skipping
# the re-sign ships a binary the kernel refuses to exec.
strip_release_binary() {
local binary="$1"
[ -f "$binary" ] || return 0
# The right flags differ per format, and the WRONG ones fail silently in
# the dangerous direction. Measured on the flagged darwin-arm64 artifact:
#
# llvm-strip --strip-all 373 symbols scanned CLEAN
# strip (no flags) 378 symbols equivalent
# strip -x -S 4058 symbols the state VirusTotal FLAGGED
# strip -X / -u -r 4058 symbols likewise
#
# Apple's strip returns success for `-x -S`, so a helper that just tries
# candidates until one exits 0 would quietly reship the flagged binary.
# GNU/LLVM `--strip-all` is not even accepted by Apple's strip, which is why
# generalising it to every platform broke the macOS build -- loudly, which
# was the lucky outcome.
#
# So: --strip-all where it is understood, plain `strip` for Mach-O, and a
# hard error when no candidate can do the job. Never a weaker fallback.
local stripped=""
for tool in "${STRIP:-}" llvm-strip strip; do
[ -n "$tool" ] || continue
command -v "$tool" >/dev/null 2>&1 || continue
if "$tool" --strip-all "$binary" 2>/dev/null; then
stripped="$tool --strip-all"
elif [ "$GOOS" = "darwin" ] && "$tool" "$binary" 2>/dev/null; then
stripped="$tool"
fi
[ -n "$stripped" ] && break
done
if [ -z "$stripped" ]; then
echo "package-release: no working strip for $binary" >&2
return 1
fi
if [ "$GOOS" = "darwin" ]; then
command -v codesign >/dev/null 2>&1 &&
codesign --sign - --force "$binary" 2>/dev/null
fi
echo "=== package-release: stripped $(basename "$binary") ==="
return 0
[ -z "$expect_value" ] || {
echo "package-release: --$expect_value needs a value." >&2
exit 2
}
if [ "$GOOS" = "windows" ]; then
# Windows ships one executable, exactly like every other runtime set. There is no
# launcher stub: a small unsigned PE whose entire job is to verify and
# execute another binary adds loader-like behavior and another artifact to
# audit. Historical launcher builds received Microsoft Wacatac verdicts,
# but that observation does not identify a stable feature or establish
# causation. Self-update — the launcher's whole reason to exist — moves OUT
# of the running process into install.ps1: Windows' executable lock only
# blocks a process from replacing ITSELF.
PAYLOAD="$BUILD_DIR/codebase-memory-mcp"
[ -f "${PAYLOAD}.exe" ] && PAYLOAD="${PAYLOAD}.exe"
[ -f "$PAYLOAD" ] || { echo "package-release: build first; missing $PAYLOAD" >&2; exit 2; }
STAGED_BINARY_NAME="codebase-memory-mcp.exe"
INSTALLER="install.ps1"
else
PAYLOAD="$BUILD_DIR/codebase-memory-mcp"
[ -f "$PAYLOAD" ] || { echo "package-release: build first; missing $PAYLOAD" >&2; exit 2; }
STAGED_BINARY_NAME="codebase-memory-mcp"
INSTALLER="install.sh"
[ -n "$GOOS" ] && [ -n "$GOARCH" ] || { usage >&2; exit 2; }
case "$GOOS/$GOARCH" in
linux/amd64 | linux/arm64 | linux/amd64-portable | linux/arm64-portable | \
darwin/amd64 | darwin/arm64 | windows/amd64 | windows/arm64) ;;
*)
echo "package-release: unsupported release target '$GOOS/$GOARCH'." >&2
exit 2
;;
esac
[ -n "$SELECTED_BINARY" ] || {
echo "package-release: --selected-binary is required." >&2
exit 2
}
[ -f "$SELECTED_BINARY" ] || {
echo "package-release: selected binary not found: $SELECTED_BINARY" >&2
exit 2
}
[ -n "$EXPECTED_SHA256" ] || {
echo "package-release: --expected-sha256 is required." >&2
exit 2
}
EXPECTED_SHA256="$(printf '%s' "$EXPECTED_SHA256" | tr '[:upper:]' '[:lower:]')"
if ! [[ "$EXPECTED_SHA256" =~ ^[0-9a-f]{64}$ ]]; then
echo "package-release: --expected-sha256 must be exactly 64 hexadecimal characters." >&2
exit 2
fi
# Work from one owner-private directory for every target. The binary is copied
# here BEFORE strip/re-sign and the archive is created from this same directory,
# so the executable that validates the adjacent sidecars is byte-for-byte the
# executable users receive. Keeping the stage beside the build also avoids a
# system /tmp mounted noexec: the validation probe must actually execute.
PACK_DIR="$(mktemp -d "$BUILD_DIR/.cbm-package.XXXXXX")"
trap 'rm -rf "$PACK_DIR"' EXIT
STAGED_BINARY="$PACK_DIR/$STAGED_BINARY_NAME"
cp "$PAYLOAD" "$STAGED_BINARY"
strip_release_binary "$STAGED_BINARY" || exit 2
# Resolve all caller-owned inputs before creating the private packaging area.
SELECTED_BINARY="$(cd "$(dirname "$SELECTED_BINARY")" && pwd -P)/$(basename "$SELECTED_BINARY")"
if [ -n "$THIRD_PARTY_NOTICES" ]; then
[ -f "$THIRD_PARTY_NOTICES" ] || {
echo "package-release: notices file not found: $THIRD_PARTY_NOTICES" >&2
exit 2
}
THIRD_PARTY_NOTICES="$(cd "$(dirname "$THIRD_PARTY_NOTICES")" && pwd -P)/$(basename "$THIRD_PARTY_NOTICES")"
fi
OUT_DIR="$(mkdir -p "$OUT_DIR" && cd "$OUT_DIR" && pwd -P)"
NAME="codebase-memory-mcp-${GOOS}-${GOARCH}"
# Gate the artifact AFTER strip/re-sign: this is the final executable image and
# no later step may mutate it.
sha256_file() {
python3 - "$1" <<'PY'
import hashlib
import pathlib
import sys
digest = hashlib.sha256()
with pathlib.Path(sys.argv[1]).open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
print(digest.hexdigest())
PY
}
assert_expected_hash() {
local path="$1"
local label="$2"
local actual
actual="$(sha256_file "$path")"
if [ "$actual" != "$EXPECTED_SHA256" ]; then
echo "package-release: $label SHA-256 mismatch: expected $EXPECTED_SHA256, got $actual" >&2
return 1
fi
}
extract_zip_member() {
local archive="$1"
local member="$2"
local destination="$3"
python3 - "$archive" "$member" "$destination" <<'PY'
import pathlib
import shutil
import stat
import sys
import zipfile
archive_path = pathlib.Path(sys.argv[1])
member_name = sys.argv[2]
destination = pathlib.Path(sys.argv[3])
with zipfile.ZipFile(archive_path, "r") as archive:
matches = [info for info in archive.infolist() if info.filename == member_name]
if len(matches) != 1:
raise SystemExit(f"package-release: ZIP must contain exactly one {member_name}")
info = matches[0]
mode = (info.external_attr >> 16) & 0xFFFF
if info.is_dir() or (mode and stat.S_IFMT(mode) not in (0, stat.S_IFREG)):
raise SystemExit(f"package-release: ZIP member is not regular: {member_name}")
destination.parent.mkdir(parents=True, exist_ok=True)
with archive.open(info, "r") as source, destination.open("xb") as output:
shutil.copyfileobj(source, output, length=1024 * 1024)
PY
}
# Hash the caller-owned file before taking the per-target output lock. A bad
# selection must not create any artifact or packaging residue.
assert_expected_hash "$SELECTED_BINARY" "selected binary" || exit 2
if [ "$GOOS" = "windows" ]; then
STAGED_BINARY_NAME="codebase-memory-mcp.exe"
INSTALLER="install.ps1"
ARCHIVE_OUT="$OUT_DIR/$NAME.zip"
else
STAGED_BINARY_NAME="codebase-memory-mcp"
INSTALLER="install.sh"
ARCHIVE_OUT="$OUT_DIR/$NAME.tar.gz"
fi
MCPB_OUT="$OUT_DIR/$NAME.mcpb"
BUILD_MCPB=0
case "$GOOS/$GOARCH" in
darwin/* | windows/* | linux/*-portable) BUILD_MCPB=1 ;;
esac
LOCK_DIR="$OUT_DIR/.${NAME}.package.lock"
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
echo "package-release: target is already being packaged (lock exists): $LOCK_DIR" >&2
exit 2
fi
cleanup() {
local status=$?
trap - EXIT
rm -rf "$LOCK_DIR"
exit "$status"
}
trap cleanup EXIT
if [ -e "$ARCHIVE_OUT" ] || [ -e "$MCPB_OUT" ]; then
echo "package-release: refusing to overwrite an existing $NAME release container." >&2
exit 2
fi
PACK_DIR="$LOCK_DIR/stage"
OUTPUT_DIR="$LOCK_DIR/output"
VERIFY_DIR="$LOCK_DIR/verify"
mkdir -p "$PACK_DIR" "$OUTPUT_DIR" "$VERIFY_DIR"
STAGED_BINARY="$PACK_DIR/$STAGED_BINARY_NAME"
cp "$SELECTED_BINARY" "$STAGED_BINARY"
# upload-artifact transports do not preserve Unix mode. Restoring the container
# metadata is allowed; chmod does not alter the selected file's byte sequence.
chmod 0755 "$STAGED_BINARY"
assert_expected_hash "$STAGED_BINARY" "staged binary" || exit 2
# The composition gate is read-only and runs against the exact executable that
# will enter both containers. Candidate derivation and scanning precede this
# packaging boundary.
bash scripts/ci/check-binary-composition.sh "$STAGED_BINARY" || exit 2
assert_expected_hash "$STAGED_BINARY" "composition-gated binary" || exit 2
assert_expected_hash "$SELECTED_BINARY" "selected binary after staging" || exit 2
cp LICENSE "$INSTALLER" "$PACK_DIR/"
scripts/gen-third-party-notices.sh "$PACK_DIR/THIRD_PARTY_NOTICES.md"
if [ -n "$THIRD_PARTY_NOTICES" ]; then
cp "$THIRD_PARTY_NOTICES" "$PACK_DIR/THIRD_PARTY_NOTICES.md"
else
scripts/gen-third-party-notices.sh "$PACK_DIR/THIRD_PARTY_NOTICES.md"
fi
ARCHIVE_TMP="$OUTPUT_DIR/$(basename "$ARCHIVE_OUT")"
if [ "$GOOS" = "windows" ]; then
(
cd "$PACK_DIR"
rm -f "$OUT_DIR/$NAME.zip"
zip -q "$OUT_DIR/$NAME.zip" \
zip -q -X "$ARCHIVE_TMP" \
codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md
)
echo "=== package-release: $OUT_DIR/$NAME.zip ==="
mkdir "$VERIFY_DIR/archive"
extract_zip_member "$ARCHIVE_TMP" "$STAGED_BINARY_NAME" \
"$VERIFY_DIR/archive/$STAGED_BINARY_NAME"
else
# BSD tar otherwise materializes macOS extended attributes as hidden
# AppleDouble `._*` members, violating the exact four-file inventory.
COPYFILE_DISABLE=1 tar -czf "$OUT_DIR/$NAME.tar.gz" -C "$PACK_DIR" \
COPYFILE_DISABLE=1 tar -czf "$ARCHIVE_TMP" -C "$PACK_DIR" \
codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md
echo "=== package-release: $OUT_DIR/$NAME.tar.gz ==="
mkdir "$VERIFY_DIR/archive"
tar -xzf "$ARCHIVE_TMP" -C "$VERIFY_DIR/archive" "$STAGED_BINARY_NAME"
fi
assert_expected_hash "$VERIFY_DIR/archive/$STAGED_BINARY_NAME" "archive executable" || exit 2
# ── MCPB bundle (registryType "mcpb" in the MCP Registry) ─────────────────
# Repackages the SAME staged binary the archive above ships — already
# stripped, re-signed and composition-gated — so the VirusTotal scan set
# dedupes the executable to the archive's object; only manifest.json is a
# new scan member. The installer script is deliberately absent: an MCPB
# host manages install/update itself.
build_mcpb_bundle() {
local out="$OUT_DIR/$NAME.mcpb"
local stage="$PACK_DIR/.mcpb-stage"
local out
out="$OUTPUT_DIR/$(basename "$MCPB_OUT")"
local stage="$LOCK_DIR/mcpb-stage"
local entry="server/$STAGED_BINARY_NAME"
local platform
case "$GOOS" in
@@ -224,12 +283,10 @@ build_mcpb_bundle() {
}
mkdir -p "$stage/server"
cp "$STAGED_BINARY" "$stage/$entry"
chmod 0755 "$stage/$entry"
cp "$PACK_DIR/LICENSE" "$PACK_DIR/THIRD_PARTY_NOTICES.md" "$stage/server/"
local mcpb_version="${VERSION:-0.0.0-dev}"
mcpb_version="${mcpb_version#v}"
# ${__dirname} is the MCPB host's substitution variable, not shell —
# hence the escapes. Hosts append .exe themselves where needed, but the
# manifest names the actual member so non-normalizing hosts also work.
cat >"$stage/manifest.json" <<EOF
{
"manifest_version": "0.3",
@@ -254,17 +311,48 @@ build_mcpb_bundle() {
}
}
EOF
rm -f "$out"
(
cd "$stage"
zip -q -X "$out" \
manifest.json "$entry" server/LICENSE server/THIRD_PARTY_NOTICES.md
)
echo "=== package-release: $out ==="
mkdir "$VERIFY_DIR/mcpb"
extract_zip_member "$out" "$entry" "$VERIFY_DIR/mcpb/$entry"
assert_expected_hash "$VERIFY_DIR/mcpb/$entry" "MCPB executable" || return 1
}
# MCPB eligibility: every darwin/windows target, but only the STATIC linux
# builds — a glibc-dynamic binary defeats the one-click-install promise.
case "$GOOS/$GOARCH" in
darwin/* | windows/* | linux/*-portable) build_mcpb_bundle || exit 2 ;;
esac
if [ "$BUILD_MCPB" -eq 1 ]; then
build_mcpb_bundle || exit 2
fi
# Recheck both byte owners after every packaging operation. No archive tool,
# manifest step, or future gate may mutate either one unnoticed.
assert_expected_hash "$STAGED_BINARY" "final staged binary" || exit 2
assert_expected_hash "$SELECTED_BINARY" "final selected binary" || exit 2
# Publish only after every container has been built and verified. Because the
# temporary outputs live below OUT_DIR, a hard link provides an atomic
# create-if-absent operation on the same filesystem; unlike mv, it cannot
# replace a path that appeared after the preflight check.
publish_no_clobber() {
local source="$1"
local destination="$2"
if ! ln "$source" "$destination"; then
echo "package-release: refusing to overwrite release container: $destination" >&2
return 1
fi
rm -f "$source"
}
publish_no_clobber "$ARCHIVE_TMP" "$ARCHIVE_OUT" || exit 2
if [ "$BUILD_MCPB" -eq 1 ]; then
if ! publish_no_clobber "$OUTPUT_DIR/$(basename "$MCPB_OUT")" "$MCPB_OUT"; then
rm -f "$ARCHIVE_OUT"
exit 2
fi
fi
echo "=== package-release: $ARCHIVE_OUT (selected sha256 $EXPECTED_SHA256) ==="
if [ "$BUILD_MCPB" -eq 1 ]; then
echo "=== package-release: $MCPB_OUT (same selected sha256) ==="
fi
+10 -1
View File
@@ -14,7 +14,7 @@ Usage: scripts/test.sh [--suites LIST] [--arch ARCH] [VAR=VAL ...]
The canonical test entry: identical in local CI, PR CI, dry run and release.
DEFAULT (no --suites) is exactly what CI runs: static contract checks
(Step 0a-0j), a CLEAN sanitizer build, every suite via the parallel harness,
(Step 0a-0s), a CLEAN sanitizer build, every suite via the parallel harness,
then the prod-binary regression guards (Steps 4-6).
Modes:
@@ -248,6 +248,15 @@ bash "$ROOT/tests/test_mcpb_bundle_contract.sh"
echo "=== Step 0p: MCPB registry entries contract (#1246) ==="
bash "$ROOT/tests/test_mcpb_registry_entries_contract.sh"
echo "=== Step 0q: release candidate derivation contract ==="
bash "$ROOT/tests/test_release_candidate_derivation_contract.sh"
echo "=== Step 0r: VirusTotal candidate-selection contract ==="
bash "$ROOT/tests/test_vt_candidate_selection_contract.sh"
echo "=== Step 0s: release gate-chain ordering contract ==="
bash "$ROOT/tests/test_release_gate_chain_contract.sh"
# Verify compiler supports target arch
verify_compiler "$CC"
+128 -14
View File
@@ -11,11 +11,9 @@
# The stub is a REAL compiled executable, so the composition gate runs
# genuinely: format detection, the ELF segment checks (-z separate-code) and
# the needle scans. The gate's two positive needles — the artifact canary and
# the SQLite OMIT_LOAD_EXTENSION marker — live in .rodata. STRIP=true no-ops
# the strip stage only because this contract packages FOREIGN goos targets
# from one host, and a host strip tool cannot legitimately serve them all;
# the real strip runs with the real binary in the artifact-flow smoke lane
# (scripts/ci/smoke-artifact.sh) on every leg.
# the SQLite OMIT_LOAD_EXTENSION marker — live in .rodata. This test packages
# the same opaque selected bytes under foreign target labels deliberately; this
# boundary proves it never invokes strip/codesign and never changes bytes.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
@@ -29,17 +27,50 @@ cat >"$FIX/stub.c" <<'EOF'
static const char keep[] = "codebase-memory-mcp OMIT_LOAD_EXTENSION";
int main(void) { puts(keep); return 0; }
EOF
"${CC:-cc}" -O0 -o "$BUILD_DIR/codebase-memory-mcp" "$FIX/stub.c"
SELECTED="$BUILD_DIR/codebase-memory-mcp"
case "$(uname -s)" in
MINGW* | MSYS* | CYGWIN*) SELECTED="${SELECTED}.exe" ;;
esac
"${CC:-cc}" -O0 -o "$SELECTED" "$FIX/stub.c"
[ -f "$SELECTED" ] || {
echo "MCPB contract compiler produced no selected-binary fixture" >&2
exit 1
}
SELECTED_SHA="$(python3 - "$SELECTED" <<'PY'
import hashlib
import pathlib
import sys
print(hashlib.sha256(pathlib.Path(sys.argv[1]).read_bytes()).hexdigest())
PY
)"
printf '%s\n' 'contract notices sentinel' >"$FIX/THIRD_PARTY_NOTICES.md"
# A pure packager has no reason to resolve any mutation tool, and its byte
# verification uses the already-required Python runtime rather than adding an
# undeclared unzip dependency. Loud sentinels catch either regression.
mkdir -p "$FIX/forbidden-tools"
for tool in strip llvm-strip codesign unzip; do
# These variables expand inside the generated sentinel, not in this shell.
# shellcheck disable=SC2016
printf '%s\n' '#!/usr/bin/env bash' \
'printf "%s\\n" "$(basename "$0")" >>"$FORBIDDEN_TOOL_LOG"' \
'exit 97' >"$FIX/forbidden-tools/$tool"
chmod +x "$FIX/forbidden-tools/$tool"
done
export FORBIDDEN_TOOL_LOG="$FIX/forbidden-tools.log"
export PATH="$FIX/forbidden-tools:$PATH"
package() { # goos goarch out-subdir [VERSION value]
local goos="$1" goarch="$2" out="$FIX/$3"
mkdir -p "$out"
if [ "$#" -ge 4 ]; then
VERSION="$4" STRIP=true BUILD_DIR="$BUILD_DIR" \
bash "$ROOT/scripts/package-release.sh" "$goos" "$goarch" --out-dir "$out"
VERSION="$4" bash "$ROOT/scripts/package-release.sh" "$goos" "$goarch" \
--selected-binary "$SELECTED" --expected-sha256 "$SELECTED_SHA" \
--third-party-notices "$FIX/THIRD_PARTY_NOTICES.md" --out-dir "$out"
else
env -u VERSION STRIP=true BUILD_DIR="$BUILD_DIR" \
bash "$ROOT/scripts/package-release.sh" "$goos" "$goarch" --out-dir "$out"
env -u VERSION bash "$ROOT/scripts/package-release.sh" "$goos" "$goarch" \
--selected-binary "$SELECTED" --expected-sha256 "$SELECTED_SHA" \
--third-party-notices "$FIX/THIRD_PARTY_NOTICES.md" --out-dir "$out"
fi
}
@@ -50,14 +81,61 @@ package linux amd64-portable portable v0.0.0-contract >/dev/null
package linux amd64 plainlinux v0.0.0-contract >/dev/null
package darwin arm64 unversioned >/dev/null
python3 - "$FIX" <<'PY'
if [ -e "$FORBIDDEN_TOOL_LOG" ]; then
echo "package-release invoked a forbidden mutation tool:" >&2
sed 's/^/ /' "$FORBIDDEN_TOOL_LOG" >&2
exit 1
fi
echo "--- hash mismatch fails before publishing any container"
mkdir -p "$FIX/hash-mismatch"
if bash "$ROOT/scripts/package-release.sh" linux arm64 \
--selected-binary "$SELECTED" \
--expected-sha256 0000000000000000000000000000000000000000000000000000000000000000 \
--third-party-notices "$FIX/THIRD_PARTY_NOTICES.md" \
--out-dir "$FIX/hash-mismatch" >"$FIX/hash-mismatch.out" 2>&1; then
echo "package-release accepted an incorrect selected-binary hash" >&2
exit 1
fi
if find "$FIX/hash-mismatch" -type f -name 'codebase-memory-mcp-*' | grep -q .; then
echo "hash mismatch left a release container behind" >&2
exit 1
fi
echo "--- existing release containers are never overwritten"
PLAIN_ARCHIVE="$FIX/plainlinux/codebase-memory-mcp-linux-amd64.tar.gz"
BEFORE_ARCHIVE_SHA="$(python3 - "$PLAIN_ARCHIVE" <<'PY'
import hashlib
import pathlib
import sys
print(hashlib.sha256(pathlib.Path(sys.argv[1]).read_bytes()).hexdigest())
PY
)"
if package linux amd64 plainlinux v0.0.0-contract >"$FIX/no-clobber.out" 2>&1; then
echo "package-release overwrote an existing release container" >&2
exit 1
fi
AFTER_ARCHIVE_SHA="$(python3 - "$PLAIN_ARCHIVE" <<'PY'
import hashlib
import pathlib
import sys
print(hashlib.sha256(pathlib.Path(sys.argv[1]).read_bytes()).hexdigest())
PY
)"
test "$BEFORE_ARCHIVE_SHA" = "$AFTER_ARCHIVE_SHA"
python3 - "$FIX" "$SELECTED" <<'PY'
import hashlib
import json
import pathlib
import stat
import sys
import tarfile
import zipfile
fix = pathlib.Path(sys.argv[1])
selected = pathlib.Path(sys.argv[2]).read_bytes()
selected_sha = hashlib.sha256(selected).hexdigest()
failures = []
@@ -80,7 +158,13 @@ def check_bundle(path, *, binary, platform, version):
mode = (info.external_attr >> 16) & 0xFFFF
if not mode & stat.S_IXUSR:
fail(f"{path.name}: {binary} lost its executable bit (mode {oct(mode)})")
bundled = bundle.read(binary)
manifest = json.loads(bundle.read("manifest.json"))
notices = bundle.read("server/THIRD_PARTY_NOTICES.md")
if bundled != selected:
fail(f"{path.name}: executable bytes differ from selected SHA-256 {selected_sha}")
if notices != b"contract notices sentinel\n":
fail(f"{path.name}: did not preserve the supplied notices file")
if manifest.get("version") != version:
fail(f"{path.name}: manifest version {manifest.get('version')!r} != {version!r}")
server = manifest.get("server") or {}
@@ -120,13 +204,43 @@ mcpbs = list(plain.glob("*.mcpb"))
if mcpbs:
fail(f"glibc-dynamic linux target must not produce a bundle: {[p.name for p in mcpbs]}")
def check_archive(path, binary):
if path.suffix == ".zip":
with zipfile.ZipFile(path) as archive:
packaged = archive.read(binary)
notices = archive.read("THIRD_PARTY_NOTICES.md")
else:
with tarfile.open(path, "r:gz") as archive:
member = archive.extractfile(binary)
notice_member = archive.extractfile("THIRD_PARTY_NOTICES.md")
packaged = member.read() if member else b""
notices = notice_member.read() if notice_member else b""
if packaged != selected:
fail(f"{path.name}: archive executable differs from selected SHA-256 {selected_sha}")
if notices != b"contract notices sentinel\n":
fail(f"{path.name}: archive did not preserve the supplied notices file")
check_archive(fix / "darwin" / "codebase-memory-mcp-darwin-arm64.tar.gz",
"codebase-memory-mcp")
check_archive(fix / "windows" / "codebase-memory-mcp-windows-amd64.zip",
"codebase-memory-mcp.exe")
check_archive(fix / "portable" / "codebase-memory-mcp-linux-amd64-portable.tar.gz",
"codebase-memory-mcp")
check_archive(fix / "plainlinux" / "codebase-memory-mcp-linux-amd64.tar.gz",
"codebase-memory-mcp")
if pathlib.Path(sys.argv[2]).read_bytes() != selected:
fail("package-release mutated the caller-owned selected binary")
if failures:
print("MCPB BUNDLE CONTRACT VIOLATED:")
for message in failures:
print(f" - {message}")
sys.exit(1)
print("mcpb bundle contract OK (darwin/windows/static-linux bundles exact, "
"manifest binds the bundled binary, exec bit preserved, version "
"stamped with 0.0.0-dev fallback, glibc-dynamic linux excluded)")
print("mcpb bundle contract OK (selected bytes hash-bound and immutable, "
"archive/MCPB members byte-identical, mutation tools unused, no-clobber, "
"darwin/windows/static-linux eligibility exact)")
PY
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env bash
# Contract: release candidates are two immutable derivations of one linker
# output. This uses the host's real compiler/strip/signing tools.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
FIX="$(mktemp -d "${TMPDIR:-/tmp}/cbm-candidate-derive.XXXXXX")"
trap 'rm -rf "$FIX"' EXIT
PREPARE="$ROOT/scripts/ci/prepare-release-candidates.sh"
[[ -x "$PREPARE" ]] || {
echo "FAIL: missing executable release-candidate producer: $PREPARE" >&2
exit 1
}
command -v python3 >/dev/null 2>&1 || { echo "FAIL: python3 is required" >&2; exit 1; }
command -v "${CC:-cc}" >/dev/null 2>&1 || {
echo "SKIP: no native C compiler is available for derivation contract"
exit 0
}
case "$(uname -s)" in
Darwin) GOOS=darwin ;;
Linux) GOOS=linux ;;
MINGW* | MSYS* | CYGWIN*) GOOS=windows ;;
*) echo "SKIP: unsupported native kernel $(uname -s)"; exit 0 ;;
esac
case "$(uname -m)" in
x86_64 | amd64 | AMD64) GOARCH=amd64 ;;
arm64 | aarch64 | ARM64) GOARCH=arm64 ;;
*) echo "SKIP: unsupported native architecture $(uname -m)"; exit 0 ;;
esac
TARGET="$GOOS-$GOARCH"
BINARY_NAME=codebase-memory-mcp
[[ "$GOOS" == windows ]] && BINARY_NAME+=.exe
INPUT="$FIX/$BINARY_NAME"
python3 - "$FIX/stub.c" <<'PY'
import pathlib
import sys
pathlib.Path(sys.argv[1]).write_text(
r'''#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const char release_canary[] =
"codebase-memory-mcp OMIT_LOAD_EXTENSION candidate-derivation-contract";
static _Thread_local unsigned long release_tls_counter;
static int visible_symbol_for_real_strip(int value) { return value + 17; }
int main(int argc, char **argv) {
const char *sentinel = getenv("CBM_CANDIDATE_EXEC_SENTINEL");
if (sentinel) {
FILE *handle = fopen(sentinel, "wb");
if (handle) fclose(handle);
}
release_tls_counter += (unsigned long)argc;
if (argc == 2 && strcmp(argv[1], "--version") == 0) {
puts("codebase-memory-mcp 0.0.0-candidate-contract");
return 0;
}
puts(release_canary);
return visible_symbol_for_real_strip(argc) == -1 || release_tls_counter == 0;
}
''',
encoding="utf-8",
)
PY
"${CC:-cc}" -g -O0 -o "$INPUT" "$FIX/stub.c"
INPUT_SHA="$(python3 - "$INPUT" <<'PY'
import hashlib, pathlib, sys
print(hashlib.sha256(pathlib.Path(sys.argv[1]).read_bytes()).hexdigest())
PY
)"
OUT="$FIX/published"
env CBM_CANDIDATE_EXEC_SENTINEL="$FIX/executed-before-selection" \
STRIP="${STRIP:-$(command -v strip)}" CODESIGN="${CODESIGN:-$(command -v codesign 2>/dev/null || true)}" \
"$PREPARE" "$GOOS" "$GOARCH" --binary "$INPUT" --out-dir "$OUT"
[[ ! -e "$FIX/executed-before-selection" ]] || {
echo "FAIL: candidate producer executed a binary before VirusTotal selection" >&2
exit 1
}
UNSTRIPPED="$OUT/$TARGET/unstripped/$BINARY_NAME"
STRIPPED="$OUT/$TARGET/stripped/$BINARY_NAME"
PROVENANCE="$OUT/$TARGET/candidate-provenance.tsv"
for path in "$UNSTRIPPED" "$STRIPPED" "$PROVENANCE"; do
[[ -f "$path" && ! -L "$path" ]] || { echo "FAIL: missing regular output $path" >&2; exit 1; }
done
# The source is never the place stripping/signing occurs.
[[ "$(python3 - "$INPUT" <<'PY'
import hashlib, pathlib, sys
print(hashlib.sha256(pathlib.Path(sys.argv[1]).read_bytes()).hexdigest())
PY
)" == "$INPUT_SHA" ]] || { echo "FAIL: linker output was mutated in place" >&2; exit 1; }
python3 - "$PROVENANCE" "$OUT/$TARGET" "$INPUT_SHA" "$TARGET" "$BINARY_NAME" <<'PY'
from __future__ import annotations
import csv
import hashlib
import pathlib
import stat
import sys
manifest = pathlib.Path(sys.argv[1])
target_root = pathlib.Path(sys.argv[2])
source_hash = sys.argv[3]
target = sys.argv[4]
binary_name = sys.argv[5]
lines = manifest.read_text(encoding="utf-8").splitlines()
if not lines or lines[0] != "# cbm-release-candidate-provenance-v1":
raise SystemExit("FAIL: candidate provenance marker is missing")
rows = list(csv.DictReader(lines[1:], delimiter="\t"))
expected_fields = (
"target", "variant", "relative_path", "source_sha256", "pre_sign_sha256",
"sha256", "size", "format", "architecture", "linkage", "transform",
"signature", "strip_tool", "strip_version", "pair_verification",
)
actual_fields = tuple(rows[0].keys()) if rows else ()
if actual_fields != expected_fields:
raise SystemExit("FAIL: candidate provenance header changed")
if [row["variant"] for row in rows] != ["unstripped", "debug-stripped", "stripped"]:
raise SystemExit("FAIL: provenance rows are not in canonical variant order")
if any(row["target"] != target for row in rows):
raise SystemExit("FAIL: provenance target is not exact")
if any(row["source_sha256"] != source_hash for row in rows):
raise SystemExit("FAIL: every candidate must bind the unchanged linker-output hash")
digests = [row["sha256"] for row in rows]
if len(set(digests)) != len(digests):
# Identical candidates would be one VirusTotal draw wearing three hats, and
# the selector would believe it has alternatives that do not exist.
raise SystemExit("FAIL: strip did not produce three byte-distinct candidates")
if [row["transform"] for row in rows] != ["copy", "strip-debug", "strip"]:
raise SystemExit("FAIL: transforms are not explicit copy/strip-debug/strip")
if any(row["pair_verification"] != "same-linker-output-v1" for row in rows):
raise SystemExit("FAIL: common linker-output provenance is not recorded")
if target.startswith("darwin-") and any(row["signature"] != "adhoc-verified" for row in rows):
raise SystemExit("FAIL: both final Darwin candidates must be ad-hoc-signature verified")
for row in rows:
expected_rel = f'{row["variant"]}/{binary_name}'
if row["relative_path"] != expected_rel:
raise SystemExit(f"FAIL: unsafe/noncanonical candidate path {row['relative_path']!r}")
path = target_root / pathlib.PurePosixPath(row["relative_path"])
data = path.read_bytes()
if hashlib.sha256(data).hexdigest() != row["sha256"] or len(data) != int(row["size"]):
raise SystemExit(f"FAIL: provenance is not content-bound: {path}")
if stat.S_IMODE(path.stat().st_mode) & 0o222:
raise SystemExit(f"FAIL: published candidate is writable: {path}")
if not stat.S_IMODE(path.stat().st_mode) & 0o111:
raise SystemExit(f"FAIL: published candidate lost executable mode: {path}")
PY
# An explicit unusable strip tool is a hard failure, and atomic publication
# means it leaves no target directory or half-manifest behind.
FAILED_OUT="$FIX/failed-publication"
if STRIP="$FIX/does-not-exist" "$PREPARE" "$GOOS" "$GOARCH" \
--binary "$INPUT" --out-dir "$FAILED_OUT" >"$FIX/failure.out" 2>&1; then
echo "FAIL: producer silently fell back after an explicit STRIP failed" >&2
exit 1
fi
[[ ! -e "$FAILED_OUT/$TARGET" ]] || {
echo "FAIL: failed derivation left a partial published target" >&2
exit 1
}
echo "PASS: native stripped/unstripped candidates derive from one linker output and publish atomically"
+213 -10
View File
@@ -14,20 +14,29 @@
# 1. every job downstream of an optional phase carries the
# `!cancelled() && !failure()` override, so it runs when an ancestor was
# deliberately skipped;
# 2. release-draft requires `needs.smoke.result == 'success'` explicitly, so a
# skipped smoke BLOCKS the draft instead of sailing past it.
# 2. release-draft requires both smoke and soak to succeed explicitly, so a
# skipped runtime gate BLOCKS the draft instead of sailing past it.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WF="$ROOT/.github/workflows/release.yml"
BUILD_WF="$ROOT/.github/workflows/_build.yml"
DRY_WF="$ROOT/.github/workflows/dry-run.yml"
SOAK_WF="$ROOT/.github/workflows/_soak.yml"
[ -f "$WF" ] || { echo "FAIL: $WF not found" >&2; exit 2; }
[ -f "$BUILD_WF" ] || { echo "FAIL: $BUILD_WF not found" >&2; exit 2; }
[ -f "$DRY_WF" ] || { echo "FAIL: $DRY_WF not found" >&2; exit 2; }
[ -f "$SOAK_WF" ] || { echo "FAIL: $SOAK_WF not found" >&2; exit 2; }
python3 - "$WF" <<'PY'
python3 - "$WF" "$BUILD_WF" "$DRY_WF" "$SOAK_WF" <<'PY'
import pathlib
import re
import sys
text = pathlib.Path(sys.argv[1]).read_text()
build_text = pathlib.Path(sys.argv[2]).read_text()
dry_text = pathlib.Path(sys.argv[3]).read_text()
soak_text = pathlib.Path(sys.argv[4]).read_text()
# Slice the file into top-level job blocks: two-space indented "name:".
blocks, current, name = {}, [], None
@@ -68,7 +77,7 @@ for job in TOLERATE:
f"{job}: `if:` lacks `!cancelled() && !failure()` (got: {c or '<none>'}).\n"
f" With skip_tests=true a skipped ancestor SKIPS this job silently.")
# 2. The draft must require smoke to have genuinely succeeded.
# 2. The draft must require both runtime gates to have genuinely succeeded.
draft = cond("release-draft")
if "needs.smoke.result == 'success'" not in draft:
failures.append(
@@ -76,13 +85,19 @@ if "needs.smoke.result == 'success'" not in draft:
" `!cancelled() && !failure()` alone is fail-open: a SKIPPED smoke\n"
" passes it, and the draft gets cut from unsmoked binaries.")
# soak is allowed to be skipped (soak_level=none) but must be enumerated, not
# ignored — otherwise 'none' and 'silently never ran' are indistinguishable.
if "needs.soak.result" not in draft:
if "needs.soak.result == 'success'" not in draft:
failures.append(
"release-draft: `if:` must mention needs.soak.result explicitly so a\n"
" legitimately skipped soak (soak_level=none) is distinguishable\n"
" from a soak that never ran.")
"release-draft: `if:` must require needs.soak.result == 'success'.\n"
" Every production release must soak the exact selected bytes; a\n"
" skipped soak must block the draft.")
soak_input = re.search(
r"^ soak_level:\s*\n(?P<body>(?:^ .*\n?)*)", text, re.M)
if not soak_input or "none" in soak_input.group("body"):
failures.append(
"release: production soak_level must not expose a 'none' choice.\n"
" The explicit bypass belongs only to dry-run; selected release\n"
" bytes must always complete soak before drafting.")
# The MCP registry validates every package URL it is handed by FETCHING it, and
# a DRAFT release's assets are not publicly readable. publish-final is the job
@@ -109,6 +124,194 @@ if "publish-final" in blocks and "publish-mcp-registry" in needs("publish-final"
" release is the product and the registry entry is metadata; a\n"
" registry-preview outage must never hold up a release.")
# 3. Candidate selection is part of the reusable build boundary. Native jobs
# may upload only candidate pairs; the reusable workflow must not complete
# until one candidate in each of the eight immutable target tuples has been
# scanned, selected, and packaged under the historical binaries-* names.
def workflow_jobs(source):
result, current, current_name = {}, [], None
for line in source.splitlines():
match = re.match(r"^ ([A-Za-z0-9_-]+):\s*$", line)
if match:
if current_name:
result[current_name] = "\n".join(current)
current_name, current = match.group(1), []
continue
if current_name is not None:
current.append(line)
if current_name:
result[current_name] = "\n".join(current)
return result
build_jobs = workflow_jobs(build_text)
select = build_jobs.get("select-package", "")
native_jobs = ["build-unix", "build-windows", "build-windows-arm64",
"build-linux-portable"]
if not re.search(r"(?m)^ scan_candidates:\s*\n(?: [^\n]*\n)*? default:\s*true\s*$",
build_text):
failures.append(
"_build.yml: workflow_call must expose scan_candidates with default true;\n"
" an omitted input must fail toward scanning, never toward bypass.")
if not select:
failures.append(
"_build.yml: select-package job is missing. Candidate scanning, tuple-local\n"
" selection, and packaging must finish inside the reusable build.")
else:
needs_match = re.search(r"^ needs:\s*\[([^]]+)\]", select, re.M)
selected_needs = set()
if needs_match:
selected_needs = {part.strip() for part in needs_match.group(1).split(",")}
missing = [job for job in native_jobs if job not in selected_needs]
if missing:
failures.append(
"select-package: must need every native candidate producer; missing "
+ ", ".join(missing))
timeout = re.search(r"^ timeout-minutes:\s*(\d+)\s*$", select, re.M)
if not timeout or int(timeout.group(1)) < 300:
failures.append(
"select-package: timeout-minutes must be at least 300 so the bounded\n"
" four-hour VirusTotal poll can complete before job cleanup.")
for token in ("--expect-targets 8", "--expect-candidates 24",
"VT_POLL_TIMEOUT_SECONDS: 14400",
"scripts/ci/select-release-candidates.py",
"scripts/ci/verify-release-selection.py",
'if ($i == "selected_sha256") sha_col = i',
'--expected-sha256 "$selected_sha"'):
if token not in select:
failures.append(f"select-package: missing required exact-set token: {token}")
if "release-candidate-scan/objects/*" not in select:
failures.append(
"select-package: VirusTotal must scan only the staged 16 candidate\n"
" objects, not archives, sidecars, or unrelated artifacts.")
if "--default-stripped" not in select or "unscanned-dry-run" not in select:
failures.append(
"select-package: the dry-run-only scan bypass must explicitly select\n"
" stripped candidates and record policy state unscanned-dry-run.")
for artifact in (
"binaries-linux-amd64",
"binaries-linux-arm64",
"binaries-linux-amd64-portable",
"binaries-linux-arm64-portable",
"binaries-darwin-amd64",
"binaries-darwin-arm64",
"binaries-windows-amd64",
"binaries-windows-arm64",
"release-selection-evidence",
):
if f"name: {artifact}" not in select:
failures.append(
f"select-package: missing canonical output artifact {artifact}")
for job in native_jobs:
body = build_jobs.get(job, "")
if not body:
failures.append(f"_build.yml: native producer job {job} is missing")
continue
if "release-candidates-" not in body:
failures.append(f"{job}: must upload release-candidates-* pair artifact")
if "name: binaries-" in body:
failures.append(
f"{job}: must not upload binaries-* before central VirusTotal selection")
# 4. Release cannot opt out of candidate scanning. Dry-run may do so only via
# its explicit, visible skip_virustotal input; either way smoke and soak see
# the reusable build only after selection/default-selection and packaging.
release_build = blocks.get("build", "")
if "scan_candidates: true" not in release_build or "secrets: inherit" not in release_build:
failures.append(
"release build: must pass scan_candidates: true and inherit the VT secret;\n"
" a release has no candidate-scan bypass.")
dry_jobs = workflow_jobs(dry_text)
dry_build = dry_jobs.get("build", "")
if "scan_candidates: ${{ !inputs.skip_virustotal }}" not in dry_build:
failures.append(
"dry-run build: scan_candidates must be exactly !inputs.skip_virustotal.")
if "secrets: inherit" not in dry_build:
failures.append("dry-run build: must inherit the VirusTotal API secret")
if "virustotal" in dry_jobs:
failures.append(
"dry-run: obsolete post-smoke virustotal job must be removed; candidate\n"
" scanning/selection belongs before smoke inside the build job.")
for caller_name, caller_jobs in (("release", blocks), ("dry-run", dry_jobs)):
for downstream in ("smoke", "soak"):
body = caller_jobs.get(downstream, "")
if not re.search(r"^ needs:\s*\[?build\]?\s*$", body, re.M):
failures.append(
f"{caller_name} {downstream}: must depend directly on completed build/select")
if "use_release_artifacts: true" not in caller_jobs.get("soak", ""):
failures.append(
f"{caller_name} soak: release-bound soak must consume the selected binaries-* artifacts")
soak_jobs = workflow_jobs(soak_text)
for job in ("soak-quick", "soak-quick-windows", "soak-quick-windows-arm64"):
body = soak_jobs.get(job, "")
for token in ("inputs.use_release_artifacts", "actions/download-artifact@", "cbm-selected-artifact"):
if token not in body:
failures.append(f"_soak.yml {job}: selected-artifact mode is missing {token}")
portable_soak = soak_jobs.get("soak-quick-linux-portable", "")
for token in ("if: ${{ inputs.use_release_artifacts }}",
"binaries-linux-${{ matrix.arch }}-portable",
"cbm-selected-artifact"):
if token not in portable_soak:
failures.append(f"_soak.yml portable soak: missing selected tuple token {token}")
# The draft must not merge all workflow artifacts: that would accidentally
# publish rejected candidates. Download canonical release containers by their
# historical name, and preserve only the small selection evidence separately.
draft_body = blocks.get("release-draft", "")
if "pattern: binaries-*" not in draft_body:
failures.append(
"release-draft: canonical container download must be narrowed to binaries-*")
if "name: release-selection-evidence" not in draft_body:
failures.append(
"release-draft: selection/VT provenance must be downloaded separately")
if re.search(r"uses:\s*actions/download-artifact@[^\n]+\n\s+with:\s*\n\s+merge-multiple:\s*true",
draft_body):
failures.append(
"release-draft: an unfiltered merge-multiple download could place rejected\n"
" candidate binaries in the public release staging directory.")
verify_body = blocks.get("verify", "")
if "needs.security.result == 'success'" not in verify_body:
failures.append(
"release verify: an explicitly skipped security dependency must block publication")
for token in ("--pattern 'release-selection.tsv'",
"scripts/ci/verify-release-selection.py",
'--selection "$RUNNER_TEMP/release-selection.tsv"',
"--require-policy virustotal-v2",
'--archive-dir "$ARCHIVE_DIR"'):
if token not in verify_body:
failures.append(
f"release verify: final draft-byte selection binding is missing token: {token}")
# The post-package scan is REQUIRED, not forbidden. It is not a duplicate of the
# candidate scan: that one covers executables only, while this one covers every
# distinct object extracted from the 14 shipped containers - install.sh,
# install.ps1, LICENSE, THIRD_PARTY_NOTICES.md, the MCPB manifest.json and the
# unpacked UI assets. Those are the bytes users pipe into a shell, and dropping
# them would narrow the promise made in README.md and SECURITY.md.
#
# Re-submitting the selected executables alongside them is close to free:
# VirusTotal is content-addressed and returns the analysis it already holds for
# identical bytes.
for token in ("crazy-max/ghaction-virustotal", "scripts/ci/check-virustotal.sh",
"scripts/ci/publish-vt-evidence.sh", "files: binaries/objects/*"):
if token not in verify_body:
failures.append(
f"release verify: full-surface VirusTotal pass is missing: {token}")
for token in ("release-candidates.tsv", "virustotal-candidate-results.tsv",
"release-selection.tsv", "scripts/ci/append-vt-notes.sh"):
if token not in verify_body:
failures.append(
f"release verify: existing candidate evidence is not reused: {token}")
if failures:
for f in failures:
print("FAIL: " + f, file=sys.stderr)
+458
View File
@@ -0,0 +1,458 @@
#!/usr/bin/env bash
# Contract: candidate staging, VT-policy selection, and final archive
# reconciliation form one content-bound fail-closed boundary.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
FIX="$(mktemp -d "${TMPDIR:-/tmp}/cbm-vt-select.XXXXXX")"
trap 'rm -rf "$FIX"' EXIT
STAGE="$ROOT/scripts/ci/stage-release-candidates.py"
SELECT="$ROOT/scripts/ci/select-release-candidates.py"
VERIFY="$ROOT/scripts/ci/verify-release-selection.py"
for tool in "$STAGE" "$SELECT" "$VERIFY"; do
[[ -x "$tool" ]] || { echo "FAIL: missing executable candidate-boundary tool: $tool" >&2; exit 1; }
done
python3 - "$ROOT" "$FIX" <<'PY'
from __future__ import annotations
import csv
import hashlib
import io
import os
import pathlib
import random
import shutil
import stat
import subprocess
import sys
import tarfile
import zipfile
root = pathlib.Path(sys.argv[1])
fix = pathlib.Path(sys.argv[2])
stage_tool = root / "scripts/ci/stage-release-candidates.py"
select_tool = root / "scripts/ci/select-release-candidates.py"
verify_tool = root / "scripts/ci/verify-release-selection.py"
TARGETS = (
"linux-amd64", "linux-arm64", "linux-amd64-portable",
"linux-arm64-portable", "darwin-amd64", "darwin-arm64",
"windows-amd64", "windows-arm64",
)
VARIANTS = ("unstripped", "debug-stripped", "stripped")
ORDER = ("stripped", "debug-stripped", "unstripped")
FIELDS = (
"target", "variant", "relative_path", "source_sha256", "pre_sign_sha256",
"sha256", "size", "format", "architecture", "linkage", "transform",
"signature", "strip_tool", "strip_version", "pair_verification",
)
RESULT_FIELDS = (
"scan_path", "sha256", "size", "association_count", "completed_engines",
"total_engines", "malicious", "suspicious", "analysis_id",
"microsoft_category", "microsoft_result", "policy_classification",
"microsoft_engine_version", "microsoft_engine_update", "virustotal_url",
)
ARCHIVES = tuple(
f"codebase-memory-mcp-{target}.tar.gz"
for target in TARGETS if not target.startswith("windows-")
) + tuple(
f"codebase-memory-mcp-{target}.zip"
for target in TARGETS if target.startswith("windows-")
) + tuple(
f"codebase-memory-mcp-{target}.mcpb"
for target in TARGETS
if target.startswith(("darwin-", "windows-")) or target.endswith("-portable")
)
def digest(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def run(*argv: object) -> subprocess.CompletedProcess[str]:
return subprocess.run([sys.executable, *(str(item) for item in argv)],
text=True, capture_output=True)
def require(condition: bool, message: str) -> None:
if not condition:
raise AssertionError(message)
def manifest(path: pathlib.Path) -> tuple[dict[str, str], list[dict[str, str]]]:
lines = path.read_text(encoding="utf-8").splitlines()
marker = lines[0]
metadata: dict[str, str] = {}
cursor = 1
while cursor < len(lines) and lines[cursor].startswith("# "):
key, separator, value = lines[cursor][2:].partition("=")
require(bool(separator), f"malformed metadata in {path}")
metadata[key] = value
cursor += 1
return {"marker": marker, **metadata}, list(csv.DictReader(lines[cursor:], delimiter="\t"))
def write_tsv(path: pathlib.Path, marker: str, metadata: dict[str, object],
fields: tuple[str, ...], rows: list[dict[str, object]]) -> None:
with path.open("w", encoding="utf-8", newline="") as handle:
handle.write(f"# {marker}\n")
for key, value in metadata.items():
handle.write(f"# {key}={value}\n")
writer = csv.DictWriter(handle, fieldnames=fields, delimiter="\t", lineterminator="\n")
writer.writeheader()
writer.writerows(rows)
def candidate_bytes(target: str, variant: str) -> bytes:
linked = f"codebase-memory-mcp:{target}:same-linker-output\n".encode()
suffix = {
"unstripped": b"debug-and-local-symbols\n",
"debug-stripped": b"local-symbols-only\n",
"stripped": b"stripped\n",
}[variant]
return linked + suffix
def make_artifacts(directory: pathlib.Path, *, reverse: bool = False) -> dict[tuple[str, str], bytes]:
objects: dict[tuple[str, str], bytes] = {}
targets = list(reversed(TARGETS)) if reverse else list(TARGETS)
for index, target in enumerate(targets):
artifact = directory / f"download-{index:02d}" / "nested" / target
artifact.mkdir(parents=True)
rows = []
source_hash = digest(candidate_bytes(target, "unstripped"))
for variant in VARIANTS:
name = "codebase-memory-mcp.exe" if target.startswith("windows-") else "codebase-memory-mcp"
data = candidate_bytes(target, variant)
objects[target, variant] = data
path = artifact / variant / name
path.parent.mkdir()
path.write_bytes(data)
path.chmod(0o500)
rows.append({
"target": target,
"variant": variant,
"relative_path": f"{variant}/{name}",
"source_sha256": source_hash,
"pre_sign_sha256": digest(data),
"sha256": digest(data),
"size": len(data),
"format": "pe" if target.startswith("windows-") else "macho" if target.startswith("darwin-") else "elf",
"architecture": target.split("-")[1],
"linkage": "portable" if target.endswith("-portable") else "dynamic" if target.startswith("linux-") else "native",
"transform": {"unstripped": "copy", "debug-stripped": "strip-debug", "stripped": "strip"}[variant],
"signature": "adhoc-verified" if target.startswith("darwin-") else "not-applicable",
"strip_tool": "contract-strip",
"strip_version": "1.0",
"pair_verification": "same-linker-output-v1",
})
write_tsv(artifact / "candidate-provenance.tsv",
"cbm-release-candidate-provenance-v1", {}, FIELDS, rows)
# Required target-bound notice adjacent to each downloaded artifact.
(artifact / "THIRD_PARTY_NOTICES.md").write_text(
f"third-party notice for {target}\n", encoding="utf-8")
return objects
def invoke_stage(source: pathlib.Path, output: pathlib.Path) -> subprocess.CompletedProcess[str]:
return run(stage_tool, source, output, "--expect-targets", 8, "--expect-candidates", 24)
artifacts = fix / "artifacts"
candidate_data = make_artifacts(artifacts)
staged = fix / "scan"
result = invoke_stage(artifacts, staged)
require(result.returncode == 0, f"valid 8-target candidate set rejected: {result.stdout}{result.stderr}")
require(sorted(path.name for path in staged.iterdir()) ==
["associations.tsv", "candidates.tsv", "notices", "objects", "scan-set.tsv"],
"stager output is not the exact atomic bundle")
cmeta, candidates = manifest(staged / "candidates.tsv")
require(cmeta["marker"] == "# cbm-release-candidates-v1", "candidate marker changed")
require(cmeta.get("targets") == "8" and cmeta.get("candidates") == "24", "candidate counts not bound")
require([(row["target"], row["variant"]) for row in candidates] ==
[(target, variant) for target in TARGETS for variant in VARIANTS],
"candidate rows are not in canonical target/variant order")
for target in TARGETS:
notice = staged / "notices" / target / "THIRD_PARTY_NOTICES.md"
require(notice.read_text(encoding="utf-8") == f"third-party notice for {target}\n",
f"wrong/missing target notice: {target}")
# Discovery/row order must not affect any staged manifest bytes.
reverse_artifacts = fix / "reverse-artifacts"
make_artifacts(reverse_artifacts, reverse=True)
reverse_stage = fix / "reverse-scan"
result = invoke_stage(reverse_artifacts, reverse_stage)
require(result.returncode == 0, f"reverse-order artifact set rejected: {result.stdout}{result.stderr}")
for name in ("candidates.tsv", "scan-set.tsv", "associations.tsv"):
require((staged / name).read_bytes() == (reverse_stage / name).read_bytes(),
f"{name} depends on discovery/row order")
def expect_stage_failure(label: str, mutate) -> None:
case = fix / f"bad-stage-{label}"
shutil.copytree(artifacts, case, symlinks=True)
mutate(case)
out = fix / f"bad-stage-out-{label}"
result = invoke_stage(case, out)
require(result.returncode != 0, f"stager accepted {label}")
require(not out.exists(), f"failed stage {label} published a partial output")
def provenance_paths(root_path: pathlib.Path) -> list[pathlib.Path]:
return sorted(root_path.rglob("candidate-provenance.tsv"))
expect_stage_failure("missing", lambda path: provenance_paths(path)[0].unlink())
expect_stage_failure("surplus", lambda path: shutil.copytree(
provenance_paths(path)[0].parent, path / "ninth" / "nested" / "extra"))
def replace_candidate_with_symlink(path: pathlib.Path) -> None:
provenances = provenance_paths(path)
binary = next(provenances[0].parent.glob("unstripped/*"))
# Windows maps the fixture's read-only mode to FILE_ATTRIBUTE_READONLY,
# which must be cleared before unlink; Unix only needs a writable parent.
binary.chmod(0o700)
binary.unlink()
target = provenances[1].parent / "unstripped" / (
"codebase-memory-mcp.exe" if "windows" in str(provenances[1])
else "codebase-memory-mcp"
)
binary.symlink_to(target)
expect_stage_failure("symlink", replace_candidate_with_symlink)
def tamper_candidate(path: pathlib.Path) -> None:
candidate = next(provenance_paths(path)[0].parent.glob("stripped/*"))
candidate.chmod(0o700)
candidate.write_bytes(b"tampered")
expect_stage_failure("tamper", tamper_candidate)
def result_rows(classifications: dict[tuple[str, str], str], *, shuffle: bool = False) -> list[dict[str, object]]:
rows = []
for row in candidates:
classification = classifications[row["target"], row["variant"]]
clean = classification == "clean"
rows.append({
"scan_path": row["scan_path"], "sha256": row["sha256"], "size": row["size"],
"association_count": 1, "completed_engines": 71, "total_engines": 71,
"malicious": 0 if clean else 1, "suspicious": 0,
"analysis_id": "analysis-" + row["sha256"][:24],
"microsoft_category": "undetected" if clean else "malicious",
"microsoft_result": "" if clean else "Trojan:Win32/Wacatac.B!ml",
"policy_classification": classification,
"microsoft_engine_version": "1.1.1", "microsoft_engine_update": "20260813",
"virustotal_url": "https://www.virustotal.com/gui/file/" + row["sha256"] + "/detection",
})
if shuffle:
random.Random(8675309).shuffle(rows)
return rows
def write_results(path: pathlib.Path, rows: list[dict[str, object]]) -> None:
write_tsv(path, "cbm-virustotal-results-v2", {
"scan_objects": 24, "associations": 24, "min_engines_policy": 50,
"min_completed_engines": 71, "max_completed_engines": 71,
}, RESULT_FIELDS, rows)
# Keys are (stripped, debug-stripped, unstripped) in the policy's preferred
# order. Three variants x two tolerated classifications is exactly eight
# combinations, so the eight targets below cover the table EXHAUSTIVELY.
TRUTH_KEYS = [
(a, b, c)
for a in ("clean", "microsoft-ml")
for b in ("clean", "microsoft-ml")
for c in ("clean", "microsoft-ml")
]
def expected_choice(key: tuple[str, str, str]) -> tuple[str, str]:
"""Return (selected_variant, decision) for a (stripped, debug, unstripped) key."""
by_variant = dict(zip(ORDER, key))
clean = [v for v in ORDER if by_variant[v] == "clean"]
if not clean:
return "stripped", "stripped-all-candidates-microsoft-ml"
chosen = clean[0]
if chosen == "stripped":
return "stripped", "stripped-preferred"
others = "-".join(f"{v}:{by_variant[v]}" for v in ORDER if v != chosen)
return chosen, f"{chosen}-clean-after-{others}"
def selection_rows(directory: pathlib.Path) -> tuple[dict[str, str], list[dict[str, str]]]:
return manifest(directory / "release-selection.tsv")
# Exhaustive truth table: distribute all four pairs across eight targets and
# repeat once, proving exact decision behavior and both result bindings.
classes: dict[tuple[str, str], str] = {}
expected: dict[str, str] = {}
expected_decisions: dict[str, str] = {}
require(len(TRUTH_KEYS) == len(TARGETS), "truth table must map one combination per target")
for index, target in enumerate(TARGETS):
key = TRUTH_KEYS[index]
for variant, classification in zip(ORDER, key):
classes[target, variant] = classification
expected[target], expected_decisions[target] = expected_choice(key)
results_file = fix / "vt-results.tsv"
write_results(results_file, result_rows(classes, shuffle=True))
selected = fix / "selected"
result = run(select_tool, "--candidates", staged / "candidates.tsv",
"--objects-dir", staged / "objects", "--out-dir", selected,
"--results", results_file)
require(result.returncode == 0, f"valid truth-table results rejected: {result.stdout}{result.stderr}")
smeta, selections = selection_rows(selected)
require(smeta["marker"] == "# cbm-release-selection-v1", "selection marker changed")
require(smeta.get("policy") == "virustotal-v2", "scanned selection policy not recorded")
require([row["target"] for row in selections] == list(TARGETS), "selection order is not canonical")
for row in selections:
target = row["target"]
require(row["selected_variant"] == expected[target], f"truth-table decision wrong for {target}")
expected_decision = expected_decisions[target]
require(row["decision"] == expected_decision,
f"truth-table decision reason wrong for {target}: {row['decision']!r}")
chosen = next(c for c in candidates if c["target"] == target and c["variant"] == expected[target])
path = selected / "selected" / target / ("codebase-memory-mcp.exe" if target.startswith("windows-") else "codebase-memory-mcp")
require(path.read_bytes() == candidate_data[target, expected[target]], f"wrong selected bytes for {target}")
require(row["selected_sha256"] == chosen["sha256"], f"selection hash wrong for {target}")
# Same logical results in canonical/reverse order produce identical evidence.
ordered_results = fix / "ordered-results.tsv"
write_results(ordered_results, result_rows(classes, shuffle=False))
selected_ordered = fix / "selected-ordered"
result = run(select_tool, "--candidates", staged / "candidates.tsv",
"--objects-dir", staged / "objects", "--out-dir", selected_ordered,
"--results", ordered_results)
require(result.returncode == 0, "canonical-order results unexpectedly rejected")
require((selected / "release-selection.tsv").read_bytes() ==
(selected_ordered / "release-selection.tsv").read_bytes(),
"selection evidence depends on VT result row order")
# Default policy is explicit dry-run evidence, not a fake clean result.
defaulted = fix / "default-selected"
result = run(select_tool, "--candidates", staged / "candidates.tsv",
"--objects-dir", staged / "objects", "--out-dir", defaulted,
"--default-stripped")
require(result.returncode == 0, f"default-stripped dry-run rejected: {result.stdout}{result.stderr}")
dmeta, drows = selection_rows(defaulted)
require(dmeta.get("policy") == "unscanned-dry-run", "default policy is not visibly marked unscanned")
require(all(row["selected_variant"] == "stripped" and
row["unstripped_classification"] == "unscanned-dry-run" and
row["stripped_classification"] == "unscanned-dry-run" for row in drows),
"default policy does not bind every target to visibly unscanned stripped bytes")
def expect_select_failure(label: str, mutate) -> None:
rows = result_rows({key: "clean" for key in candidate_data})
mutate(rows)
evidence = fix / f"bad-results-{label}.tsv"
write_results(evidence, rows)
out = fix / f"bad-selected-{label}"
result = run(select_tool, "--candidates", staged / "candidates.tsv",
"--objects-dir", staged / "objects", "--out-dir", out,
"--results", evidence)
require(result.returncode != 0, f"selector accepted {label}")
require(not out.exists(), f"failed selection {label} published partial output")
expect_select_failure("hard", lambda rows: rows[0].update(policy_classification="hard"))
expect_select_failure("incomplete", lambda rows: rows[0].update(completed_engines=49))
expect_select_failure("missing", lambda rows: rows.pop())
expect_select_failure("surplus", lambda rows: rows.append(dict(rows[0], analysis_id="extra-analysis")))
expect_select_failure("tampered-hash", lambda rows: rows[0].update(sha256="0" * 64))
expect_select_failure("cross-target", lambda rows: (
rows[0].update(scan_path=rows[2]["scan_path"], sha256=rows[2]["sha256"], size=rows[2]["size"]),
rows[2].update(scan_path=candidates[0]["scan_path"], sha256=candidates[0]["sha256"], size=candidates[0]["size"]),
))
# Reconcile all 14 canonical containers, recursively, against the selected
# eight hashes. Eligible MCPBs repeat exactly the matching selected bytes.
archives = fix / "containers"
archive_for: dict[str, bytes] = {}
for row in selections:
target = row["target"]
archive_for[target] = candidate_data[target, row["selected_variant"]]
for name in ARCHIVES:
target = next(target for target in TARGETS if name.startswith(f"codebase-memory-mcp-{target}."))
destination = archives / target / ("mcpb" if name.endswith(".mcpb") else "archive") / name
destination.parent.mkdir(parents=True, exist_ok=True)
binary_name = "codebase-memory-mcp.exe" if target.startswith("windows-") else "codebase-memory-mcp"
member = f"server/{binary_name}" if name.endswith(".mcpb") else binary_name
if name.endswith((".zip", ".mcpb")):
with zipfile.ZipFile(destination, "w") as handle:
info = zipfile.ZipInfo(member)
info.create_system = 3
permissions = 0o755 if not target.startswith("windows-") else 0o644
info.external_attr = (stat.S_IFREG | permissions) << 16
handle.writestr(info, archive_for[target])
else:
with tarfile.open(destination, "w:gz") as handle:
info = tarfile.TarInfo(member)
info.size = len(archive_for[target])
info.mode = 0o755
handle.addfile(info, io.BytesIO(archive_for[target]))
result = run(verify_tool, "--selection", selected / "release-selection.tsv",
"--archive-dir", archives)
require(result.returncode == 0, f"exact canonical containers rejected: {result.stdout}{result.stderr}")
def expect_verify_failure(label: str, mutate) -> None:
case = fix / f"bad-containers-{label}"
shutil.copytree(archives, case)
mutate(case)
result = run(verify_tool, "--selection", selected / "release-selection.tsv",
"--archive-dir", case)
require(result.returncode != 0, f"archive verifier accepted {label}")
expect_verify_failure("missing", lambda path: next(path.rglob("*.mcpb")).unlink())
expect_verify_failure("surplus", lambda path: (path / "surplus.zip").write_bytes(b"not allowed"))
def mutate_zip(path: pathlib.Path) -> None:
archive = next(path.rglob("codebase-memory-mcp-windows-amd64.zip"))
with zipfile.ZipFile(archive, "w") as handle:
handle.writestr("codebase-memory-mcp.exe", b"mutated-selected-binary")
expect_verify_failure("mutation", mutate_zip)
def remove_unix_executable_mode(path: pathlib.Path) -> None:
archive = next(path.rglob("codebase-memory-mcp-darwin-arm64.mcpb"))
with zipfile.ZipFile(archive, "r") as handle:
entries = [(info, handle.read(info)) for info in handle.infolist()]
with zipfile.ZipFile(archive, "w") as handle:
for info, payload in entries:
if info.filename == "server/codebase-memory-mcp":
info.create_system = 3
info.external_attr = (stat.S_IFREG | 0o644) << 16
handle.writestr(info, payload)
expect_verify_failure("unix-executable-mode", remove_unix_executable_mode)
def wrong_target(path: pathlib.Path) -> None:
archive = next(path.rglob("codebase-memory-mcp-linux-amd64.tar.gz"))
with tarfile.open(archive, "w:gz") as handle:
data = archive_for["linux-arm64"]
info = tarfile.TarInfo("codebase-memory-mcp")
info.size = len(data)
handle.addfile(info, io.BytesIO(data))
expect_verify_failure("wrong-target", wrong_target)
print("PASS: exact candidate staging, exhaustive VT truth table, fail-closed selection, deterministic evidence, dry-run marking, and 14-container reconciliation")
PY
+49 -7
View File
@@ -19,6 +19,19 @@ hash_file() {
sha256sum "$1" 2>/dev/null | awk '{print $1}' || shasum -a 256 "$1" | awk '{print $1}'
}
# Public policy surfaces must not promise a stricter/different gate than the
# one actually enforced here. Native ClamAV/Defender jobs remain separate and
# may still use an any-detection-fails rule.
for surface in "$ROOT/README.md" "$ROOT/SECURITY.md" "$ROOT/docs/index.html"; do
if grep -Eqi '(zero malicious( and| or) zero suspicious (required|verdicts required)|zero malicious or suspicious verdicts (are )?required|no exception path in this release gate)' "$surface"; then
fail "stale zero-tolerance VirusTotal promise contradicts the Microsoft !ml policy: $surface"
fi
done
grep -Fq 'Microsoft `!ml` tolerance' "$ROOT/README.md" || \
fail "README must link the exact documented Microsoft !ml tolerance"
grep -Fq 'Policy identifier: `cbm-vt-candidate-selection-v1`' "$ROOT/SECURITY.md" || \
fail "SECURITY.md must name the versioned candidate-selection policy"
# Tripwire for the REVERTED endpoint-verification mechanism specifically. The
# current `!ml` tolerance is a policy branch inside this gate, not a callout to
# an external verification service, and must never become one.
@@ -105,7 +118,16 @@ write_response wrong-size 0 0 60 "$PROBE_SHA" "$((PROBE_SIZE + 1))"
cat > "$FIX/responses/upload-alias.json" <<EOF
{"meta":{"file_info":{"sha256":"$PROBE_SHA","size":$PROBE_SIZE}},
"data":{"id":"canonical-analysis","type":"analysis","attributes":{"status":"completed",
"data":{"id":"upload-alias","type":"analysis","attributes":{"status":"completed",
"stats":{"malicious":0,"suspicious":0,"undetected":60,"harmless":0,
"timeout":0,"confirmed-timeout":0,"failure":0,"type-unsupported":0},
"results":{"Microsoft":{"category":"undetected","result":null,
"engine_version":"1.26070","engine_update":"20260808"}}}}}
EOF
cat > "$FIX/responses/mismatched-response-id.json" <<EOF
{"meta":{"file_info":{"sha256":"$PROBE_SHA","size":$PROBE_SIZE}},
"data":{"id":"different-analysis","type":"analysis","attributes":{"status":"completed",
"stats":{"malicious":0,"suspicious":0,"undetected":60,"harmless":0,
"timeout":0,"confirmed-timeout":0,"failure":0,"type-unsupported":0},
"results":{"Microsoft":{"category":"undetected","result":null,
@@ -258,22 +280,36 @@ clean_output="binaries/objects/probe=$(url_for clean-analysis)"
[ "$(run_gate "$clean_output")" = "0" ] || fail "clean exact scan must pass: $(cat "$FIX/last.log")"
alias_output="binaries/objects/probe=$(url_for upload-alias)"
[ "$(run_gate "$alias_output")" = "0" ] || \
fail "a content-bound canonical response alias must pass: $(cat "$FIX/last.log")"
fail "a content-bound action-output path alias must pass: $(cat "$FIX/last.log")"
RESULTS="$FIX/work/binaries/vt-results.tsv"
[ -f "$RESULTS" ] || fail "clean gate did not atomically publish its results manifest"
grep -q '^# cbm-virustotal-results-v1$' "$RESULTS" || fail "results marker missing"
grep -q '^# cbm-virustotal-results-v2$' "$RESULTS" || fail "results marker missing"
grep -q "objects/probe.*$PROBE_SHA.*$PROBE_SIZE.*60" "$RESULTS" || \
fail "results do not bind path/hash/size/actual engine count"
grep -Fq 'upload-alias' "$RESULTS" || \
fail "results must retain the action's submitted analysis ID"
grep -q $'undetected\t\tclean\t' "$RESULTS" || \
fail "clean result must carry an explicit clean policy classification"
# A differing analysis ID is NOT a failure: VirusTotal is content-addressed and
# may answer for already-known bytes with its own canonical analysis. The
# verdict is bound to the object by hash and size (wrong-hash / wrong-size
# below), which is what actually keeps a tuple's stripped and unstripped
# candidates apart — they differ in hash by construction.
[ "$(run_gate "binaries/objects/probe=$(url_for mismatched-response-id)")" = "0" ] || \
fail "a content-bound response must pass even when VirusTotal returns its own analysis ID: $(cat "$FIX/last.log")"
for id in one-malicious one-suspicious; do
[ "$(run_gate "binaries/objects/probe=$(url_for "$id")")" != "0" ] || \
fail "$id must block"
done
[ "$(run_gate "binaries/objects/probe=$(url_for low-engines)")" != "0" ] || \
fail "completed analysis below the 50-engine policy must block"
grep -q 'only 49/49 decisive engines' "$FIX/last.log" || fail "low-engine failure is not explicit"
# A low decisive-engine count must NOT block. It is VirusTotal fleet
# availability on the day, not a property of our binary: a ~300 MB release
# artifact is skipped or timed out by many engines, so the count moves run to
# run and a floor turns shipping into a lottery. It is reported, not enforced.
[ "$(run_gate "binaries/objects/probe=$(url_for low-engines)")" = "0" ] || \
fail "a clean verdict must stand regardless of how many engines answered: $(cat "$FIX/last.log")"
grep -q 'NOTE: .*49/49 decisive engines' "$FIX/last.log" || \
fail "a below-reference engine count must still be reported explicitly"
# A single Microsoft `!ml` verdict is the tolerated case: it passes, but it is
# reported and recorded rather than silently downgraded to "clean".
@@ -285,6 +321,9 @@ grep -q 'Microsoft = Trojan:Script/Wacatac.B!ml' "$FIX/last.log" || \
fail "gate must report the flagging engine and label"
grep -q $'objects/probe\t.*\t1\t0\t' "$FIX/work/binaries/vt-results.tsv" || \
fail "tolerated detection was not preserved as workflow evidence"
grep -Fq $'malicious\tTrojan:Script/Wacatac.B!ml\tmicrosoft-ml\t' \
"$FIX/work/binaries/vt-results.tsv" || \
fail "tolerated result must retain Microsoft's exact label and policy classification"
if grep -q 'detected by: Kaspersky' "$FIX/last.log"; then
fail "gate listed a non-flagging engine as a detection"
fi
@@ -292,6 +331,9 @@ fi
# Everything adjacent to the tolerated case still blocks.
[ "$(run_gate "binaries/objects/probe=$(url_for ms-signature)")" != "0" ] || \
fail "a Microsoft signature (non-!ml) detection must block"
grep -Fq $'malicious\tTrojan:Win32/Emotet\thard\t' \
"$FIX/work/binaries/vt-results.tsv" || \
fail "blocking result must be recorded as hard with its exact label"
[ "$(run_gate "binaries/objects/probe=$(url_for other-engine)")" != "0" ] || \
fail "a non-Microsoft detection must block"
[ "$(run_gate "binaries/objects/probe=$(url_for two-engines)")" != "0" ] || \
@@ -357,4 +399,4 @@ printf 'tampered\n' > "$FIX/work/binaries/objects/probe"
[ "$(run_gate "$clean_output")" != "0" ] || fail "tampered staged object must block before polling"
grep -q 'changed after extraction' "$FIX/last.log" || fail "tamper failure is not diagnosable"
echo 'PASS: VT gate is exact-set, content-bound, >=50-engine; tolerates exactly one Microsoft !ml verdict and nothing else'
echo 'PASS: VT gate is exact-set and content-bound; tolerates exactly one Microsoft !ml verdict and nothing else; engine count is evidence, not a gate'
+168 -123
View File
@@ -1,46 +1,151 @@
#!/usr/bin/env bash
# Contract for idempotent, evidence-bound VirusTotal release-note publication.
# Contract: release notes reuse the exact pre-smoke candidate verdicts.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SCRIPT="$ROOT/scripts/ci/append-vt-notes.sh"
PUBLISH="$ROOT/scripts/ci/publish-vt-evidence.sh"
FIX="$(mktemp -d "${TMPDIR:-/tmp}/vt-notes-contract.XXXXXX")"
trap 'rm -rf "$FIX"' EXIT
fail() { echo "FAIL: $*" >&2; exit 1; }
mkdir -p "$FIX/bin" "$FIX/binaries"
ARCHIVE_SHA='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
MEMBER_SHA='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
ASSET_SHA='cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'
cat > "$FIX/binaries/associations.tsv" <<EOF
# cbm-release-scan-associations-v3
# archives=1
# binaries=1
# runtime_files=1
# associations=2
# scan_objects=2
association_type archive archive_sha256 variant kind member asset_path mime scan_path object_sha256 size
member codebase-memory-mcp-linux-amd64.tar.gz $ARCHIVE_SHA release binary codebase-memory-mcp objects/member $MEMBER_SHA 40
member codebase-memory-mcp-linux-amd64.tar.gz $ARCHIVE_SHA release runtime LICENSE objects/license $ASSET_SHA 20
EOF
cat > "$FIX/binaries/vt-results.tsv" <<EOF
# cbm-virustotal-results-v1
# scan_objects=2
# associations=2
# min_engines_policy=50
# min_completed_engines=59
# max_completed_engines=61
scan_path sha256 size association_count completed_engines total_engines malicious suspicious analysis_id microsoft_category microsoft_engine_version microsoft_engine_update virustotal_url
objects/member $MEMBER_SHA 40 1 59 62 0 0 analysis-a undetected 1.26070 20260808 https://www.virustotal.com/gui/file/$MEMBER_SHA/detection
objects/license $ASSET_SHA 20 1 61 64 0 0 analysis-b undetected 1.26070 20260808 https://www.virustotal.com/gui/file/$ASSET_SHA/detection
EOF
mkdir -p "$FIX/bin" "$FIX/evidence"
python3 - "$FIX/evidence" <<'PY'
import csv
import hashlib
import pathlib
import sys
root = pathlib.Path(sys.argv[1])
targets = (
"linux-amd64", "linux-arm64", "linux-amd64-portable",
"linux-arm64-portable", "darwin-amd64", "darwin-arm64",
"windows-amd64", "windows-arm64",
)
variants = ("unstripped", "debug-stripped", "stripped")
field_key = {v: v.replace("-", "_") for v in variants}
candidate_fields = (
"target", "variant", "relative_path", "source_sha256", "pre_sign_sha256",
"sha256", "size", "format", "architecture", "linkage", "transform",
"signature", "strip_tool", "strip_version", "pair_verification", "scan_path",
)
result_fields = (
"scan_path", "sha256", "size", "association_count", "completed_engines",
"total_engines", "malicious", "suspicious", "analysis_id",
"microsoft_category", "microsoft_result", "policy_classification",
"microsoft_engine_version", "microsoft_engine_update", "virustotal_url",
)
selection_fields = (
"target", "selected_variant", "selected_path", "selected_sha256",
"selected_size", "decision", "unstripped_sha256", "unstripped_scan_path",
"unstripped_classification", "unstripped_analysis_id",
"unstripped_virustotal_url",
"debug_stripped_sha256", "debug_stripped_scan_path",
"debug_stripped_classification", "debug_stripped_analysis_id",
"debug_stripped_virustotal_url",
"stripped_sha256", "stripped_scan_path",
"stripped_classification", "stripped_analysis_id", "stripped_virustotal_url",
)
def digest(value: str) -> str:
return hashlib.sha256(value.encode()).hexdigest()
def write(path, marker, metadata, fields, rows):
with path.open("w", encoding="utf-8", newline="") as handle:
handle.write(f"# {marker}\n")
for key, value in metadata.items():
handle.write(f"# {key}={value}\n")
writer = csv.DictWriter(handle, fieldnames=fields, delimiter="\t", lineterminator="\n")
writer.writeheader()
writer.writerows(rows)
classes = {(target, variant): "clean" for target in targets for variant in variants}
classes["linux-amd64", "stripped"] = "microsoft-ml"
classes["darwin-arm64", "stripped"] = "microsoft-ml"
classes["darwin-arm64", "unstripped"] = "microsoft-ml"
candidates = []
results = []
by_pair = {}
for target in targets:
source_sha = digest(f"{target}:linker-output")
for variant in variants:
sha = digest(f"{target}:{variant}")
name = "codebase-memory-mcp.exe" if target.startswith("windows-") else "codebase-memory-mcp"
row = {
"target": target, "variant": variant, "relative_path": f"{variant}/{name}",
"source_sha256": source_sha, "pre_sign_sha256": sha, "sha256": sha,
"size": "1234", "format": "pe" if target.startswith("windows-") else "macho" if target.startswith("darwin-") else "elf",
"architecture": target.split("-")[1],
"linkage": "portable" if target.endswith("-portable") else "dynamic" if target.startswith("linux-") else "native",
"transform": {"unstripped": "copy", "debug-stripped": "strip-debug", "stripped": "strip"}[variant],
"signature": "adhoc-verified" if target.startswith("darwin-") else "not-applicable",
"strip_tool": "strip", "strip_version": "contract",
"pair_verification": "same-linker-output-v1", "scan_path": f"objects/{sha}",
}
candidates.append(row)
by_pair[target, variant] = row
classification = classes[target, variant]
clean = classification == "clean"
results.append({
"scan_path": row["scan_path"], "sha256": sha, "size": row["size"],
"association_count": "1", "completed_engines": "61", "total_engines": "64",
"malicious": "0" if clean else "1", "suspicious": "0",
"analysis_id": f"analysis-{sha[:20]}",
"microsoft_category": "undetected" if clean else "malicious",
"microsoft_result": "" if clean else "Trojan:Win32/Wacatac.B!ml",
"policy_classification": classification,
"microsoft_engine_version": "1.1.1", "microsoft_engine_update": "20260813",
"virustotal_url": f"https://www.virustotal.com/gui/file/{sha}/detection",
})
result_by_pair = {(row["target"], row["variant"]): result for row, result in zip(candidates, results)}
selections = []
for target in targets:
order = ("stripped", "debug-stripped", "unstripped")
clean = [v for v in order if classes[target, v] == "clean"]
chosen = clean[0] if clean else "stripped"
selected = by_pair[target, chosen]
if not clean:
decision = "stripped-all-candidates-microsoft-ml"
elif chosen == "stripped":
decision = "stripped-preferred"
else:
others = "-".join(f"{v}:{classes[target, v]}" for v in order if v != chosen)
decision = f"{chosen}-clean-after-{others}"
row = {
"target": target, "selected_variant": chosen,
"selected_path": f"selected/{target}/" + ("codebase-memory-mcp.exe" if target.startswith("windows-") else "codebase-memory-mcp"),
"selected_sha256": selected["sha256"], "selected_size": selected["size"],
"decision": decision,
}
for variant in variants:
candidate = by_pair[target, variant]
result = result_by_pair[target, variant]
row.update({
f"{field_key[variant]}_sha256": candidate["sha256"],
f"{field_key[variant]}_scan_path": candidate["scan_path"],
f"{field_key[variant]}_classification": result["policy_classification"],
f"{field_key[variant]}_analysis_id": result["analysis_id"],
f"{field_key[variant]}_virustotal_url": result["virustotal_url"],
})
selections.append(row)
write(root / "release-candidates.tsv", "cbm-release-candidates-v1",
{"targets": 8, "candidates": 24}, candidate_fields, candidates)
write(root / "virustotal-candidate-results.tsv", "cbm-virustotal-results-v2",
{"scan_objects": 24, "associations": 24, "min_engines_policy": 50,
"min_completed_engines": 61, "max_completed_engines": 61}, result_fields, results)
write(root / "release-selection.tsv", "cbm-release-selection-v1",
{"policy": "virustotal-v2", "targets": 8, "candidates": 24},
selection_fields, selections)
PY
cat > "$FIX/current.md" <<'EOF'
Intro text.
<!-- cbm-security-verification:start -->
## Security Verification
stale data
<!-- cbm-security-verification:end -->
@@ -63,84 +168,58 @@ if [ "$1" = release ] && [ "$2" = edit ]; then
shift
done
fi
if [ "$1" = release ] && [ "$2" = upload ]; then
shift 3 # release upload <tag>
while [ "$#" -gt 0 ]; do
case "$1" in
--clobber) shift ;;
--repo) shift 2 ;;
*) cp "$1" "$STUB_UPLOAD_DIR/$(basename "$1")"; shift ;;
esac
done
exit 0
fi
exit 2
EOF
chmod +x "$FIX/bin/gh"
run_notes() { # current capture
run_notes() {
(cd "$FIX" &&
PATH="$FIX/bin:$PATH" \
GH_TOKEN=stub \
VERSION=v1.0.0 \
GH_TOKEN=stub VERSION=v1.0.0 \
GITHUB_REPOSITORY=DeusData/codebase-memory-mcp \
VT_ASSOCIATIONS=binaries/associations.tsv \
VT_RESULTS_PATH=binaries/vt-results.tsv \
STUB_RELEASE_BODY="$1" \
STUB_CAPTURE="$2" \
VT_CANDIDATES=evidence/release-candidates.tsv \
VT_RESULTS_PATH=evidence/virustotal-candidate-results.tsv \
RELEASE_SELECTION=evidence/release-selection.tsv \
STUB_RELEASE_BODY="$1" STUB_CAPTURE="$2" \
bash "$SCRIPT")
}
run_notes "$FIX/current.md" "$FIX/first.md"
[ "$(grep -c 'cbm-security-verification:start' "$FIX/first.md")" = 1 ] || fail "start marker duplicated"
[ "$(grep -c 'cbm-security-verification:end' "$FIX/first.md")" = 1 ] || fail "end marker duplicated"
grep -q 'Intro text.' "$FIX/first.md" || fail "content before marked section was lost"
grep -q 'Outro text.' "$FIX/first.md" || fail "content after marked section was lost"
! grep -q 'stale data' "$FIX/first.md" || fail "stale marked section was appended instead of replaced"
grep -q '2 distinct extracted byte objects' "$FIX/first.md" || fail "unique extracted-object scope missing"
grep -q '2 exact extracted archive members' "$FIX/first.md" || fail "extracted member scope missing"
grep -q '1 downloadable archive' "$FIX/first.md" || fail "archive provenance scope missing"
grep -q '59–61 decisive engine results' "$FIX/first.md" || fail "measured engine range missing"
grep -q 'Microsoft returned a decisive clean verdict for all 1 executable objects' "$FIX/first.md" || \
fail "executable Microsoft coverage is not stated"
grep -q "$ARCHIVE_SHA" "$FIX/first.md" || fail "full archive SHA-256 provenance missing"
grep -q '| Downloadable archive | SHA-256 provenance |' "$FIX/first.md" || \
fail "archive SHA provenance table missing"
grep -q 'Downloadable .tar.gz/.zip release containers were not submitted to VirusTotal' "$FIX/first.md" || \
fail "archive exclusion is not explicit"
! grep -q "gui/file/$ARCHIVE_SHA/detection" "$FIX/first.md" || \
fail "archive VirusTotal link must not be published"
! grep -q '| Engines | VirusTotal |' "$FIX/first.md" || \
fail "archive provenance table must not imply archive scans"
for asset in virustotal-associations.tsv virustotal-scan-set.tsv virustotal-results.tsv virustotal-evidence-checksums.txt; do
grep -q "releases/download/v1.0.0/$asset" "$FIX/first.md" || fail "public evidence link missing: $asset"
grep -q 'Intro text.' "$FIX/first.md" || fail "content before section was lost"
grep -q 'Outro text.' "$FIX/first.md" || fail "content after section was lost"
! grep -q 'stale data' "$FIX/first.md" || fail "stale section was appended"
grep -q '24 executable scans' "$FIX/first.md" || fail "candidate scan scope missing"
grep -q '8 release products' "$FIX/first.md" || fail "product scope missing"
grep -q '3 candidate(s).*Microsoft' "$FIX/first.md" || fail "tolerated findings not disclosed"
grep -q 'no other decisive engine reported malicious or suspicious' "$FIX/first.md" || fail "non-decisive engines are overstated as clean"
! grep -q 'every other engine was clean' "$FIX/first.md" || fail "non-decisive engines are incorrectly described as clean"
grep -q '| Product | Stripped candidate | Unstripped candidate | Shipped |' "$FIX/first.md" || fail "candidate table missing"
grep -q '| `linux-amd64` |' "$FIX/first.md" || fail "Linux product row missing"
grep -q '| `windows-arm64` |' "$FIX/first.md" || fail "Windows product row missing"
grep -q 'archive containers were not redundantly submitted' "$FIX/first.md" || fail "no-rescan boundary missing"
for asset in release-candidates.tsv virustotal-candidate-results.tsv release-selection.tsv; do
grep -q "releases/download/v1.0.0/$asset" "$FIX/first.md" || fail "evidence link missing: $asset"
done
! grep -q '70+' "$FIX/first.md" || fail "unmeasured engine claim returned"
run_notes "$FIX/first.md" "$FIX/second.md"
# python3 rather than cmp/diff: this contract runs on the Windows VM's MSYS2
# shell too, where neither is guaranteed to be on PATH (cmp is not).
python3 -c 'import sys;sys.exit(0 if open(sys.argv[1],"rb").read()==open(sys.argv[2],"rb").read() else 1)' \
"$FIX/first.md" "$FIX/second.md" || fail "marked release-note update is not idempotent"
python3 -c 'import sys;sys.exit(open(sys.argv[1],"rb").read()!=open(sys.argv[2],"rb").read())' \
"$FIX/first.md" "$FIX/second.md" || fail "release-note update is not idempotent"
cp "$FIX/binaries/vt-results.tsv" "$FIX/binaries/vt-results.clean.tsv"
printf '\textra-cell\n' >> "$FIX/binaries/vt-results.tsv"
if run_notes "$FIX/current.md" "$FIX/surplus-capture.md"; then
fail "surplus result TSV cells must fail closed"
cp "$FIX/evidence/release-selection.tsv" "$FIX/evidence/release-selection.clean.tsv"
python3 - "$FIX/evidence/release-selection.tsv" <<'PY'
import pathlib, sys
p = pathlib.Path(sys.argv[1])
text = p.read_text()
p.write_text(text.replace("\t1234\tstripped-preferred", "\t9999\tstripped-preferred", 1))
PY
if run_notes "$FIX/current.md" "$FIX/tampered.md"; then
fail "selection hash/size mismatch must fail closed"
fi
mv "$FIX/binaries/vt-results.clean.tsv" "$FIX/binaries/vt-results.tsv"
cp "$FIX/binaries/associations.tsv" "$FIX/binaries/associations.clean.tsv"
sed 's/# archives=1/# archives=2/' "$FIX/binaries/associations.clean.tsv" > \
"$FIX/binaries/associations.tsv"
if run_notes "$FIX/current.md" "$FIX/archive-count-capture.md"; then
fail "archive provenance count must match association metadata"
fi
mv "$FIX/binaries/associations.clean.tsv" "$FIX/binaries/associations.tsv"
mv "$FIX/evidence/release-selection.clean.tsv" "$FIX/evidence/release-selection.tsv"
cat > "$FIX/reversed.md" <<'EOF'
Intro text.
<!-- cbm-security-verification:end -->
stale reversed data
<!-- cbm-security-verification:start -->
@@ -149,38 +228,4 @@ if run_notes "$FIX/reversed.md" "$FIX/reversed-capture.md"; then
fail "reversed verification markers must fail closed"
fi
mkdir -p "$FIX/public"
run_publish() {
(cd "$FIX" &&
PATH="$FIX/bin:$PATH" \
GH_TOKEN=stub \
VERSION=v1.0.0 \
GITHUB_REPOSITORY=DeusData/codebase-memory-mcp \
VT_ASSOCIATIONS=binaries/associations.tsv \
VT_EXPECTED_SCAN_SET=binaries/scan-set.tsv \
VT_RESULTS_PATH=binaries/vt-results.tsv \
STUB_UPLOAD_DIR="$FIX/public" \
bash "$PUBLISH")
}
cat > "$FIX/binaries/scan-set.tsv" <<EOF
# cbm-release-scan-set-v2
# scan_objects=2
# associations=2
scan_path sha256 size association_count association_kinds
objects/member $MEMBER_SHA 40 1 binary
objects/license $ASSET_SHA 20 1 runtime
EOF
run_publish
for asset in virustotal-associations.tsv virustotal-scan-set.tsv virustotal-results.tsv virustotal-evidence-checksums.txt; do
[ -s "$FIX/public/$asset" ] || fail "public evidence asset was not uploaded: $asset"
done
[ "$(wc -l < "$FIX/public/virustotal-evidence-checksums.txt" | tr -d ' ')" = 3 ] || \
fail "public evidence checksum inventory is incomplete"
cp "$FIX/binaries/scan-set.tsv" "$FIX/binaries/scan-set.clean.tsv"
sed '1s/cbm-release-scan-set-v2/wrong-marker/' "$FIX/binaries/scan-set.clean.tsv" > "$FIX/binaries/scan-set.tsv"
if run_publish; then
fail "public evidence publication must reject a wrong scan-set marker"
fi
mv "$FIX/binaries/scan-set.clean.tsv" "$FIX/binaries/scan-set.tsv"
echo 'PASS: VT release evidence is durable, scoped, measured, full-hash and idempotent'
echo 'PASS: release notes reuse all 24 candidate verdicts, disclose selection, and avoid a duplicate scan'
+35 -9
View File
@@ -83,7 +83,7 @@ windows_archive_names = (binary, "LICENSE", "install.ps1", "THIRD_PARTY_NOTICES.
# defined and cannot fork per venue.
package_release = read("scripts/package-release.sh")
zip_call = re.search(
r"zip -q \"\$OUT_DIR/\$NAME\.zip\" \\\n(?P<members>(?:.|\n)*?)\n", package_release
r'zip -q -X "\$ARCHIVE_TMP" \\\n\s+(?P<members>[^\n]+)', package_release
)
require(zip_call is not None, "package-release.sh must build the Windows zip in one zip call")
zip_members = zip_call.group("members").split() if zip_call else []
@@ -92,6 +92,13 @@ require(
"package-release.sh must archive EXACTLY "
f"{' '.join(windows_archive_names)} (found: {' '.join(zip_members) or 'nothing'})",
)
require(
"--selected-binary" in package_release
and "--expected-sha256" in package_release
and 'assert_expected_hash "$VERIFY_DIR/archive/$STAGED_BINARY_NAME"' in package_release,
"package-release.sh must bind the selected input and archived executable to the same "
"caller-supplied SHA-256",
)
# ── 2. No shipped surface may name the payload or build a launcher ───────────
# These are every file that decides what the Windows release contains or how a
@@ -140,18 +147,37 @@ require(
)
# Every archive is produced through the ONE canonical packaging entry, so the
# four-file layout above governs all of them. A reappearing --variant flag would
# mean the retired two-composition split is back.
# four-file layout above governs all of them. Selection may choose stripped or
# unstripped only inside the immutable Windows architecture tuple; it must hand
# final bytes to the packager by hash, never revive a payload composition.
build_workflow = read(".github/workflows/_build.yml")
for archive, call in (
("codebase-memory-mcp-windows-amd64.zip", "scripts/package-release.sh windows amd64"),
("codebase-memory-mcp-windows-arm64.zip", "scripts/package-release.sh windows arm64"),
):
require(
"scripts/package-release.sh" in build_workflow
and "--selected-binary" in build_workflow
and "--expected-sha256" in build_workflow,
"_build.yml must hand hash-bound selected bytes to the canonical packager",
)
for target in ("windows-amd64", "windows-arm64"):
require(
re.search(re.escape(call) + r"(?!\s+--variant)", build_workflow) is not None,
f"_build.yml must produce {archive} via the canonical packaging entry ('{call}')",
target in build_workflow,
f"_build.yml must retain the {target} release product",
)
artifact_smoke = read("scripts/ci/smoke-artifact.sh")
require(
"scripts/ci/prepare-release-candidates.sh" in artifact_smoke
and '${GOOS}-${GOARCH}/stripped/$SELECTED_NAME' in artifact_smoke
and "--selected-binary" in artifact_smoke
and "--expected-sha256" in artifact_smoke,
"local artifact smoke must derive both candidates, default-select stripped within its "
"target tuple, and package the selected bytes by SHA-256",
)
require(
'codesign --sign' not in artifact_smoke,
"local artifact smoke must not sign after candidate derivation; packaging starts from "
"already-final bytes",
)
# ── 3. install.ps1 retires the running binary before publishing ──────────────
# This is THE step that replaces the launcher. Windows keeps an image lock on a
# running .exe: it cannot be overwritten, but it CAN be renamed out of the way.