fix/script-exec-bits
260 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5a479facf7 |
fix(release): invoke the new CI scripts via bash, and pin the exec-bit class
Completes the previous commit, which carried only the two mode changes because
the call-site edits were not staged when it landed (--amend is denied in this
repo, so this is additive rather than a rewrite).
Call sites now go through `bash`, which is what most of this repo already does and
which cannot break if a mode bit is lost to a patch application or a non-POSIX
checkout:
scripts/package-release.sh -> bash scripts/ci/check-binary-composition.sh
.github/workflows/release.yml -> bash scripts/ci/append-vt-notes.sh
With the 100755 modes from the previous commit, both sides are now correct, and
either alone would have been sufficient.
tests/test_script_exec_bit_contract.sh pins the class so it cannot recur: 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.
The committed mode is the thing that matters and the thing no local run can
check -- the working copy having the bit is exactly why this shipped.
Verified in both directions: passes on this tree, and fails on the exact defect
when the mode and the call site are reverted together. It joins backslash
continuations before analysing, because its own first draft reported a false
positive on
... && bash \
test-infrastructure/vm/vm-run-tests.sh --soak
where the interpreter sits on the preceding line. A contract that cries wolf
teaches people to ignore contracts, so that had to be right before it could be
useful.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
||
|
|
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>
|
||
|
|
4836b9a146 |
security: record the mimalloc local patch in the vendored manifest
The release build blocked at Layer 8 (vendored dependency integrity):
MISMATCH: vendored/mimalloc/src/options.c
expected: 96ef01e4...
actual: 192ce06a...
That is the integrity check working exactly as designed. The hardening pass
patched mimalloc's version banner to stop baking __DATE__/__TIME__ into every
binary, which is an intentional and reviewed change to vendored code -- but an
intentional change is indistinguishable from a supply-chain edit until someone
records it, which is the entire point of the manifest. Updating the recorded
checksum is the review being written down.
Scope verified before committing: the manifest diff is exactly ONE line, the
options.c hash, and it matches the hash CI computed. No other vendored file
moved. scripts/security-vendored.sh's other layers still pass on the updated
tree -- no subprocess, network or dangerous calls in vendored code, dlopen
confined to sqlite3.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
||
|
|
1f674c805e |
harden: remove capability that should never have shipped
Microsoft's ML flagged the rc.1 release binaries. The decisive evidence is that the SAME sha256 went from 0/62 clean to Microsoft-detected in about an hour with no byte change, so the verdict lives partly in scanner-side state and no code change can promise a clean result. What code CAN do is stop shipping things that have no business in a release artifact, which is worth doing on its own merits and incidentally widens the classifier margin. Every claim below is verified against a built binary by the new gate, not by reading source. Executable stack (the worst of the findings). vendored/nomic/code_vectors_blob.S is the only assembly in the build and carried no .note.GNU-stack. An unannotated object makes ld assume the worst for the whole link, so EVERY Linux release we have ever shipped had GNU_STACK RWE. Adds the note (cause) plus ELF-only -Wl,-z,noexecstack (outcome); the gate fails the release if it returns. Test seams are now opt-in, never opt-out. TEST_SEAMS=1 defines CBM_ENABLE_TEST_SEAMS; without it the crash-orphan probe -- which forks a child that ignores SIGTERM and loops forever, then writes its pid to a caller-supplied path -- and the lease-ownership marker compile to trivial stubs, so call sites are untouched and the binary holds no fork, no signal handler and no env-var string. Opt-IN is the point: forgetting the flag yields a clean binary rather than a leaky one. scripts/test.sh requests it in the leg that consumes it, and tests/test_worker_watchdog.sh now asserts the capability up front instead of dying later with an opaque "Killed: 9". The daemon's background version check is gone. It spawned curl against api.github.com/repos/.../releases/latest on the first eligible session of every run to say "a newer version exists" -- a release URL and an outbound request in every shipped binary, for something the install scripts already report. The INJECTABLE SEAM survives: update_ops is still honoured, the fakes in tests/test_daemon_application.c still cover notice/ownership/cancellation/replay, and with no provider application_update_subscribe_locked returns early so no generation ever starts. "No network request by default" is now structural. Dead capability out of release builds. The tar.gz/zip extraction block (gzip_decompress through cbm_extract_binary_from_zip, plus its cli.h declarations) moves under CBM_CLI_ENABLE_TEST_API -- verified self-contained, zero uses of any helper outside it, only callers the excluded updater and tests/test_cli.c. Downloading an archive, decompressing it, picking an executable out of it and marking it executable is the canonical dropper composite; it is now absent rather than merely unreachable. SQLite is built with -DSQLITE_OMIT_LOAD_EXTENSION (no caller of load_extension anywhere in src/ or internal/), removing that API surface and part of the dlopen/dlsym surface. Temp files and environment scanning (S2/S3). Predictable paths in mcp.c, artifact.c and diagnostics.c are created privately and exclusively and written through the returned descriptor; pass_envscan.c no longer descends symlinked directories out of the project root, and its fixed 512-byte path buffers no longer truncate into pointer arithmetic that could land outside the buffer. Build-time entropy. mimalloc's version banner baked __DATE__/__TIME__ into every binary, so two builds of identical source seconds apart could never share a hash and no release could inherit a false-positive determination made about its predecessor. Local patch removes it (marked to survive refreshes), -Wdate-time makes any future use a build error, and -Wl,--no-insert-timestamp stops the PE header carrying the link clock. scripts/ci/check-binary-composition.sh is the proof that each removal stays removed, wired into package-release.sh after strip so the local artifact-flow smoke enforces exactly what CI does. It asserts absences plus a CANARY string, so handing it a compressed, stubbed or empty file fails instead of passing vacuously, and a missing tool is a hard error -- a skipped assertion must never look like a satisfied one. Two build-system traps found by that gate, both of which had silently defeated a fix: the product binary is compiled in one shot from sources, so a flag flip did not rebuild it (now tracked by a .build-config stamp that also removes the binary, making it independent of mtime granularity); and prod_sqlite3.o / prod_mimalloc.o depended on a single named source, so SQLITE_OMIT_LOAD_EXTENSION and the mimalloc patch BOTH compiled to nothing on the first incremental build. Source review would have called them done. Deliberately NOT changed. Three seams stay in release artifacts because scripts/smoke-test.sh runs against the real artifact and needs them: CBM_TEST_CRASH_ON and CBM_TEST_HANG_ON inject the faults that prove supervisor recovery, and CBM_TEST_WINDOWS_USER_PATH_RUN_ID is what keeps the PATH smoke from writing the tester's actual PATH. The gate treats those as an allowlist, so a NOVEL seam still fails. The true no-UI standard build is deferred rather than rushed: src/ui/* is in PROD_SRCS and four files outside src/ui reference UI symbols, including the daemon that serves the UI, so that assertion reports instead of failing until the split lands -- a gate everyone knows is red teaches people to ignore gates. No grammar is removed. ObjectScript accounts for essentially all binary growth since the last provably-clean release (+21.4MB rodata, +1.1MB text from two four-line shims), which made it the obvious ablation candidate, but a dry run performed twelve real Defender endpoint scans across standard/UI and amd64/arm64 with ObjectScript, the daemon and the expanded hooks all present and every scan was clean. Nothing there is a deterministic trigger, so cutting a community-contributed language would spend a real feature on unproven margin. Lean is not a candidate either: at 99.6MB of source it is by far the largest grammar, but it shipped in v0.9.0 which scanned 20/20 clean, so removing it would produce a novel unscanned profile instead of restoring a known-good one. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
ae52db3147 |
test: make three Windows nondeterminisms deterministic
All three cost real release cycles, and none is fixed by widening a budget. daemon_application_cancels_physical_job_only_after_final_session waited for the SUBSCRIBER COUNT to reach 2, then cancelled both sessions and asserted the physical job had started exactly once. The job starts asynchronously after subscription, so both cancels could land first and leave starts == 0 -- arguably the correct outcome. It now waits for the state the assertions actually require. Verified 47/47 on Windows, the only platform it ever failed. The parallel harness refused outright when the suite leader had already exited, because taskkill /T cannot walk a tree from a dead PID. But the leader can exit between the timeout decision and that call, so the harness itself lost a race: a natural exit at the wrong moment failed the whole wave. It now proves cleanup the only way still available -- nothing parented to that PID -- and its contract asserts the PROPERTY rather than the phrase "tree cleanup" it used to grep for. That string pin is what broke when the guard was reworded while behaving correctly; the contract now checks rc==2 AND that the descendant really did survive, which would also catch a guard that claims to fail closed while leaking. extract_wide_flat_file_is_linear took ONE sample per size, so the ratio carried the noise of both. On a loaded Windows VM linear code measured 51x against a 40x bound (184ms -> 9387ms). Best-of-N instead: timing noise only ever adds time, so the minimum is the cheapest good estimate of the noise-free cost. The bound is deliberately unchanged -- it sits where linear (~20x) and quadratic (~128x) are each >=2x away, so raising it would move the test toward the very signal it exists to catch. Now measures 19.1x on Windows, 21.4x on macOS. Also: the smoke's `cli` helper redirected stderr to a file and discarded it, so any of the 10 bare `VAR=$(cli ...)` assignments could kill the run under `set -euo pipefail` printing NOTHING. One such abort cost a full Windows cycle just to locate and still could not be attributed. It now surfaces the command and its stderr. Neutral wording on purpose: one call site expects a non-zero exit and must not read as a failure. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
afd67c51b0 |
refactor(cli): install scripts own updating; the binary carries no URL
Completes the move started on Windows. `update` prints the install script's
command and exits 0 on every platform; the download/extract/chmod/exec sequence
is compiled out of release builds entirely, and the MCP background thread that
ran cbm_popen("curl ... api.github.com ...") on first tool call is gone.
The installers now place themselves beside the binary, which closes the last
gap: `update` referenced a raw.githubusercontent URL purely because an
install.sh-installed user had no local copy to point at. Two details make that
safe.
The source is the install script from the archive we JUST checksum-verified,
never "$0" -- which does not exist under `curl | bash`, and would pin the user
to the OLD installer forever. Since the script ships inside the verified
archive, it inherits that verification instead of needing its own.
And it is published by atomic rename, never written over the live path. Bash
reads a script incrementally by byte offset, so overwriting the file it is
executing continues reading NEW bytes at the OLD offset: silent, bizarre
corruption. cp to a temp name then mv -f swaps the directory entry while the
running shell keeps its original inode. Windows follows the same rule via
Copy-Item + Move-Item even though PowerShell parses up front.
Deliberately NOT done: spawning the new installer to delete and replace its
predecessor. Fetch remote code -> drop to disk -> spawn -> self-delete is a
textbook stager, and self-deletion is among the most heavily weighted
heuristics there is. It also gains nothing -- `"$DLBIN" install` already runs
the NEW release's install logic, because the binary owns the install.
Net: zero install or download URLs remain in the shipped binary, and the
allow-list entry is retired with it. Both scripts scan CLEAN (0/61) with the
self-placement code in them.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
||
|
|
a28749fc4e |
fix(release): strip Mach-O with the tool that can actually do it
The previous change generalised `--strip-all` to every platform. Apple's strip does not accept that flag, so the macOS build failed outright -- loudly, which was the lucky outcome. The unlucky version was already in the tree before that: `strip -x`. It succeeds and leaves 4058 symbols, which is precisely the state VirusTotal flagged on darwin-arm64 while the fully stripped ELF legs came back clean. Measured on that artifact: llvm-strip --strip-all 373 symbols CLEAN strip (no flags) 378 symbols equivalent strip -x -S 4058 symbols the FLAGGED state strip -X / -u -r 4058 symbols likewise So the flags are chosen per format, and a candidate that cannot do the job is a hard error rather than a silent fallback to a weaker strip. Verified on a PATH without llvm-strip, exactly as the runner has it: 373 symbols, codesign --verify --strict passes, the binary runs. Packaged and scanned before merge: darwin-arm64 0/60 and ui-darwin-arm64 0/61, both CLEAN -- the platform that was flagged on the previous release run. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
0b31a5dcdf |
fix(security): declare the printed install-script URL on the allow-list
DCO / dco (push) Has been cancelled
The `update` handoff prints a curl one-liner when install.sh is not beside the binary, and the security audit blocked the release on it: BLOCKED: src/cli/cli.c: URL not on allow-list: https://raw.githubusercontent.com/DeusData/ Two things were wrong. The URL was not declared, and it was split across string literal continuations, so the audit extracted only the first fragment -- an allow-list entry for the real URL could never have matched it. Hoisted to a single CBM_INSTALL_SH_URL token so the declaration means what it says. It is the same URL as the documented one-liner install, and it is printed for the user to paste: the binary no longer downloads anything. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
9ecabe0a6f |
refactor(cli): move self-update out of the product on every platform
Windows already handed updating to install.ps1, because a running .exe cannot
replace its own image. The remaining platforms now do the same, for a different
and better reason.
An in-process updater is structurally a downloader: it fetches a remote archive,
verifies it, extracts it, marks the result executable and runs it. That composite
lived in every shipped artifact -- along with the release URLs it fetched -- to
serve a command most users run a handful of times. The install scripts already
do all of it, are idempotent (so re-running one IS the update), and run while cbm
is not running.
`update` now prints the exact command for the running platform and exits 0:
unix bash "<dir>/install.sh" (or the curl one-liner when not adjacent)
windows powershell -ExecutionPolicy Bypass -File "<dir>\install.ps1"
Both scripts ship inside their release archive, so the printed path resolves
next to the binary. Flags are still parsed and validated, so `update --dry-run
--ui` keeps rejecting typos rather than silently accepting them.
The updater itself is EXCLUDED from release builds rather than left to dead-code
elimination: build_update_url, download_verify_install, extract_and_install_binary,
checksum fetch, detect_os/detect_arch and the cli_download_* helpers now compile
only under CBM_CLI_ENABLE_TEST_API, which release builds do not define. The C
suite still covers the flow through the activation test seam. Verified against
the release binary: `releases/latest/download` is gone entirely.
Also removes the MCP background update check. It ran on the first tool call and
did:
cbm_popen("curl -sf --max-time 5 -H 'Accept: application/vnd.github+json' "
"'https://api.github.com/repos/.../releases/latest' 2>/dev/null")
An embedded shell command plus a startup network callout, in every session, to
tell users a newer version exists. It shelled out, depended on curl being
installed, and phoned home from every agent session. The install scripts report
versions; this did not need to be in the server.
Smoke follows the same collapse. Phases 6c and 14 asserted a Windows-specific
handoff and a POSIX in-process replacement; they now assert one platform-neutral
contract selected by UPDATE_SCRIPT, and 14a's byte-identity check compares the
driver against ITSELF before and after -- comparing against "$BINARY" passed on
Windows only because Windows has no signing step, while the POSIX fixture
ad-hoc re-signs its copy and so differed before `update` ever ran.
The Linux glibc guard is RELOCATED, not retired. The standard linux asset links
glibc 2.38+ and breaks Debian 11, RHEL 8 and Ubuntu 20.04, so the installer must
fetch the static "-portable" build. That constraint moved from the binary to
install.sh, and the smoke now guards it where the behaviour lives.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
||
|
|
c725d9fad0 |
fix(release): strip Mach-O fully, not just local symbols
The first strip landed as `strip -x` on Mach-O, out of caution that a full strip could leave an image dyld will not load. That caution was wrong for this binary, and it cost a release cycle. `-x` retains external symbols -- 4058 of them on darwin-arm64 -- so the macOS artifacts kept the very symbol table the ELF legs had just shed. Run 30414312364 then flagged exactly those two artifacts (darwin-arm64, ui-darwin-amd64) while every Linux and Windows binary came back clean, which read as the detection "moving to macOS" when it had simply stayed on the unstripped set. Measured on the flagged darwin-arm64 artifact: strip -x 4058 symbols VirusTotal 1 malicious / 61 --strip-all 373 symbols VirusTotal 0 malicious / 61 CLEAN The packaged binary still verifies (`codesign --verify --strict` passes, having been re-signed after stripping) and still runs. Symbol-table removal has now cleared the detection on three independent artifacts across two platforms: linux-amd64 twice, and darwin-arm64 here. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
af250e7d65 |
fix(release): strip shipped binaries
DCO / dco (push) Has been cancelled
Release binaries carried their full symbol table. Production compiles without -g, but the linker keeps .symtab regardless, so every archive shipped ~536 KB of internal function names that nothing needs: a bigger download and a free map of the internals, by accident rather than by decision. Nothing symbolizes at runtime -- mem_profile.c is not in the production build and never calls backtrace_symbols -- so no diagnostics are lost. It also had a concrete cost. Microsoft's ML scored the unstripped linux-amd64 binary Trojan:Script/Wacatac.B!ml (1 engine of 62) and blocked release run 30398064336 at the VirusTotal gate. That verdict is a decision-boundary artifact, not a property of the code, and the evidence is unambiguous: * the dry-run build two days earlier ( |
||
|
|
0a22fb14df |
fix(windows): absorb transient file locks when retiring a running image
DCO / dco (push) Has been cancelled
Release run 30385988475 lost three Windows smoke jobs to one phenomenon on two
different surfaces. A Windows file that was just written or just executed can
refuse deletion and rename for a moment while a scanner reads it or while the OS
finishes reaping a child. Both conditions clear on their own within moments.
It reads as nondeterminism because it is timing: windows-2025 passed both amd64
variants while windows-latest -- the SAME image, windows-2025-vs2026 -- failed
both, and windows-11-arm failed too.
PRODUCT: uninstall abandoned a live installation.
With one binary per platform, uninstall must remove the very image it is
executing, and on Windows a running image can only be renamed, never
overwritten. That rename got a single attempt, so a transient sharing violation
surfaced to the user as
error: failed to remove ...; completed configuration/index cleanup may remain
with the install left half-torn-down. install.ps1 already retries this exact
operation ten times for this exact reason; the C path now does the same, and
only for sharing/access/lock violations, so a file that is genuinely held still
fails closed instead of spinning.
Proven with a failure-injection seam rather than by racing a real scanner:
3 injected failures must still succeed, 64 must still fail AND leave the target
in place. Deterministic, so it does not need CI to learn whether it works.
HARNESS: the smoke killed itself silently.
Phases that install a binary then `rm -rf` the directory hit the same lock. Under
`set -euo pipefail` that returns non-zero and ends the run WITHOUT printing
anything -- the logs jump straight from "OK 13h" to job cleanup with no FAIL
line, which is what made this expensive to read.
None of those 16 removals is an assertion; they are fixture cleanup, and
_smoke.yml already states the principle ("a temp dir on an ephemeral runner must
never fail the job") that smoke-test.sh did not honour. They now go through
smoke_rmtree, which retries so the disk is actually reclaimed -- these fixtures
hold ~300 MB binaries and runners are disk-tight -- then warns and continues.
Verified: Windows VM full smoke ALL PASSED (previously died after 13h), macOS
full smoke ALL PASSED, activation_transaction + cli 259 passed on Windows, all
10 shell contracts pass, lint-ci clean.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
||
|
|
2740208f40 |
fix(cli): report a failed install --dry-run plan check in the exit status
The Windows smoke guards that an invalid PATH seam must fail closed rather than
silently falling back to the live HKCU\Environment\Path. That guard went green
in a way that meant nothing: the seam still refused, and install still printed
"PATH configuration failed", but the process exited 0.
Windows release builds used to compile a separate managed-install path. With one
binary per platform they compile the shared cbm_cmd_install, which since
|
||
|
|
a54ea95719 |
fix(windows): ship one binary — remove the launcher stub flagged as a dropper
DCO / dco (push) Has been cancelled
Windows shipped a PAIR: a small permanent launcher (codebase-memory-mcp.exe)
plus the real product binary (codebase-memory-mcp.payload.exe). The launcher
existed for exactly one reason — a running .exe cannot replace its own image
on Windows, so an in-process self-update needs a second resident binary to do
the swap.
That stub is statically indistinguishable from a dropper: a small, unsigned,
zero-prevalence PE whose whole job is verify-and-execute another binary.
Defender's ML scored it Trojan:Win32/Wacatac.B!ml and blocked the v0.9.1-rc.1
release at the VirusTotal gate. It is not fixable in our code on x64 —
bcrypt-free, stripped, VERSIONINFO'd, minimal-resource and even
resource-FREE builds on CI's own MSYS2 CLANG64 toolchain were all flagged,
while the product binary scans clean on every platform.
So remove the stub and move self-update OUT of the process into install.ps1,
which runs while cbm is NOT running: Windows' image lock only blocks a
process from replacing ITSELF. now prints the exact PowerShell
command (with the Unblock-File hint for Mark-of-the-Web); install.ps1 is
idempotent, so re-running it IS the update — it stops the daemon, renames the
running binary aside (the one mutation Windows permits on a running image),
publishes the new one, and sweeps retired copies.
Windows now matches Linux and macOS: ONE binary per platform.
* packaging, install.ps1, npm and PyPI wrappers all carry a single binary
* the launcher/payload ABI contract and ~2500 lines of stub state machinery
are deleted
* every daemon start, CLI call and hook fire loses a process spawn, a named
pipe handshake and an stdio relay
* test_windows_bundle_contract.sh is rewritten as an INVERTED contract: it
now asserts no shipped surface can reintroduce a launcher/payload pair,
and that install.ps1 retires the running binary before publishing
Verified: VirusTotal 0/67 on the packaged binary and 0/58 on install.ps1 (no
certificate involved); macOS and Linux full suites green; Windows guards all
green including the new update-handoff contract; npm 10/10; PyPI 3/3.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
||
|
|
c3b35691ab |
fix(soak): resolve the .exe whenever it exists, not only when -x fails
Both hosted-runner Windows soak legs failed at Phase 1/diagnostics with an empty frontend stderr while the daemon's own log showed a healthy diagnostics.start: the workflows pass the suffix-less binary path, msys resolves it transparently (so the old 'append .exe only when the plain name is not executable' branch never fired), and soak-test keys BOTH native-Windows gates — the cygpath'd CBM_CACHE_DIR and the coproc stdio branch — off the literal .exe suffix. A suffix-less native binary therefore received a POSIX-form cache path: daemon logs mis-rooted (diagnostics grep zero, arm64) and the frontend's cache path handling died silently (x64 SIGPIPE at first write). Reproduced on the VM's CLANG64 environment with a suffix-less invocation (same FAIL) and red->green proven: with the normalization the identical invocation runs the full quick soak to PASSED. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
657d4b0284 |
fix(soak): give the first diagnostics snapshot a 30s window
Both Windows runner soak legs died right after 'server running' while the daemon's own log shows a healthy diagnostics.start with valid paths — the wait polled 10s for a snapshot whose first WRITE lands one 5s interval after start and can exceed 10s on a cold 4-vCPU runner mid-initial-index (the VM's 18 cores never miss it; the x64 leg's SIGPIPE was the same failure path dying inside the pipeline). Budget doctrine: the wait sits above the worst case. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
0c0111eb33 |
fix(smoke): header-only fixture-server readiness polls
Hosted Windows runners reset the readiness poll's full-archive GET mid-body (WinError 10054 on the server, ~99 aborted transfers per job) while the identical stack passes on the VM; every windows smoke job died at 'fixture server did not serve' with a healthy server. A HEAD proves the artifact routes without transferring the body per poll; the download phases still own full-body transport and report phase-precise if that is ever broken. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
6d8b39466b |
fix(smoke): Phase 15 probes the UI a fresh profile can actually serve
Every ui-variant smoke job failed on every platform in dry run 30226483380 — and it turns out a ui-variant Phase 15 had never legitimately passed anywhere: the old probe (a) never passed --ui=true, but the HTTP UI is a persisted DEFAULT-OFF setting, so a fresh profile (every runner, every smoke HOME) can never serve; (b) fed stdin an empty file, but the UI does not pin the process — stdio EOF ends it cleanly (rc=0) before any poll sees it serve; (c) gave a 10s window to a server measured to bind ~6s after launch on a FAST host; (d) POSTed an MCP initialize at /rpc, which speaks the UI's own narrow query protocol, not MCP. The rebuilt probe mirrors the drive-listing guard's proven invocation (--ui=true --port=N equals-form, held-open stdin), waits 30s, and verifies the API surface with the stateless GET /api/ui-config. Validated red->green against the release-shaped artifact: the full darwin ui artifact lane now passes 15a/15b legitimately. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
1d822c86bf |
fix(soak): BSD-safe mktemp template for the per-leg log
BSD mktemp substitutes only TRAILING X's, so the template
cbm-soak-leg-XXXXXX.log creates a near-literal file on macOS and the
second leg of the same job collides with the first leg's leftover
('File exists'). First runs mask it — which is why the quick leg
passed and query-leak failed locally, and why CI's macos soak jobs
(BSD mktemp, two legs per job) would fail the same way.
Drop the suffix; the path is only a log handle.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
||
|
|
c1425d2e27 |
ci: write the shard manifest before any suite runs; warn on absent upload
The manifest content (leg, slice, list hash) is fully determined at slicing time, so write it up front: a red run's manifest is exactly as load-bearing for the cross-shard union proof as a green one's. With that, a missing manifest at upload time can only mean the job died before the harness started (contract step / build failure) — that failure is already the job's red, so the upload warns instead of stacking a second error on top (the recurring 'No files were found with the provided path: build/c/test-logs/shard-manifest.txt' annotation). The shard-completeness job still gates the union. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
ad713bce93 |
fix(mcp): guarantee valid UTF-8 in tree-format output
Dry run 30206811293: macos-15-intel release smoke failed B3 with a visibly correct 'semantic: 50' table — BSD grep returns no-match for the ENTIRE output when any line carries a NUL, control byte, or invalid UTF-8 (verified: printf poisoned input -> grep -qE fails), so one bad byte in one row silently unmatches every anchored check. Close the class at the emission choke point: control bytes and invalid UTF-8 force the quoted form; quoting escapes controls as \u00XX and replaces invalid sequences with U+FFFD (RFC 3629 validation, NUL-safe). Tool output is valid UTF-8 by construction. Unit test proven RED-on-revert (raw bytes emitted) and green with the fix. The smoke's B3 failure path now also dumps od -c bytes so a future red names the exact byte. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
a20a50df77 |
fix(ci): executable bits on canonical entry scripts
git-apply of the staged unification recorded the new canonical entries as 100644; workflows execute them directly (run: scripts/ci/...), so a fresh CI checkout would fail at ci-ok/shard-union/soak/package steps with permission denied — the same class the parity worktree's own preflight just tripped over locally. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
0a40fe8bea |
feat(ci): local-remote parity lanes — artifact-flow smoke, glibc floor, Defender-ON everywhere
Closes the closable local-vs-remote venue gaps so that, outside arch legs / YAML glue / release plumbing, every local red is a remote red and vice versa on shared legs: - scripts/package-release.sh: THE canonical archive step (names + five-file Windows bundle layout defined once); _build.yml's eight inline archive blocks become calls to it. - scripts/ci/smoke-artifact.sh: the artifact-flow smoke lane — build, package, extract, then the canonical wrapper in artifact mode; wired as run.sh smoke-artifact (compose service), win.sh smoke-artifact, and directly runnable on macOS. Archive-layout bugs now surface locally instead of in a release dry run. - glibc-floor leg (Dockerfile.glibc22 + compose + run.sh): portable binary smokes on ubuntu-22.04/glibc 2.35; the dynamic binary must refuse there (2.38+ floor by design). - Defender-ON parity (user directive): scripts/ci/ensure-defender.ps1 enables + VERIFIES real-time protection, fail-closed; runs in every Windows CI job (_test x2, _soak x3, _smoke, pr.yml) AND in the VM preflight; _smoke.yml's scan engine-failure soft-skip becomes a red gate. Expected cost: slower Windows jobs (AV scanning during install/build/test I/O); the next dry run proves the runner side. - Contracts: launcher-bundle five-file check retargeted onto package-release.sh + per-archive canonical-call association; venue-parity contract requires the new lanes, counts one ensure-defender step per Windows job, adds --help probes for the new entries. Extended contract fails on the pre-change tree (verified: 20 violations on HEAD). - VM README: ephemerality/Defender posture documented — utmctl has no snapshot verb, so per-run revert stays a manual qcow2 option; the sweep preflight remains the standing mechanism. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
3fc93d12da |
ci: unify smoke/soak/test venues onto canonical leg entries
The staged venue-unification bundle: _smoke.yml onto the shared wrappers with an extracted-artifact input (CBM_SMOKE_ARTIFACT_DIR), soak-legs.sh as the one canonical soak entry (quick + query-leak legs across all venues), protected per-user TEMP roots shared across venues, clean-disk preflights before every VM/Docker run, win.sh and run.sh routed through the canonical scripts, soak.yml retired, and the venue-parity contract (whitelist walker: workflows may provision or call canonical entries, nothing else). Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
026b5ed70e |
feat(memlab): idle gap to separate per-request from per-second growth
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
7e53592abc |
feat(memlab): select the repeated tool to isolate the leaking path
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
2c5861f2ed |
fix(memlab): hand native Windows paths to the win32 python driver
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
09f74fb1af |
fix(memlab): native cache path on Windows, keep server stderr
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
99680d3fc5 |
fix(memlab): surface driver output when no request succeeds
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
95c13dea37 |
fix(memlab): run from a stamped root on native Windows
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
6a483344c6 |
feat(diagnostics): deterministic memory-attribution driver
Fixed corpus, fixed request count, one process, no daemon handshake and no reindex or crash-recovery phases, so two runs differ only where the code does. Requests go through a request/response driver rather than a batched pipe: closing stdin makes the server treat it as a client disconnect and shut down before answering, which yields a census of a process that never worked. Linux gains a measurement-only --wrap shim so the profiler can observe allocations the way the Windows interposer already does; macOS has no --wrap and stays census-only, which the driver reports rather than passing off as zero. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
7a2376987e |
feat(diagnostics): allocation-site profiler for the #581 analysis harness
The census could say which POOL held memory but never which allocation put it there, so each hypothesis needed a code change plus an eight-minute soak to test and the investigation could only ask one yes/no question at a time. Records allocations at or above a threshold (default 32 KiB, since arena fragmentation is driven by large transient runs) against their captured stack, into fixed tables whose exhaustion is counted and reported rather than silently dropped. Emitted alongside every census sample, so pool totals and attribution always describe the same instant, plus an explicit unattributed remainder so incomplete coverage announces itself. scripts/memlab-report.py ranks sites by retained-byte growth using a linear fit and can diff two platforms' profiles against each other. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
0d46fffbaa |
fix(test-infra): bind the smoke fixture server without a reverse-DNS lookup
The macOS Intel leg failed the fixture contract on every run while every other platform passed. Removing the readiness fsync did not change it; the hardened diagnostics named the real cause on the first remote run: waited=30.0s, exit_status=alive, port_file=absent, staged_temp_files=none, empty startup log -- the process was healthy but had never reached publish_port. http.server.HTTPServer.server_bind() resolves socket.getfqdn(host). On a host whose resolver does not answer for the bind address that call blocks for the resolver timeout, so the constructor never returns and no port is ever published. The fixture now binds through a subclass that keeps the threading server but skips the FQDN resolution, which only feeds CGI-style variables this fixture never serves. Proven both directions locally by forcing socket.getfqdn to hang: the subclass publishes its port immediately, the stock server never does. That forcing hook is kept as a permanent contract guard, so the reverse-DNS dependency cannot return without turning the gate red on every platform rather than on one runner nobody can reproduce. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
636dd3f92c |
fix(test-infra): repair the native Windows soak transport and release checks
The native Windows soak could never start its server, so that gate had never actually measured anything. The root cause was the harness, not the daemon: an MSYS filesystem FIFO feeding a native Windows process delivers empty stdin (reproduced 5/5), while a Bash anonymous coprocess carries the full JSON-RPC initialize exchange to the same protected payload, and the failing daemon log showed an orderly client disconnect rather than a daemon-side eviction. The soak now uses a named coprocess on native Windows only, duplicating its endpoints to stable fd3/fd4 and closing the originals so closing fd3 still delivers EOF; POSIX keeps the FIFO path. The coproc syntax sits inside eval because macOS system Bash 3.2 must still parse this file even though only MSYS2 Bash 5 executes that branch. With transport repaired the next exact failure surfaced: native Windows Python cannot open an MSYS /c/... diagnostics path. Both consumers now pipe the file through stdin, the pattern already established elsewhere in the repo, and the soak recovery contract forbids reintroducing a direct native-Python open. Also on the release path: - The smoke fixture server no longer fsyncs before atomically publishing its port. This is ephemeral readiness signalling, not crash-durable state, and the macOS Intel runner failed inside that durability sync. When the contract does fail it now reports the observable state -- waited, exit status, port file, staged temp files, interpreter, startup log -- because the previous verdict named nothing on the one runner we cannot reproduce locally. - The POSIX publication test hook no longer compiles into Windows builds, fixing an exact -Werror unused-variable failure. The setter keeps a parameter-consuming Windows stub because it is public API. - The Wine leg assembles the real release layout (payload plus canonical launcher) and version-checks both, running the launcher through cmd so it has a Windows-visible parent. The unsupported Wine soak is removed: Wine stays a fast compile/package/version check, and native Windows is authoritative for daemon, locking, ACL and process-lifetime semantics. - run.sh soak-windows routes to the native Windows VM, which validates the payload, builds the protected per-user temp root, stamps ACLs, and refuses success without a completion summary. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
62821375c4 |
fix: harden daemon release paths and verification
DCO / dco (push) Has been cancelled
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
f7a8e373f4 |
fix(qa): make crash and tolerance gates fail loudly
The fuzz harness swallowed the target's exit status through '|| true', so a SIGSEGV counted as a pass; the status now propagates (a planted crash yields 139) and payloads derive from a logged, replayable seed, with a missing python3 failing the gate instead of generating zero mutations. The Windows guard runner classified unknown exit codes as skips; the contract is now explicit (0 green, 1 red, 2 precondition-skip, anything else a failure) and an all-skip run fails as verifying nothing. The smoke suite gains crash-class detection on the phase-9b tolerance paths and phase 11 kill handling (rc >= 128 or a missing jsonrpc banner fails), a free-port pick plus readiness poll for the UI phase, and a phase-15b failure that actually exits nonzero. The soak reader resynchronizes on late JSON-RPC responses by draining to the matching id, and the analysis fails when fewer than 60 percent of snapshot attempts produced rows — a vacuous analysis previously passed. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
f7231aa2d9 |
test: harden the parallel runner's stamp and completion accounting
The Windows build-dir DACL stamp resets inherited ACEs before granting, verifies itself out loud, and re-stamps at the serial-tail boundary — the arm image ships explicit ACEs that survived a grant-only stamp and turned the whole suite red. Stamp verification failures now fail the run instead of warning. A suite that exits 0 without printing its summary line is counted as a failure rather than silently passing. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
dc5649d1e6 |
test: re-stamp the Windows build-dir DACL at the serial-tail boundary
On one dry run the two arm shards split: the shard whose serial tail held the install-flow suites failed the source-directory policy minutes after its pre-wave stamp, while its sibling — same runner image, same stamp — passed with no install-flow suites to notice. Wave suites spawn Cygwin-family tooling that can rewrite the build directory's DACL behind the first stamp, so the stamp is now a function invoked both pre-wave and at the tail boundary, where the deadline-sensitive suites that depend on it actually run. Both invocations self-verify out loud. Verified on the VM from a deliberately reset (inherited) build directory through the real --par path: both stamps report clean, mini shard green. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
acce9a4582 |
test: make the Windows build-dir DACL stamp verify itself out loud
The dry run's windows-11-arm shard failed its install-flow tests with the source-directory refusal the stamp exists to prevent — while the identical commands, run from the identically dirty state on the ARM64 VM, stamp clean and pass. The difference is invisible because the stamp was fully silenced; a silent load-bearing step cost a full CI round to even learn whether it had run. The stamp now reports each icacls failure with the user and directory, re-inspects the DACL afterwards, and prints one line in either direction: stamped clean, or the surviving cross-account grants. Verified on the VM from a deliberately reset (inherited, Authenticated- Users-writable) build directory through the real --par path: stamp reports clean, suites green. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
9fb3bdaff1 |
ci,test: keep a spaced SANITIZE value one make argument end to end
DCO / dco (push) Has been cancelled
The windows-11-arm leg's first execution (broad matrix, release dry
run) died before any test ran: the trap-UBSan SANITIZE value passed
unquoted through the workflow ternary, so -fsanitize-trap=undefined
arrived as its own word and make consumed the leading -f as its
makefile flag ('No rule to make target sanitize-trap=undefined').
- The workflow quotes the ternary value so test.sh receives one
argument.
- test.sh forwards make arguments through an ARRAY instead of a
re-split string, so a VAR=VAL whose value contains spaces survives
every boundary (empty-array expansion kept bash-3.2-safe for the
macOS runners under set -u).
Verified: argument-parse proof (argc=1 with the four-flag value) and a
full macOS suite run through the changed plumbing, 6776 passed,
0 failed — identical totals.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
||
|
|
e4400b6f49 |
fix(smoke): retire fixture daemons before every install-fixture cleanup
DCO / dco (push) Has been cancelled
Phase 9b's fixture installs spawn ephemeral daemons whose mapped generation backings pin files in the fixture HOME; the cleanup rm then races the daemon's asynchronous drain and loses nondeterministically on Windows (Device or resource busy on a generations backing — the same class the final-phase daemon retirement already fixed for the smoke cache, two rounds green then one red on identical code). The retirement is now a shared helper with a cheap not-running probe, called before every fixture cleanup that follows an install and by the final phase; a daemon that will not retire stays a hard failure. The 9b fixtures also route LOCALAPPDATA under the fixture HOME: their installs staged launcher pairs into the real runner profile, outside the fixture's isolation and the harness's cleanup reach. Verified: full VM smoke green end to end (all 9b subphases, phase 17 through the shared helper). Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
04141465ee |
test: stamp the Windows build-directory DACL inside the parallel runner
The workflow pre-created build\c with a protected DACL so build products would inherit it, but the diagnostic note from the failing CI shard named the real actor: MSYS2's Cygwin layer writes POSIX-emulating DACLs — including a CREATOR OWNER (S-1-3-0) mutation grant — onto directories its tools touch, overwriting the pre-created shape during the build. The activation transaction's source-directory policy correctly refuses that grant, so the install-flow tests failed on CI while the VM (whose harness stamps AFTER building) stayed green. The stamp now runs inside run-tests-parallel.sh on Windows, after any builder has had its say and immediately before the suites — one code path for CI and the VM. Two idempotent steps, as before: protect the directory, then /reset the children onto the clean inherited set. Verified: full VM parallel run green (6669 passed, 0 failed) through the new path. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
568e613141 |
test-infra: overlap the local ladder, open the serial tail, keep container I/O off virtiofs
Local runs left most of the machine idle: the serial tail ran sixteen suites one at a time on otherwise-idle cores, the three platform legs were launched by hand (usually sequentially), and every container-leg object write crossed the virtiofs bind mount. - run-tests-parallel.sh tail scheduling in two phases: the FLEX suites (timing-shaped but free of the shared per-account daemon runtime namespace) run CBM_TAIL_JOBS-wide (default 2), then the EXCL group — daemon-family plus the suites that drive daemon one-shots or supervisor rendezvous — runs strictly sequentially on a machine exactly as quiet as the old fully-serial tail gave it. The wave was already fed longest-first by the shard dealing order, so the drain-out no longer ends on a heavy straggler. - ladder.sh: one maintained entry point for the full local push gate with the legs overlapped — lint, the Linux container suite, and the Windows VM suite in the background, the macOS suite in the foreground, one verdict per leg, logs kept per leg. A missing prerequisite fails its leg loudly instead of silently skipping. - win.sh test-par now runs through vm-run-tests.sh (--par mode): the full parallel harness under the CI-shaped protected temp root with complete output. It previously ran under the MSYS-shared /tmp and piped through `tail -25` — the same truncated-blindness class that hid 40 Windows failures from the `test` command. - docker-compose: build artifacts and the incremental fixture cache move to named volumes on the container VM's native filesystem. Object writes over the virtiofs bind mount are the container legs' largest avoidable I/O cost, and the fixture cache now survives across container runs. - test_mem(win): the first valid full-parallel VM run proved working-set trimming beats the re-touch mitigation (19 MB resident of a 256 MB double-touch at 18 parallel suites). The RSS probe now VirtualLocks a 64 MB span — locked pages are exempt from trimming, making the measurement pressure-immune — with bounded touch-and-sample retries when the lock is unavailable. Red-to-green under the same 18-job load. - cli: the portable install's staging error now appends the activation refusal note (predicate, SID, object) like the managed path already does — a bare "activation transaction I/O failed" on a CI-only failure is undiagnosable without it. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
d7ce789e8f |
ci: shard the C-suite legs, prove cross-shard coverage, warm compiler caches
PR iteration was bounded by single-job suite runs (~40-50 min critical path) and by every first-of-ref build starting cold. Same suites, same sanitizers, same gates — the schedule, the cache reuse, and a new runtime coverage proof change. - run-tests-parallel.sh accepts CBM_TEST_SHARD="i/N" and runs a deterministic slice of --list-suites; unset selects everything, so the path is inert outside CI. Known-heavy suites are dealt to shards in weight order first (naive modulo stacked the two slowest, store_arch and daemon_runtime, onto one job), the parallel wave and the serial tail are sliced separately so every shard keeps its own quiet tail, and the per-shard union guard proves each job ran exactly its slice. - shard-completeness job: per-shard guards cannot see a mis-plumbed CBM_TEST_SHARD (two jobs running the same slice passes every local check while a slice runs nowhere). Every leg now uploads a manifest (leg, i/N, sha of the full suite list, its slice) and one aggregation job re-proves per leg that the shards agreed on the list, indices are exactly 1..N, and the union of slices IS the list. Runs on unsharded topologies too, where each leg's single manifest must cover the list. - workflow topology (shard_suites input, off by default = byte-identical to the pre-shard matrix): ubuntu legs run 3 shards, Windows 2 (every extra Windows shard re-pays ~5 min of MSYS2 setup), macOS stays at 1 (not the critical path; mac runner concurrency ceilings are the tightest). The Windows test job also pre-creates build\c with a protected DACL so build products inherit it — workspace drive roots grant Authenticated Users Modify by inheritance, which the activation transaction's source-directory policy correctly refuses. - ccache: a final ref-less restore key lets a fresh PR start from the newest cache GitHub's scoping permits, and a nightly build-only cache-warm workflow keeps main-scoped caches at most a day stale — without it the fallback had no warm source, since nothing built in main's scope. The strictly-per-ref policy this replaces cost ~8-12 minutes on every first-of-ref build for no safety gain: CCACHE_COMPILERCHECK=content makes a stale or foreign cache able to miss but never to return wrong output. Sharded legs restore-all/ save-one (every shard builds the identical objects, so shard 1's cache carries the full set) — save volume stays flat and warm caches stop being evicted by per-shard duplicates. - suite timeouts: daemon_runtime joins the slow tier. It measures ~610s solo on arm64 under ASan (10-round loop, no hang), so the 900s default under a loaded 4-job CI runner was a slowness kill masquerading as a hang detector. Validated locally: 3-shard union over the real suite list is complete with no duplicates, one top-heavy suite per shard, and the summed 3-shard totals reproduce the unsharded run exactly (6776 passed, 0 failed, 4 skipped). Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
664058e86b |
test-infra(win): CI-shaped VM harness, managed-install smoke gate, calibrated suite tier
The 40 hidden Windows failures fixed in the previous commit reached CI unseen because the local Windows leg never validly ran the C suite: MSYS2's shared /tmp fails the secure-ancestry validation (refusals, not signal) and the ssh driver piped everything through `tail -40`. This closes those holes and hardens the gates that let an install failure scroll past. - vm-run-tests.sh (new, used by win.sh test / ubsan-test / trap-ubsan-test): gives the suites CI's exact protected per-user temp root (owner-stamped, protected current-SID DACL, mirroring the workflow's pwsh step), shapes the runner's build directory like a real user checkout (drive-root trees inherit an Authenticated-Users Modify ACE that profile-rooted checkouts do not have, which the activation source-directory policy correctly refuses; DACL re-rooted two-step — directory protect then child /reset, since inheritance flags are directory-only and a /T re-root leaves files with empty deny-all DACLs), streams the FULL output, and refuses to report success unless the runner printed its completion summary. - smoke: Phase 8 no longer swallows the install exit code. On Windows it now requires the managed install to SUCCEED and to publish an exact two-link canonical launcher — a staging refusal used to scroll past as tolerated noise while the downstream config assertions kept passing. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
6ab5e3fa79 |
fix(smoke): retire the account daemon before the harness ends
The account daemon legitimately outlives its last client for a moment while it drains, holding its log and config DB open. POSIX rm does not care, but on Windows an open file blocks deletion, so the caller's cleanup of the smoke cache raced the daemon's asynchronous shutdown and failed with "Device or resource busy" after every phase had passed — first visible on windows-latest once the agent-config phases stopped failing earlier, and reproduced identically on the local Windows VM. The smoke now ends with an explicit phase: stop the daemon through its own lifecycle command, wait (bounded) until status reports not-running, and give Windows one beat for the final handle close. A daemon that will not retire is now a smoke FAILURE in its own right instead of a cleanup accident. Verified red-to-green on the VM and green on macOS. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
34f9ad1b3d |
fix(daemon,win): CI-green fixes and a daemon-stability guard
Fixes surfaced while bringing the shared-coordination-daemon branch green across macOS, the Linux containers, and the real Windows-ARM64 VM, plus a new stability guard that caught one of them. - daemon(win): the CLI teardown transition latched a failed release as permanent, but a Windows participant-state release must briefly try-hold the shared startup/legacy gates and legitimately collides with a concurrent one-shot's teardown; the release is retriable by contract (it always retains the transition), so parallel one-shot commands no longer report "CLI coordination cleanup failed" despite succeeding. Found by the new stability guard's churn section. - cli/main(win): install/update/uninstall and the Augment hook-script removal derive the managed launcher's identity from its plain drive-form path (cli_windows_plain_utf8), not the \\?\ extended-length form, so agent-config ownership matches on Windows and uninstall no longer leaves the MCP entry or owned hook scripts behind. - security: the `daemon start --open` browser launch is now shell-free (ShellExecuteW on Windows, cbm_exec_no_shell elsewhere) instead of system(); the CORS origin check spells out its two literal loopback URLs so the static URL audit sees a complete value. Both clear the Layer-1 allow-list audit. - coordination: version-cohort lock retries now sleep a per-process jittered interval — fixed-period retries can phase-lock two participants so one starves. The activation-quiesce test's observation window now covers worst-case candidate staging (three tamper-defense hashes over a ~1 GB sanitizer binary exceed the old 30 s budget on container I/O). - smoke/soak: the agent-config smoke matches Windows config paths through their escaped-backslash quoted form; the soak parser skips the update-available banner before the JSON summary and gained a one-shot CLI admission-churn phase inside the RSS/FD leak window. The build-dir safety fixture creates a real symlink/junction (MSYS2 ln -s otherwise deep-copies) so its traversal-refusal contract is genuinely exercised, and the symlinked-agent-roots test demotes its planted links to an unprivileged owner when run as root so the refusal is really tested. - tests(win): a new daemon-stability guard exercises the parameter surface, hook fail-open with its rate-limited notice, start-twice and occupied-port handling, busy-stop refusal against a live MCP session, kill -9 crash recovery, and sequential/parallel one-shot churn. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
0baddf7887 |
daemon: permanent lifecycle, daemon-backed CLI/hooks, real-Windows hardening, long-path launcher transactions
Daemon lifecycle and Windows correctness, verified on a real Windows 11 ARM64 VM through the maintained test-infrastructure/vm drivers, plus the macOS and Linux arm64 suites and the container lint gate. Daemon lifecycle: - daemon start/stop/status subcommands. `daemon start` launches a PERMANENT daemon (spawn shape is byte-exact argv; survives idle periods and session ends) and reports an already-active daemon instead of failing. Permanence is honored at every stop latch: last-committed-client disconnect, host initial-client window, coordinator release, and application final-session close — a permanent daemon also keeps admitting new sessions after its last one closes. - daemon stop refuses while sessions are active and lists the blocking peers (pid/role) that must finish first; an idle daemon drains through the activation-shutdown machinery with the ACK ordered after connection interrupts. A second stop is idempotent. The wire ops are no-cohort first-frame requests with peer fingerprint authentication, so stop/status never conflict with an exact-build admission gate. - One-shot CLI commands now execute through the daemon (index workers keep their local supervised path). A cold CLI run that had to spawn a temporary daemon prints a hint that `daemon start` removes the per-command startup tax; a warm daemon is recycled silently. - Hooks are connect-only fail-open: with no daemon present the hook emits a visible, rate-limited notice (Claude-dialect systemMessage plus stderr for other dialects) and always exits 0 — augmentation is never allowed to block the caller's tool use. - Version skew: a newer-build client automatically drains an older-build permanent daemon (strict semantic-version triples only; dev builds never auto-drain) and the build-conflict message names `cbm daemon stop` as the manual escape hatch. Windows IPC/runtime (real-VM verified): - ipc(win): persistent pending overlapped ConnectNamedPipe. The accept path used to destroy its listening pipe instance on every 20 ms poll timeout; a client attaching in the teardown window was severed or left on an orphaned pipe object whose HELLO no server handle could ever read, absorbing the connect until the client's own timeout expired. The pending connect now survives poll timeouts and nothing is destroyed while a client could be attaching. - ipc(win): drain-before-close for final responses. Closing a named-pipe server handle can discard a just-sent response before the peer reads it (POSIX stream sockets never lose buffered data on close). A bounded cbm_daemon_ipc_connection_drain (read-until-peer-EOF; no-op on POSIX, immediate on interrupted connections) now precedes close in runtime_worker_finish and runtime_reject_inline, so hello-conflict, capacity and disconnect acknowledgements reliably reach the peer. - runtime: CLOSE_INTENT wire frame. A Windows named-pipe client has no transport half-close, so close_begin now announces departure with an explicit frame (ordered after APPLICATION_CANCEL, before the local interrupt); the server releases the client's admission on receipt instead of waiting for the handle to close. Admission-drop timing is now identical to POSIX shutdown() semantics on every platform. - runtime(win): client close cancellation. close_begin serializes with request publication under the send lock, best-effort sends the active token's APPLICATION_CANCEL frame, then interrupts local I/O; the server cancels MCP/subprocess work promptly. Contract tests accept both correct outcomes (interrupted transport or decoded CANCELLED). - runtime: activation acknowledgement ordering. The activation ACK is the requester's license to act on "snapshotted and draining", so every connection interrupt is now initiated before the ACK is sent; a session could previously get one more request serviced after the requester observed the ACK. - service(win): deadline-bounded private-file prepare. The conflict-log prepare retry loop (100 x Sleep(2), which rounds up to the ~16 ms timer granularity) burned ~1.6 s against permanently obstructed paths, stalling hello rejections past the client's timeout. The retry budget is now a 250 ms deadline; transient share collisions still retry. - subprocess(win): cmd.exe /C payload encoder quotes metacharacters correctly (root cause of the git-on-Windows failure cluster). - watcher: SHA-256 buffer sizing (CBM_SZ_64 -> CBM_SZ_128) and a native Windows stop/unwatch cancellation test with exact-image verification. - httpd: send_all writes in bounded 64 KiB slices. A single giant nonblocking send() on Windows is absorbed wholesale into AFD kernel buffering regardless of SO_SNDBUF, so send deadlines and interrupts could never engage against a slow peer (and the full payload was pinned in nonpaged pool). Slicing restores a deterministic backpressure point; a test hook pins SO_SNDBUF for the deadline and interrupt tests. - ui/http: shutdown lifecycle — interrupt checks, response-wide send deadline, explicit connection states, refusal to free a server while a listener-owned connection is active. Windows long-path support: - Central path-aware wide conversion (canonicalize via GetFullPathNameW and prepend the extended-length prefix for absolute paths >=240) at the compat chokepoints (cbm_fopen/compat_fs/mkstemp/mkdtemp), sqlite store opens, and the daemon build-fingerprint/log paths. Deep managed installs (a 64-hex generation directory routinely exceeds MAX_PATH) now index, stage and activate correctly. - activation transaction: its own file APIs and the component-walking ancestry validators now operate in the extended-length namespace; the launcher path is canonicalized (and prefixed when deep) once at entry so every downstream exact-string comparison stays form-consistent. - Executable self-resolution uses the wide APIs (GetModuleFileNameW, GetFileAttributesW) so non-ASCII install paths survive argv[0] resolution. Windows launcher install/uninstall transaction: - FileRenameInfoEx names are NUL-terminated in an over-allocated buffer. FileNameLength governs per the contract, but filter drivers read FileName as NUL-terminated and appended adjacent heap bytes to created names — a flaky, garbage-suffixed rename target. Both the CLI and the launcher rename helpers are fixed. - Uninstall retires state via rename-aside (.cbm -> .cbm-retired-v1-<tag>-<pid>) with the retired tag shortened to 16 hex chars so the bare rename target stays under the FileRenameInfoEx NT-conversion ceiling at guard depths; 64 bits still uniquely identify the generation. - When the running launcher's mapped generation backings pin .cbm against rename, the backings are relocated to activation-<pid>-N .retired tombstones beside the install (a mapped image may be renamed, never deleted; the launcher's liveness-guarded sweep reclaims stale tombstones). Every relocation is recorded, and a FAILED uninstall reverses the moves after restoring .cbm — via MoveFileExW with extended-length paths on both arguments, since the deep generation target is beyond the handle-based rename's bare-path reach — so a restored install keeps its generation backings and stays runnable. - After a committed uninstall the retired tree's backings are relocated out so the tree is shallow enough for the detached cleanup's rd, and the cleanup's working directory strips the extended-length prefix (CreateProcessW lpCurrentDirectory silently ignores prefixed paths). - Files created under Administrators-default-owner directories (CopyFileW destinations, CREATE_NEW tombstones, probe directories) are explicitly owner-stamped so the exact-owner validators hold on runner images; guard fixtures stamp hand-built trees the same way. Diagnostics, tests and infra: - diagnostics: discovery is now an always-delivered JSON control record (new cbm_log_control) that survives CBM_LOG_LEVEL suppression and paths containing spaces; placement honors $TMPDIR with /tmp fallback via a diagnostics-local helper; the soak parser reads the JSON record; documented in docs/CONFIGURATION.md. Red-first coverage for suppressed log levels, TMPDIR-with-spaces, and native Windows output-contract assertions. - tests(win): daemon_ipc/daemon_frontend fixtures now build endpoint parents with production-shaped ancestry (LocalAppData on Windows, via th_secure_runtime_parent_new) — the runtime ancestry validation correctly refuses temp roots whose ancestors grant mutation rights to Authenticated Users (C:/msys64/tmp, GitHub-runner work dirs) — and drive the documented startup-owner publication flow before reading generation-bound endpoint addresses. This turns the 26 Windows failures previously visible in CI's full-test job green without weakening any validation. - tests(win): the launcher guard covers the full permanent-launcher contract including failed-uninstall restore and immediate reinstall after uninstall; new daemon lifecycle and reworked hook-augment guards run the start/recycle/stop flow end to end. - tests: CBM_SKIP_PERF is now actually consumed by the test runner (it was set by CI but never read, so perf suites ran everywhere); four throughput/bench suites are classified as perf, the heavy store_arch suite moved to the slow-timeout tier, and two wall-clock-sensitive assertions were rewritten as invariant checks with coarse hang-detector backstops. - build/test infra: build-dir safety contract, UI dev-proxy security contract, soak daemon-recovery contract, path-safety helper, the Windows VM worktree-sync contract wired into scripts/test.sh, and vm/win.sh guards building its clean embedded-UI product in an isolated BUILD_DIR so it cannot clobber the incremental test build. provision-windows.sh now installs Node.js for the guards UI build. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |