install.ps1 created its staging directory with New-Item, which inherits whatever
%TEMP% carries. It then ran the downloaded binary from that directory, and the
binary validates its own directory and refuses inherited cross-account mutation
grants. So the installer handed our own validator a directory the installer had
made wrong:
error: failed to stage install candidate: activation transaction I/O failed:
acl-grants-cross-account-mutation to S-1-5-21-...-1003
Five reporters, three different offending identities - a CodexSandboxUsers local
group, a synthesized SID recorded in .codex/cap_sid, and orphaned SIDs left by
uninstalled software - all naming an ACE the installer inherited rather than
anything cbm wrote. Still live on v0.10.4 (#1614). Closes#1529, #1614, #1571.
cbm's own C staging already creates its directory with a protected owner-only
DACL (win_mkdtemp_private_create). install.ps1 was the single path that skipped
it, which is exactly why redirecting TMP/TEMP to a fresh directory worked around
the failure - that workaround is now unnecessary.
Applied after creation rather than atomically on purpose: the CreateDirectory
overload taking a DirectorySecurity exists on Windows PowerShell 5.1 but not on
PowerShell 7, and Set-Acl works on both. The directory name is unpredictable and
nothing is written into it before the download, so the window is not usefully
attackable.
Best-effort by design: a filesystem that cannot carry a DACL must not fail the
install, and if the hardening does not take, the binary's own validation still
refuses - the honest outcome rather than a silent downgrade.
Pure ASCII, as PowerShell 5.1 decodes a BOM-less .ps1 as ANSI.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Externalizing the integration templates (#1492/#1493) and the UI bundle
(#1501/#1503) was done to reduce the Microsoft `Wacatac.B!ml` surface. It did
not work: across dry runs the flagged artifact count stayed at ~3 and the
detections merely moved between artifacts.
Dissection of run 31286803592 shows there is no structural cause to fix. The
verdicts split across every axis at once — linux-amd64 (dynamic) flagged while
linux-amd64-portable (static) is clean, but linux-arm64 (dynamic) clean while
linux-arm64-portable (static) is flagged. The two macOS binaries have identical
segment structure and split clean/flagged. Siblings from one build landed in
different variant buckets (.B vs .C). Entropy is low everywhere
(code_vectors.bin 4.166, grammar tables 3.464 bits/byte, against 7.5-8.0 for
packed payloads), so the packed-payload hypothesis is excluded too.
So the complexity bought nothing, and installation goes back to being
self-contained: one binary that carries its own UI and agent integration
templates, with no adjacent data file that has to resolve before `install`
works. Only the UI-capable composition ships from now on, under the historical
unsuffixed archive name.
Removed: src/ui/asset_pack.{c,h}, asset_pack_stub.c, asset_manifest_stub.c,
scripts/pack-ui-assets.mjs, src/cli/integration_assets.{c,h},
assets/cbm-integrations.json, scripts/gen-integrations-hash.sh, the
--verify-runtime-assets probe (nothing adjacent left to verify), and the
composition gates A6/A7 whose property is now deliberately inverted.
Restored: scripts/embed-frontend.sh, src/ui/embedded_{assets.h,stub.c}, the
compiled-in hook/adapter template bodies, and the embed/EMBED_OBJS build path.
Kept from the reverted commits, re-applied by hand where a wholesale file
restore would have dropped them:
- cbm_module_path_utf8() in both self-path sites. GetModuleFileNameA renders
through the ANSI code page and mangles non-ASCII install paths.
- the /__cbm/ui-readiness HMAC proof, secure_random and cbm_hmac_sha256, so
`daemon start --open` still waits for a genuine CBM listener.
- X-Content-Type-Options: nosniff on served assets.
- the MinGW noexecstack gate, -lbcrypt, and the cppcheck/zip CI fixes.
Archives are now codebase-memory-mcp-<os>-<arch>[-portable] with exactly four
members (binary, LICENSE, installer, THIRD_PARTY_NOTICES.md). That restores the
names every static package manifest already points at — aur, chocolatey,
homebrew, scoop, winget and glama were all broken by the -ui- rename.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The prior fix put cbm-integrations.json into the Windows archive, and install.ps1
promptly rejected it: its pre-extraction namespace guard permits only the exact
release entries in $WindowsArchiveNames and throws "archive contains an
unexpected root entry" on anything else. The asset is now a real release member,
so it belongs in that allow-list. Added in archive order (binary,
cbm-integrations.json, LICENSE, install.ps1, THIRD_PARTY_NOTICES.md), matching
package-release.sh and the windows-bundle contract's windows_archive_names.
The guard also REQUIRES each listed entry exactly once, so this both permits and
mandates the asset — an archive missing it now fails install.ps1 up front rather
than at first hook render. install.sh has no equivalent namespace allow-list (it
extracts the tarball directly), which is why the Unix smoke passed; no change
needed there.
install.ps1 stays pure ASCII (0 non-ASCII bytes); windows-bundle contract passes.
This was the Windows smoke advancing PAST the archive-staging fix (asset now
present) to the installer's own entry validation.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
pr-smoke failed on all three platforms: the binary's install/uninstall render
templates from the integration asset, and every place that stages the binary
without the asset made those operations fail closed with "integration assets
missing". The asset must travel with the binary in EVERY layout, not just the
release archive. Five staging paths were missing it:
- scripts/build.sh — stage cbm-integrations.json next to the freshly built
binary, so `build/c/codebase-memory-mcp install` works straight out of a
build tree (dev, and the base for the smoke fixture).
- scripts/smoke-local.sh — include the asset in the fixture tarball and its
required-sidecar check. Member set and ORDER now mirror package-release.sh
exactly (binary, cbm-integrations.json, LICENSE, install.sh, notices); the
fixture was smoking an archive layout we never actually ship.
- install.sh / install.ps1 — after installing the binary, copy the asset beside
it in the install dir. `install` already publishes a verified copy to
~/.cbm/assets/<version>/, but a later install/uninstall run from the install
dir resolves the asset NEXT TO THE BINARY first, so without the adjacent copy
that lookup misses and a re-install or uninstall fails on a machine that just
installed successfully. Best-effort atomic rename, same shape as the existing
install.sh/ps1 sidecar copy. install.ps1 stays pure ASCII.
- scripts/smoke-test.sh Phase 14 — this phase hand-stages the binary into a
fresh HOME without going through install, so nothing populates ~/.cbm/assets;
stage the asset next to each staged copy so the uninstall it drives resolves.
Verified: scripts/smoke-local.sh on the standard binary now runs clean through
all 16 phases — zero "integration assets missing" / hook_script_uninstall /
agent-cleanup-failed lines (was 5). windows-bundle, smoke-fixture, exec-bit and
no-embedded-scripts contracts pass; install.ps1 has zero non-ASCII bytes.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
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>
install.ps1 did not run. It failed to PARSE, dying with a cascade of syntax
errors before its first statement, and it has been in that state on main since
61a432c.
Windows PowerShell 5.1 decodes a BOM-less .ps1 as ANSI, not UTF-8. The three
em-dashes in this file are 3 UTF-8 bytes each, and cp1252 renders them as a
sequence ENDING IN A DOUBLE QUOTE. Two sat in comments and were harmless; the
third sat inside a string literal:
"error: could not retire the existing $BinName - close all running"
where the injected quote closed the string early and left the rest of the line
as garbage expression, taking the enclosing if/foreach blocks with it.
This shipped broken because nothing executed the script. The smoke covers it in
Phase 13, but every Windows smoke run since the launcher removal aborted at an
earlier phase, and the VirusTotal check only ever scanned the bytes. Since
install.ps1 is now the sole Windows install AND update path, a parse error there
is a total loss of both.
Fixed as ASCII rather than by adding a BOM: the documented install pipes the
file straight into the parser (irm ... | iex), where a BOM would become part of
the string and break that path instead.
The bundle contract now rejects any non-ASCII codepoint in install.ps1 and names
it, so this cannot return silently.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
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>
install.ps1 kept detecting arch=amd64 on windows-11-arm because it runs under x64
emulation, where neither $env:PROCESSOR_ARCHITECTURE nor .NET OSArchitecture (on
Windows PowerShell 5.1) reports the real Arm64. Add a CBM_ARCH env override to
install.ps1 (wins over auto-detect; also a genuine escape hatch for emulated
invocations) and have smoke Phase 13 pass the authoritative DL_ARCH. Deterministic
-- no reliance on in-process detection under emulation. Same emulated-arch class
as #907 (uname) and #908.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
install.ps1 reported "arch: amd64" on windows-11-arm and 404'd on the amd64 zip:
$env:PROCESSOR_ARCHITECTURE reports the emulated "AMD64" for an x64 process under
ARM64 emulation, and PROCESSOR_ARCHITEW6432 is unset for 64-bit emulated procs.
RuntimeInformation.OSArchitecture reports the real OS arch (Arm64) regardless of
process emulation. Same emulated-arch class as the smoke uname fix (#907); this
one is a genuine product improvement -- real ARM users invoking install.ps1 from
an x64 context now get the arm64 binary.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Two release-blockers surfaced once #882 made every smoke leg a required gate
(continue-on-error previously masked them):
- smoke-unix ran the *dynamic* linux binary on ubuntu-22.04 / 22.04-arm, where
it cannot start (it links glibc 2.38+) -> Phase 1 died. Older-glibc coverage
is the -portable (static) binary's job and is green via the smoke-linux-portable
broad legs, so drop the impossible dynamic broad legs (keep macOS).
- the ClamAV scan died on every linux leg: apt auto-starts the clamav-freshclam
daemon which holds the freshclam log lock, so the manual freshclam failed with
"Failed to lock the log file ... Resource temporarily unavailable". Stop the
daemon first.
Also add native ARM64 Windows as a first-class release target -- the one platform
we test (windows-11-arm) but never shipped:
- build-windows-arm64 job (CLANGARM64) producing codebase-memory-mcp[-ui]-windows-arm64.zip
- smoke-windows is now arch-aware: windows-11-arm smokes the NATIVE arm64 binary
instead of the x86_64 binary under emulation
- a native soak-quick-windows-arm64 leg (no sanitizer -- ASan is unavailable on
native ARM64 Windows)
- arm64 detection in install.ps1
release.yml already publishes windows-arm64 (asset filter + merge-multiple) and
build_update_url/detect_arch already resolve it; npm/pip/install.sh compute arm64
already. scoop/winget/chocolatey per-version manifests are updated at release time.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Distilled from PR #702 (fixes#697):
- install.ps1: replace the try/catch that swallowed `codebase-memory-mcp
install` failures with a $LASTEXITCODE check that reports the exit
code and fails the installer, instead of silently leaving no coding
agent configured.
- README: document Unblock-File for the Mark-of-the-Web restriction
that blocks the downloaded install.ps1, plus the execution-policy
escape hatch.
- cbm_exec_no_shell (Windows): switch from _spawnvp, whose CRT does not
quote arguments containing spaces (the taskkill filter "IMAGENAME eq
codebase-memory-mcp.exe" arrived as three bare tokens), to
CreateProcessW over a two-pass MSVC-convention quoted command line
(cbm_build_cmdline, exposed for tests via compat_fs_internal.h).
- UTF-8-correct widening: the quoted command line is assembled in UTF-8
bytes and converted once via cbm_utf8_to_wide, so non-ASCII arguments
(e.g. a non-ASCII %USERPROFILE%) survive intact instead of being
byte-widened as Latin-1 mojibake.
- tests: regression guard for the #697 taskkill filter, MSVC quoting
edge cases (empty arg, embedded quote, trailing backslashes), UTF-8
widening guards (2-byte sequence with explicit code points; mixed
2-/3-byte round-trip via cbm_utf8_to_wide), and live CreateProcessW
spawn tests. All Windows-only, exercised by the Windows CI leg.
Co-authored-by: ShauryaaSharma <shauryasofficial27@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
install.ps1:
- Remove both Unblock-File calls (Defender ClickFix.R!ml signal)
- Remove ExecutionPolicy ByPass from usage comment (scanned by Defender)
- Add HTTPS-only URL scheme check on CBM_DOWNLOAD_URL
install.sh:
- Add HTTPS-only URL scheme check on CBM_DOWNLOAD_URL
pkg/go wrapper:
- Add validateURLScheme() to httpGet and fetchChecksums (defense-in-depth,
matching Python wrapper's _validate_url_scheme from PR #248)
All installers now allow localhost/127.0.0.1 for testing but reject
http://, ftp://, file:// and other schemes.
install.sh: one-liner for macOS/Linux — detects OS/arch (Rosetta-
aware), downloads release, verifies checksum, extracts, signs on
macOS, runs install -y for all 10 agents. Supports --ui flag and
CBM_DOWNLOAD_URL env var for testing.
install.ps1: one-liner for Windows — Invoke-WebRequest + Expand-
Archive + Unblock-File (strips MOTW), installs to %LOCALAPPDATA%,
adds to user PATH via [Environment]::SetEnvironmentVariable.
CI pre-signing: add codesign --sign - step for macOS builds in
both dry-run.yml and release.yml, before archiving. Release
binaries now ship pre-signed.
Phase 12 smoke tests: real HTTP download via local artifact server,
checksum verification, archive extraction, binary verification.
Runs only when SMOKE_DOWNLOAD_URL is set (CI provides it).
Phase 13 smoke tests: install.sh E2E — runs full script with local
URL + isolated HOME, verifies binary placed, signed, runs, and
agent configs created.
CI HTTP server: smoke jobs start python3 HTTP server serving the
built binary as a tar.gz/zip archive + checksums.txt. Enables
Phases 12-13 in CI on all platforms.
Update security allowlist: remove system() entry (eliminated),
add cbm_popen for pgrep. Update README with one-liner Quick Start.