Skip to content

Commit 12bf64c

Browse files
authored
Merge branch 'main' into feat/pkl-call-extraction
2 parents 2d03bf6 + e16c4af commit 12bf64c

34 files changed

Lines changed: 2325 additions & 721 deletions

‎.github/workflows/dry-run.yml‎

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
# Manual trigger: test everything before pushing a release.
22
# Each step can be skipped for faster iteration.
33
#
4-
# Pipeline: lint → test → build → smoke/soak (sequential chain)
5-
# Security: security-static + codeql-gate (independent island)
4+
# Pipeline: lint → test → build → smoke/soak → virustotal (sequential chain)
5+
# Security: security-static + codeql-gate (independent island)
66
#
77
# Security does NOT block lint/test/build/smoke/soak. All jobs must pass
88
# for the overall workflow to be green.
9+
#
10+
# VirusTotal runs LAST and scans the same artifacts the release scans, so a
11+
# detection surfaces here instead of mid-release. It is the one dry-run job
12+
# whose verdict does not depend on our code: the same unchanged bytes can come
13+
# back clean one day and flagged the next, which is precisely why finding out
14+
# during a dry run is worth the wait.
15+
#
16+
# Two independent skips, because they answer different questions:
17+
# skip_builds no artifacts at all — "do lint and tests pass?"
18+
# skip_virustotal build + smoke + soak, no scan — "do the archives pass CI?"
19+
# The second exists because the scan costs API quota and polls for up to two
20+
# hours, which is pure noise while iterating on a build or smoke failure.
921
name: Dry Run
1022

1123
on:
@@ -20,7 +32,11 @@ on:
2032
type: boolean
2133
default: false
2234
skip_builds:
23-
description: 'Skip build + smoke'
35+
description: 'Skip build + smoke (also skips VirusTotal — nothing to scan)'
36+
type: boolean
37+
default: false
38+
skip_virustotal:
39+
description: 'Skip VirusTotal scan (still builds + smokes the archives)'
2440
type: boolean
2541
default: false
2642
soak_level:
@@ -84,3 +100,55 @@ jobs:
84100
with:
85101
duration_minutes: 10
86102
run_asan: ${{ inputs.soak_level == 'full' }}
103+
104+
# ── VirusTotal (final step) ────────────────────────────────────
105+
# Mirrors the release workflow's scan so a dry run answers the question the
106+
# release will ask, before we tag anything. The only difference is where the
107+
# archives come from: the release pulls them off a published release, a dry
108+
# run has none, so they come from the build job's artifacts — the SAME files,
109+
# produced by the same canonical package-release.sh.
110+
#
111+
# Runs after smoke and soak deliberately. It is the slowest job (polling can
112+
# take up to two hours) and the one most likely to be waiting on someone
113+
# else's queue, so it must not delay the feedback that is actually about our
114+
# code.
115+
virustotal:
116+
if: ${{ inputs.skip_builds != true && inputs.skip_virustotal != true && !cancelled() && needs.build.result == 'success' && needs.smoke.result != 'failure' && needs.soak.result != 'failure' }}
117+
needs: [build, smoke, soak]
118+
runs-on: ubuntu-latest
119+
permissions:
120+
contents: read
121+
steps:
122+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
123+
with:
124+
persist-credentials: false
125+
126+
- name: Download build artifacts
127+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
128+
with:
129+
pattern: binaries-*
130+
merge-multiple: true
131+
path: assets
132+
133+
# Same extraction the release verify job performs, so the bytes handed to
134+
# VirusTotal here are the bytes a user would download and run.
135+
- name: Extract archive artifacts
136+
run: scripts/ci/extract-release-archives.sh assets binaries
137+
138+
- name: VirusTotal scan
139+
uses: crazy-max/ghaction-virustotal@936d8c5c00afe97d3d9a1af26d017cfdf26800a2 # v5.0.0
140+
id: virustotal
141+
with:
142+
vt_api_key: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
143+
files: binaries/*
144+
145+
# Same ZERO-tolerance gate as the release. A detection here is a real
146+
# finding even when it is a false positive: it is what the release gate
147+
# will do, so the dry run should say so now. Resolve it upstream (vendor
148+
# false-positive submission for the exact hashes, then re-run) — never by
149+
# loosening this gate.
150+
- name: Wait for VirusTotal results
151+
env:
152+
VT_API_KEY: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
153+
VT_ANALYSIS: ${{ steps.virustotal.outputs.analysis }}
154+
run: scripts/ci/check-virustotal.sh

‎Makefile.cbm‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ endif
7575
# CBM_BIND_TS_ALLOCATOR=1: bind the tree-sitter runtime to mimalloc (#424). Only
7676
# the prod build uses mimalloc (MI_OVERRIDE=1); the test build is CRT+ASan, where
7777
# binding would create an alloc/free mismatch, so the guard is prod-only.
78-
CFLAGS_PROD = $(CFLAGS_COMMON) -O2 -DCBM_BIND_TS_ALLOCATOR=1 $(TEST_SEAM_DEFINE) $(CFLAGS_EXTRA)
78+
CFLAGS_PROD = $(CFLAGS_COMMON) -O2 -DCBM_BIND_TS_ALLOCATOR=1 $(TEST_SEAM_DEFINE) \
79+
$(GLOBAL_OVERRIDE_DEFINE) $(CFLAGS_EXTRA)
7980
CXXFLAGS_PROD = $(CXXFLAGS_COMMON) -O2
8081

8182
# Test flags: debug + sanitizers (override SANITIZE= to disable on Windows)
@@ -386,9 +387,34 @@ UI_SRCS = \
386387
# derives it from the MI_OVERRIDE option); MI_OVERRIDE alone is not read by the
387388
# source — it is kept here only as this project's prod/test marker.
388389
MIMALLOC_SRC = vendored/mimalloc/src/static.c
390+
# Linux belongs on this list too (#1360). The "Unix relies on static-link-order
391+
# override" strategy described above never actually worked: mimalloc defines
392+
# malloc/free ONLY when MI_MALLOC_OVERRIDE is set (it gates alloc-override.c),
393+
# so with the define off there were no strong symbols for the link order to
394+
# prefer. Measured on the shipped v0.9.1-rc.1 artifacts — linux-arm64 glibc AND
395+
# musl-static both report 0/6 allocator-owned size classes, i.e. ordinary malloc
396+
# was going to libc and every purge/reclaim option set in cbm_mem_init applied
397+
# only to the bound sqlite/tree-sitter populations.
398+
#
399+
# The macOS objection does not transfer. That abort is specific to the two-level
400+
# namespace, where this binary's free becomes mi_free while system libraries keep
401+
# allocating from the system allocator, so a pointer crossing the boundary hits
402+
# "mi_free: invalid pointer". ELF is a flat namespace: interposition is
403+
# process-wide and all-or-nothing, which is the configuration mimalloc is
404+
# normally deployed in. macOS therefore stays off, deliberately and permanently.
405+
#
406+
# TWO defines, switched together in ONE place so they cannot drift:
407+
# MI_MALLOC_OVERRIDE reaches the mimalloc translation unit and turns the
408+
# override on; CBM_MEM_GLOBAL_OVERRIDE reaches OUR sources and tells the startup
409+
# audit what to expect. CBM_MEM_GLOBAL_OVERRIDE goes only into CFLAGS_PROD,
410+
# because MIMALLOC_CFLAGS_TEST compiles mimalloc with -DMI_OVERRIDE=0 and no
411+
# override define at all — a test binary has no global override BY CONSTRUCTION
412+
# and must never be told to expect one, or it warns on every run.
389413
MIMALLOC_OVERRIDE_DEFINE :=
390-
ifeq ($(IS_MINGW),yes)
414+
GLOBAL_OVERRIDE_DEFINE :=
415+
ifneq ($(filter yes,$(IS_MINGW) $(IS_LINUX)),)
391416
MIMALLOC_OVERRIDE_DEFINE := -DMI_MALLOC_OVERRIDE=1
417+
GLOBAL_OVERRIDE_DEFINE := -DCBM_MEM_GLOBAL_OVERRIDE=1
392418
endif
393419
MIMALLOC_CFLAGS = -std=c11 -O2 -w \
394420
-Ivendored/mimalloc/include \

‎internal/cbm/zstd_store.c‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
#include <stddef.h>
88
#include <stdint.h>
99

10-
int cbm_zstd_compress(const char *src, int srcLen, char *dst, int dstCap, int level) {
11-
size_t rc = ZSTD_compress(dst, (size_t)dstCap, src, (size_t)srcLen, level);
10+
int64_t cbm_zstd_compress(const char *src, size_t srcLen, char *dst, size_t dstCap, int level) {
11+
size_t rc = ZSTD_compress(dst, dstCap, src, srcLen, level);
1212
if (ZSTD_isError(rc)) {
1313
return 0;
1414
}
15-
return (int)rc;
15+
return (int64_t)rc;
1616
}
1717

1818
int64_t cbm_zstd_decompress(const char *src, size_t srcLen, char *dst, size_t dstCap) {
@@ -31,6 +31,6 @@ size_t cbm_zstd_frame_content_size(const char *src, size_t srcLen) {
3131
return (size_t)n;
3232
}
3333

34-
size_t cbm_zstd_compress_bound(int inputSize) {
35-
return ZSTD_compressBound((size_t)inputSize);
34+
size_t cbm_zstd_compress_bound(size_t inputSize) {
35+
return ZSTD_compressBound(inputSize);
3636
}

‎internal/cbm/zstd_store.h‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
#include <stdint.h>
66

77
// Zstd compression at specified level (1=fast .. 22=best).
8-
// Returns compressed size on success, 0 on error.
9-
int cbm_zstd_compress(const char *src, int srcLen, char *dst, int dstCap, int level);
8+
// srcLen/dstCap are size_t for the same reason decompression's are: a >2 GiB
9+
// database would otherwise wrap through int and hand the encoder a capacity that
10+
// disagrees with the real buffer. Returns compressed size on success, 0 on error.
11+
int64_t cbm_zstd_compress(const char *src, size_t srcLen, char *dst, size_t dstCap, int level);
1012

1113
// Zstd decompression. srcLen/dstCap are size_t so a >2 GiB destination capacity
1214
// is never truncated (a large DB artifact, or a crafted one, would otherwise
@@ -20,6 +22,6 @@ int64_t cbm_zstd_decompress(const char *src, size_t srcLen, char *dst, size_t ds
2022
size_t cbm_zstd_frame_content_size(const char *src, size_t srcLen);
2123

2224
// Maximum compressed size bound for given input size.
23-
size_t cbm_zstd_compress_bound(int inputSize);
25+
size_t cbm_zstd_compress_bound(size_t inputSize);
2426

2527
#endif // CBM_ZSTD_STORE_H
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env bash
2+
# Extract the release archives into a flat directory of scannable files.
3+
#
4+
# Turns the per-platform artifacts (codebase-memory-mcp-<goos>-<goarch>.tar.gz /
5+
# .zip, produced by the canonical package-release.sh) into one directory holding
6+
# the actual executables, each renamed to its platform so a scan report names
7+
# something a human can act on. The install scripts are copied in beside them:
8+
# they ship in the release too, and a compromised installer matters as much as a
9+
# compromised binary.
10+
#
11+
# Usage: extract-release-archives.sh <archive-dir> <output-dir>
12+
#
13+
# Lives in scripts/ rather than inline in the workflow because the venue-parity
14+
# contract requires it: a venue may provision, plumb artifacts, or call a
15+
# canonical leg script, and this is leg logic. It also means the dry run and the
16+
# release can extract identically instead of drifting apart in two YAML copies.
17+
set -euo pipefail
18+
19+
ARCHIVE_DIR="${1:?extract-release-archives: missing <archive-dir>}"
20+
OUT_DIR="${2:?extract-release-archives: missing <output-dir>}"
21+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
22+
23+
mkdir -p "$OUT_DIR"
24+
shopt -s nullglob
25+
26+
# Counts BINARIES recovered from archives, deliberately not "files in OUT_DIR".
27+
# The install scripts below are copied in unconditionally, so a whole-directory
28+
# count is never zero and would have reported success on an empty input — a
29+
# clean VirusTotal run over nothing, which is the precise false green this
30+
# script exists to prevent. Caught by running it against an empty directory.
31+
extracted=0
32+
33+
for archive in "$ARCHIVE_DIR"/*.tar.gz; do
34+
name="$(basename "$archive" .tar.gz)"
35+
tar -xzf "$archive" -C "$OUT_DIR" 2>/dev/null || true
36+
if [ -f "$OUT_DIR/codebase-memory-mcp" ]; then
37+
mv "$OUT_DIR/codebase-memory-mcp" "$OUT_DIR/${name}"
38+
extracted=$((extracted + 1))
39+
fi
40+
done
41+
42+
for archive in "$ARCHIVE_DIR"/*.zip; do
43+
name="$(basename "$archive" .zip)"
44+
unzip -o "$archive" -d "$OUT_DIR" >/dev/null 2>&1 || true
45+
if [ -f "$OUT_DIR/codebase-memory-mcp.exe" ]; then
46+
mv "$OUT_DIR/codebase-memory-mcp.exe" "$OUT_DIR/${name}.exe"
47+
extracted=$((extracted + 1))
48+
fi
49+
done
50+
51+
if [ "$extracted" -eq 0 ]; then
52+
echo "FAIL: no binaries extracted from '$ARCHIVE_DIR' — a scan here would have"
53+
echo " reported clean without examining a single byte"
54+
ls -la "$ARCHIVE_DIR" 2>/dev/null || true
55+
exit 1
56+
fi
57+
58+
# Shipped alongside the binaries, so scanned alongside them: a compromised
59+
# installer matters as much as a compromised executable.
60+
cp "$ROOT/install.sh" "$OUT_DIR/" 2>/dev/null || true
61+
cp "$ROOT/install.ps1" "$OUT_DIR/" 2>/dev/null || true
62+
63+
ls -la "$OUT_DIR"
64+
total="$(find "$OUT_DIR" -maxdepth 1 -type f | wc -l | tr -d ' ')"
65+
echo "extracted $extracted binarie(s); $total file(s) queued for scanning"

‎scripts/smoke-test.sh‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,58 @@ if ! echo "$OUTPUT" | grep -qE 'v?[0-9]+\.[0-9]+|dev'; then
187187
fi
188188
echo "OK"
189189

190+
echo ""
191+
echo "=== Phase 1b: allocator override matches this platform's contract ==="
192+
# Asserts the SHIPPED binary's actual allocator wiring, per platform:
193+
#
194+
# Windows, Linux -> ordinary malloc MUST reach mimalloc (all size classes
195+
# owned). If it does not, every purge/reclaim option in
196+
# cbm_mem_init is decoration and freed pages stay committed
197+
# — that is #581, which hid in production for months
198+
# precisely because nothing asserted it on a real artifact.
199+
# macOS -> it MUST NOT. Enabling the override there aborts on the
200+
# first pointer crossing the two-level-namespace boundary
201+
# ("mi_free: invalid pointer"), so "owned" here would mean
202+
# we shipped a binary that crashes on index.
203+
#
204+
# Both directions fail. A silent flip either way is a release blocker, which is
205+
# why this lives in smoke (real artifact, all platforms) and not only in a unit
206+
# test built from source.
207+
ALLOC_LOG=$("$BINARY" cli list_projects 2>&1 >/dev/null || true)
208+
case "$(uname -s)" in
209+
Darwin)
210+
if echo "$ALLOC_LOG" | grep -q 'mem.allocator.not_owned'; then
211+
echo "FAIL: macOS emitted the not_owned WARNING; expected the by-design"
212+
echo " bound-populations INFO line (see #1360)"
213+
exit 1
214+
fi
215+
if echo "$ALLOC_LOG" | grep -q 'mem.allocator.owned'; then
216+
echo "FAIL: macOS reports ordinary malloc as allocator-owned. The override"
217+
echo " must stay OFF here: under the two-level namespace it aborts"
218+
echo " with 'mi_free: invalid pointer' on the first crossing pointer."
219+
exit 1
220+
fi
221+
echo "OK: macOS serves ordinary malloc from the system allocator, no warning"
222+
;;
223+
MINGW*|MSYS*|CYGWIN*|Linux)
224+
if echo "$ALLOC_LOG" | grep -q 'mem.allocator.not_owned'; then
225+
echo "FAIL: ordinary malloc does NOT reach mimalloc on $(uname -s)."
226+
echo " Allocator tuning is inert and freed pages will stay committed (#581/#1360)."
227+
echo "$ALLOC_LOG" | grep 'mem.allocator' | head -2
228+
exit 1
229+
fi
230+
if echo "$ALLOC_LOG" | grep -q 'mem.allocator.bound_populations_only'; then
231+
echo "FAIL: $(uname -s) reports bound-populations-only; the global override"
232+
echo " is expected to be compiled in on this platform (#1360)."
233+
exit 1
234+
fi
235+
echo "OK: ordinary malloc reaches the allocator on $(uname -s)"
236+
;;
237+
*)
238+
echo "SKIP: no allocator contract defined for $(uname -s)"
239+
;;
240+
esac
241+
190242
if [ "$SMOKE_MODE" != "--agent-config-only" ]; then
191243
echo ""
192244
echo "=== Phase 2: index test project ==="
@@ -338,6 +390,64 @@ if [ "$TOTAL" -lt 1 ]; then
338390
echo "FAIL: search_graph for 'compute' returned 0 results"
339391
exit 1
340392
fi
393+
394+
echo ""
395+
echo "=== Phase 3z: no tool duplicates its payload into structuredContent (#1375) ==="
396+
# Asserts on the SHIPPED artifact what the unit suite asserts from source: a
397+
# non-JSON payload must travel ONCE. It used to appear twice — content[0].text
398+
# plus an identical structuredContent.text — costing 2.05x the bytes on a large
399+
# query_graph, i.e. half the 10 MiB transport budget and double the tokens billed
400+
# to every LLM caller.
401+
#
402+
# In smoke as well as the unit suite because this is a WIRE-FORMAT property: it
403+
# is what a real client actually receives from the real binary, and a from-source
404+
# test cannot prove the released artifact behaves the same way.
405+
DUP_TOOLS=0
406+
DUP_CHECKED=0
407+
for TOOL_ARGS in "search_graph --project $PROJECT --name-pattern compute" \
408+
"search_code --project $PROJECT --query compute" \
409+
"get_architecture --project $PROJECT" \
410+
"index_status --project $PROJECT"; do
411+
# shellcheck disable=SC2086
412+
ENVELOPE=$("$BINARY" cli $TOOL_ARGS --json 2>/dev/null || true)
413+
[ -z "$ENVELOPE" ] && continue
414+
VERDICT=$(printf '%s' "$ENVELOPE" | python3 -c '
415+
import json,sys
416+
try:
417+
d = json.loads(sys.stdin.read())
418+
except Exception:
419+
print("skip"); raise SystemExit
420+
if d.get("isError"):
421+
print("skip"); raise SystemExit
422+
content = d.get("content") or []
423+
text = content[0].get("text", "") if content else ""
424+
sc = d.get("structuredContent")
425+
if not isinstance(sc, dict):
426+
print("no-structured"); raise SystemExit
427+
try:
428+
payload_is_object = isinstance(json.loads(text), dict)
429+
except Exception:
430+
payload_is_object = False
431+
if payload_is_object:
432+
print("skip"); raise SystemExit
433+
print("dup" if sc.get("text") == text and text else "ok")
434+
')
435+
case "$VERDICT" in
436+
dup) echo "FAIL: $(echo "$TOOL_ARGS" | cut -d" " -f1) repeats its payload in structuredContent (#1375)"; DUP_TOOLS=$((DUP_TOOLS+1)) ;;
437+
ok) DUP_CHECKED=$((DUP_CHECKED+1)) ;;
438+
no-structured) echo "FAIL: $(echo "$TOOL_ARGS" | cut -d" " -f1) has no structuredContent object (outputSchema requires one)"; DUP_TOOLS=$((DUP_TOOLS+1)) ;;
439+
esac
440+
done
441+
if [ "$DUP_TOOLS" -ne 0 ]; then
442+
echo "FAIL: $DUP_TOOLS tool(s) duplicate their payload on the shipped binary"
443+
exit 1
444+
fi
445+
if [ "$DUP_CHECKED" -eq 0 ]; then
446+
echo "FAIL: no tool produced a non-JSON payload — this check proved nothing"
447+
exit 1
448+
fi
449+
echo "OK: $DUP_CHECKED tool(s) deliver their payload exactly once"
450+
341451
echo "OK: search_graph found $TOTAL result(s) for 'compute'"
342452

343453
# 3b: trace_path — verify compute has callers

‎scripts/test.sh‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ bash "$ROOT/tests/test_parallel_harness_contract.sh"
230230
echo "=== Step 0j: venue parity contract (one harness, every venue) ==="
231231
bash "$ROOT/tests/test_venue_parity_contract.sh"
232232

233+
echo "=== Step 0k: spawn console-window contract (#1427) ==="
234+
bash "$ROOT/tests/test_spawn_no_window_contract.sh"
235+
233236
# Verify compiler supports target arch
234237
verify_compiler "$CC"
235238

0 commit comments

Comments
 (0)