Commit Graph

9 Commits

Author SHA1 Message Date
Martin Vogel 5f8273222c fix(ci): release notes report the shipped binary, not every candidate
Release notes now carry one row per product — the bytes actually published, with
their VirusTotal verdict — instead of a column per candidate.

Several candidates are scanned so the selector has an alternative when an opaque
classifier flags one, but a reader installing cbm cares about what they receive,
not about what we discarded. The rejected candidates' verdicts remain in the
published evidence TSVs for anyone auditing the selection, and they stay useful
to US as a development signal; in a changelog they are noise, and listing a
"microsoft-ml" verdict next to a binary nobody can download invites exactly the
wrong conclusion.

Also removes a claim that had become FALSE: the section asserted that every scan
had a minimum of 50 decisive engines. That floor was removed deliberately (it is
VirusTotal fleet availability, not a property of our binary) and the last release
observed 31. Published notes would have stated something untrue. The observed
range is still reported, now over the shipped binaries.

The disclosure of a tolerated Microsoft `!ml` is preserved and narrowed to the
binaries that ship. The contract test now pins both directions: a flagged binary
that SHIPS must be disclosed, and flagged candidates that were rejected must not
appear at all. Its fixture gained a target that draws the tolerated verdict on
all three candidates, so the disclosure branch is actually exercised — previously
every flagged candidate was rejected in favour of a clean sibling, leaving that
path untested.

README and SECURITY.md updated: they promised notes "link every candidate result".

All five release/VT contract tests pass.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-14 12:48:40 +02:00
Martin Vogel 7b4533b67d fix(ci): finish the three-candidate wiring and restore full-surface VT scanning
Two things, both found by the dry-run and by sweeping what it exposed.

1. The three-candidate change was incomplete. The dry-run failed at
   stage-release-candidates.py, which carries its OWN copy of the transform
   validation that the previous commit only fixed in the selector:

     stage-release-candidates: candidate linux-amd64/debug-stripped has
     invalid transform: 'strip-debug'

   Sweeping for that assumption found it in six places, not two:
   stage-release-candidates.py, select-release-candidates.py,
   verify-release-selection.py, append-vt-notes.sh and two contract-test
   fixtures — as hardcoded 16s, `len(TARGETS) * 2`, two-entry VARIANTS tuples
   and two-key truth tables. All are now derived from len(VARIANTS).

   Field prefixes needed care: the variant NAME keeps its hyphen because it is
   the on-disk directory, while the evidence columns use underscores, so
   `debug-stripped` reads `debug_stripped_sha256`. Every lookup now goes through
   an explicit FIELD_KEY map instead of interpolating the variant directly.

   The selection contract test now covers the truth table EXHAUSTIVELY: three
   variants x two tolerated classifications is exactly eight combinations, and
   there are exactly eight targets, so every case is exercised once.

