Files
deusdata--codebase-memory-mcp/tests/test_mcpb_bundle_contract.sh
2026-08-13 19:16:19 +02:00

247 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
# Contract: scripts/package-release.sh builds the MCPB bundles the MCP
# Registry advertises (#1246) — exactly where eligible, never elsewhere.
#
# A broken bundle is worse than none: an mcpb registry entry points hosts at
# one-click install, so a wrong member set, a manifest that mis-names the
# entry point, or a lost executable bit fails AT THE USER, on a machine we
# never see. This contract pins the bundle's shape at its single canonical
# producer, for every target family, on every leg.
#
# 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. 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)"
FIX="$(mktemp -d "${TMPDIR:-/tmp}/cbm-mcpb-contract.XXXXXX")"
trap 'rm -rf "$FIX"' EXIT
BUILD_DIR="$FIX/build"
mkdir -p "$BUILD_DIR"
cat >"$FIX/stub.c" <<'EOF'
#include <stdio.h>
static const char keep[] = "codebase-memory-mcp OMIT_LOAD_EXTENSION";
int main(void) { puts(keep); return 0; }
EOF
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" 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 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
}
echo "--- packaging the four target families from one stub build tree"
package darwin arm64 darwin v0.0.0-contract >/dev/null
package windows amd64 windows v0.0.0-contract >/dev/null
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
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 = []
def fail(message):
failures.append(message)
def check_bundle(path, *, binary, platform, version):
if not path.is_file():
fail(f"missing bundle: {path.name} in {path.parent.name}/")
return
with zipfile.ZipFile(path) as bundle:
names = sorted(bundle.namelist())
expected = sorted(["manifest.json", binary, "server/LICENSE",
"server/THIRD_PARTY_NOTICES.md"])
if names != expected:
fail(f"{path.name}: member set {names} != {expected}")
return
info = bundle.getinfo(binary)
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 {}
if server.get("type") != "binary":
fail(f"{path.name}: server.type must be 'binary'")
if server.get("entry_point") != binary:
fail(f"{path.name}: entry_point {server.get('entry_point')!r} != {binary!r}")
command = (server.get("mcp_config") or {}).get("command")
if command != "${__dirname}/" + binary:
fail(f"{path.name}: mcp_config.command {command!r} does not target the bundled binary")
platforms = (manifest.get("compatibility") or {}).get("platforms")
if platforms != [platform]:
fail(f"{path.name}: compatibility.platforms {platforms!r} != {[platform]!r}")
check_bundle(fix / "darwin" / "codebase-memory-mcp-darwin-arm64.mcpb",
binary="server/codebase-memory-mcp", platform="darwin",
version="0.0.0-contract")
check_bundle(fix / "windows" / "codebase-memory-mcp-windows-amd64.mcpb",
binary="server/codebase-memory-mcp.exe", platform="win32",
version="0.0.0-contract")
check_bundle(fix / "portable" / "codebase-memory-mcp-linux-amd64-portable.mcpb",
binary="server/codebase-memory-mcp", platform="linux",
version="0.0.0-contract")
# Without VERSION the manifest must say so loudly, not invent a release.
check_bundle(fix / "unversioned" / "codebase-memory-mcp-darwin-arm64.mcpb",
binary="server/codebase-memory-mcp", platform="darwin",
version="0.0.0-dev")
# Eligibility is a fence, not a default: the glibc-dynamic linux build gets
# an archive but NO bundle.
plain = fix / "plainlinux"
if not (plain / "codebase-memory-mcp-linux-amd64.tar.gz").is_file():
fail("plain linux target must still produce its tar.gz")
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 (selected bytes hash-bound and immutable, "
"archive/MCPB members byte-identical, mutation tools unused, no-clobber, "
"darwin/windows/static-linux eligibility exact)")
PY