2. Full-surface VirusTotal scanning is restored. This PR had moved scanning
   upstream to the candidates and deleted the post-package pass, which silently
   narrowed coverage from everything we ship to executables only. The 42 runtime
   files across the 14 containers — install.sh, install.ps1, LICENSE,
   THIRD_PARTY_NOTICES.md, the MCPB manifest.json and the unpacked UI assets —
   were still extracted, structurally verified and strings-audited, but no
   longer scanned at all. install.sh and install.ps1 are the highest-consequence
   non-executable bytes we publish; users pipe them straight into a shell.

   The verify job scans every extracted object again, under the same policy.
   Re-submitting the selected executables alongside them is close to free
   because VirusTotal is content-addressed and answers for identical bytes from
   its own record — the same property that made the analysis-id equality check
   untenable two commits ago.

   The gate-chain contract asserted the opposite ("duplicate post-package
   VirusTotal path remains"). That assertion is inverted: the pass is required,
   and it is not a duplicate, since it covers a strictly larger set. README and
   SECURITY.md updated from "archive containers are checksummed rather than
   redundantly rescanned" to state the full covered surface.

All five release/VT contract tests pass.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-14 00:21:31 +02:00
Martin Vogel 3904e59372 ci: select release binaries before smoke testing
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-13 19:16:19 +02:00
Martin Vogel a4336dc40a feat(release): ship one archive set and tolerate a single Microsoft !ml verdict
Completes the collapse to a single shipped composition and replaces the
zero-tolerance VirusTotal gate with a narrow, disclosed policy.

Packaging and installers
  - package-release.sh loses --variant; archives are codebase-memory-mcp-<os>-<arch>
    with exactly four members. install.sh/install.ps1 lose --ui/--standard.
  - The extractor drops CBMUIPK pack parsing and --archive-scope; its scan-set and
    association manifests (which the gate depends on) are unchanged otherwise.
  - npm/PyPI/Go wrappers: the runtime "set" is one file again. The Windows lock
    and race fixes from #1495/#1496 are kept; only multi-file set membership goes.
    This also fixes `pip install` on Windows, which rejected the fifth archive
    member against a hardcoded four-name allowlist.
  - The wrappers' post-download probe moves from --verify-runtime-assets (removed)
    to --version, which proves the same thing: the binary executes.

VirusTotal gate
  - Exactly ONE detection is tolerated, and only when the engine is Microsoft AND
    the label ends in `!ml`. Two or more engines, any non-`!ml` label, any other
    vendor, any suspicious verdict and every infrastructure error still block.
  - A tolerated object prints TOLERATED:, never OK:, and its counts are recorded
    in vt-results.tsv exactly as a blocked one would be.
  - append-vt-notes.sh mirrors the policy. It previously hard-failed on any
    malicious count, so loosening only the gate would have passed the scan and
    then died at note publication. The notes now DISCLOSE a tolerated detection
    and link to SECURITY.md rather than claiming "0 malicious" for everything.

Rationale for the tolerance is in the gate itself: the verdict is not a property
of our bytes. It inverts across architectures and link modes, moves between
sibling artifacts of one build, and lands in different variant buckets for the
same source. The same `!ml` family hits llama.cpp, GitHub's own `gh`, Microsoft's
own Go toolchain and Anthropic's Claude installer.

The zero-tolerance contract becomes test_vt_gate_policy_contract.sh, asserting
the full matrix: 1x Microsoft !ml passes and reports TOLERATED; a Microsoft
signature label, a non-Microsoft engine, two engines, a suspicious verdict and
every malformed-response case still block. Its tripwire is narrowed to the
reverted endpoint-verification mechanism rather than the words "false positive",
so it no longer fires on a deliberate in-gate policy branch.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-09 16:16:08 +02:00
Martin Vogel 23e4fb0b4e fix(ci): scan extracted UI release files only
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-09 02:39:09 +02:00
Martin Vogel 8018561cfe fix(release): externalize runtime assets and harden VT verification
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-08 17:35:05 +02:00
Martin Vogel 359485b6c2 fix(release): make the new CI scripts executable, and pin that they stay so
Every unix build leg of release run 30499236230 died during packaging:

  scripts/package-release.sh: line 190: scripts/ci/check-binary-composition.sh: Permission denied

The composition gate was committed 100644 while being invoked as a command. It
passed every local check because my WORKING COPY had the exec bit -- only the
committed mode was wrong, and nothing you can run locally reveals that.

scripts/ci/append-vt-notes.sh had the identical defect waiting in the verify
step, the last step of the release, so this would have failed a second time after
two hours of tests, build, smoke and soak.

Fixed on BOTH sides, because either alone suffices and the pair is mode-proof:
the two scripts are now 100755, and their call sites invoke them through `bash`,
which is what most of this repo already does and which cannot break if a mode bit
is ever lost to a patch application or a non-POSIX checkout.

tests/test_script_exec_bit_contract.sh pins the class: any tracked .sh whose
COMMITTED mode is non-executable must not appear as the first word of a command
in workflows, scripts, test-infrastructure or the Makefiles. Verified in both
directions -- it passes on this tree, and fails on the exact defect when the mode
and the call site are reverted. It also joins backslash continuations before
analysing, because its own first draft reported a false positive on

    ... && bash \
        test-infrastructure/vm/vm-run-tests.sh --soak

and a contract that cries wolf teaches people to ignore contracts.

Product code is untouched: the test phase of the failed run was 27/27 green on
this exact tree, and a file mode cannot change a test outcome.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-30 03:04:15 +02:00
Martin Vogel 3e343a3eb2 ci(release): revert the VT gate to zero tolerance
The pre-release ML false-positive tolerance (single-engine Microsoft "!ml"
verdicts downgradable with Defender endpoint evidence, #1340) is reverted by
owner decision: cbm does not ship binaries carrying a VirusTotal detection,
demonstrably false or not. A "trojan" badge on a release asset is a
reputation cost the project is not willing to price in, however good the
accompanying evidence.

The gate returns to its original form: any detection, by any engine, on any
artifact, on any version blocks the release. The endpoint verification tool
and the evidence side-channel are removed with it; the notes renderer keeps
its extracted-script form but only ever states a verified "0 detections".

False positives are resolved upstream instead: verify the bytes on a real
Defender endpoint, submit a Microsoft false-positive report for the exact
hashes, wait for the detection to clear, then RE-RUN the failed verify job --
which does not rebuild, so the cleared hashes are the shipped hashes.

tests/test_vt_gate_zero_tolerance_contract.sh pins the decision: clean
passes; 1 malicious and 1 suspicious each block across stable, -rc., -pre
and -alpha versions; plus a tripwire for the specific reverted evidence
mechanism returning. Loosening this gate again has to consciously delete
that contract.

The skip_tests dispatch input and the script-extracted notes step survive
the revert -- both are orthogonal to gating policy.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-29 21:18:58 +02:00
Martin Vogel 48fc942418 ci(release): double-verify ML antivirus false positives instead of re-rolling builds
Release run 30464288732 was blocked by the VirusTotal gate: three linux-amd64
binaries flagged 1/62 by Microsoft's Wacatac.B!ml -- fully stripped binaries
(0 symbols, verified on the exact artifacts), the state that scanned clean in
the two previous cycles. Meanwhile a real Defender endpoint (engine
1.1.26060.3008, signatures 1.455.410.0 updated the same day, RTP on) scans
the identical bytes clean. Four cycles of evidence now say the same thing:
this verdict is an unstable ML decision boundary, not a property of the code,
and no build-side lever moves it durably -- stripping, downloader removal and
metadata changes each "worked" only until a later build flipped it back.

So stop treating the flag as buildable-away and verify it honestly instead:

  check-virustotal.sh may downgrade BLOCKED to TOLERATED only when ALL hold:
    - pre-release version (-rc./-pre/-alpha/-beta); stable releases never
    - every failing file flagged by exactly ONE engine
    - that engine is Microsoft and the verdict ends in "!ml" (never a
      signature name)
    - hash-pinned Defender ENDPOINT evidence is attached to the draft release
      (defender-endpoint-verification.txt) proving Microsoft's shipping
      product, signature-updated at scan time, reports the exact bytes clean

  av-endpoint-verify.sh (new) produces that evidence: downloads the draft
  assets, scans them on the local Windows VM endpoint, refuses to attest if
  RTP is off or Defender itself detects, uploads the hash-pinned result.
  The gate prints the exact command when evidence is missing; re-running the
  failed verify job does not rebuild, so the bytes stay fixed.

  append-vt-notes.sh (new, extracted from inline YAML per venue-parity) then
  renders the release-notes table honestly: a tolerated file reads "1/62 ML
  false positive, endpoint-verified clean", never "0 detections".

tests/test_vt_gate_tolerance_contract.sh pins all nine decision directions
against a stubbed VT API and release store -- clean pass, stable-never,
missing/stale/DETECTED evidence, signature-named verdict, non-Microsoft
engine, multi-engine -- so the tolerance provably fails closed.

Also: release.yml gains skip_tests for re-releases of an already test-green
tree (build/smoke/soak/verify always run; lint failures still gate via
!cancelled() && !failure()).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-29 20:45:13 +02:00