OpenCode reads either opencode.json or opencode.jsonc. We always wrote the
.json name, so a user whose real config is opencode.jsonc got a SECOND config
file that OpenCode ignores: the MCP server never appeared, while the installer
reported success. Silent, and indistinguishable from "the tool is broken".
The install target is now whichever file already exists, .jsonc first since it
is the one we used to miss. With neither present, .json is created exactly as
before, so fresh installs are unchanged. Other clients in this file (pochi,
kilo) already resolve .jsonc paths — OpenCode was the gap, not the exception.
Reported by @iandol in discussion #1560, who noticed their config was .jsonc
while the installer named .json. Verified by revert: the test fails without
the fix and passes with it.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Codex sanitizes the environment of stdio MCP subprocesses and passes through
only the names listed in `env_vars`. Our generated block had `command` and
`args` and nothing else, so a user with a custom CBM_CACHE_DIR got a
Codex-spawned server reading the DEFAULT cache while their account daemon used
the configured one. The two disagree during initialization, the connection
closes, and Codex exposes no cbm tools at all — a total failure that looks like
a broken MCP server rather than a missing environment variable.
The name is listed unconditionally rather than only when the variable happens
to be set during install: `env_vars` names variables to forward IF PRESENT, so
listing it costs nothing when unset, and it keeps working for someone who sets
CBM_CACHE_DIR after installing — a case install-time detection would silently
miss, producing exactly this bug again with no way to tell.
Reported by @anton-matosov, who verified the corrected block end-to-end before
filing.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The installed SKILL.md carried an unquoted `description` whose value contains
"Triggers on: ". In YAML a colon-space inside an unquoted scalar is a
nested-mapping indicator, so strict readers — js-yaml's `load`, the frontmatter
parser in `npx skills` — reject the whole document. The skill does not load
wrongly; it does not load at all, and nothing in the install output says why.
Quoting the scalar fixes it without touching a word of the text.
The test checks the RULE rather than this string: for every skill, any
frontmatter value containing ": " must be quoted. Pinning the current wording
would let the next skill someone adds reintroduce exactly this bug. Verified by
reverting the fix — the test goes red, and green again with it restored.
Reported by @xezzon.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A reporter ran `uninstall`, then could not install again: every attempt said
"active CBM sessions and operations could not be stopped safely". A reboot
proved no such session existed, and the remedy line v0.10.2 added made it worse
— it told them to run `codebase-memory-mcp daemon status`, a binary they had
just uninstalled.
Both halves were wrong for their case:
The guard funneled two unrelated outcomes into one message. A cohort that is
BUSY means real sessions hold it and closing something is the fix. A reservation
that FAILED — lock I/O, leftover coordination state, permissions — means nothing
is running at all, and telling that reader to close sessions sends them after
processes that do not exist. Same misattribution class as #1416 and #1535: a
non-session failure wearing a session costume. They are now separate messages,
and the failure one says plainly that nothing needs to be closed.
The remedy also has to survive the situation it prints in. Install, and any
retry after uninstall, runs with no cbm on PATH; advice that starts with
`codebase-memory-mcp ...` is unusable exactly when it is needed. The busy
message now leads with the actionable step (close the editor/agent holding it)
and mentions the command only as an aid where a binary still exists; the
failure message asks for a cache listing, which needs no cbm at all.
A recorded transaction refusal still outranks both generic messages, so the
#1416 attribution is unchanged.
This does NOT yet explain why their reservation fails with nothing running —
that needs their environment, which is being requested on the issue. What it
does is stop sending them after the wrong thing while we find out.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Two regressions we shipped ourselves, both with the same shape: a change that
was correct in the abstract broke a concrete thing users had working.
**`update --ui` became a hard error (#1544).** v0.10.2 removed the ui/standard
chooser and deleted the flags that drove it, so a command in scripts, aliases
and muscle memory started failing with "unknown update option" where v0.10.1
had printed the install-script handoff. Retiring a CHOICE is fine; retiring the
WORDS is a break users get nothing for. Both flags are accepted again, do
nothing, and print one note explaining that every build now includes the UI.
A genuinely unknown flag is still rejected — the point is compatibility, not
silence. Reported by @ehendrix23.
**The generated Pi extension could not load (#1550).** Since #1392 the
extension has been generated from the tool registry, emitting
`export function register(pi)`. Pi loads an extension by calling its DEFAULT
export as a factory, so the file failed to load — and a Pi extension that fails
to load takes every `pi` command with it, `pi doctor` included, not just cbm's
tools. It now default-exports the factory. Reported by @musichen.
The existing generator test passed throughout: it asserted WHICH tools the file
lists, never that the file Pi loads is loadable. The new test pins the export
shape, which is the property that actually matters to the consumer.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Five field reports in the 24 hours after v0.10.0 all pointed at the same thing:
gates that were right in principle refused real, ordinary setups, and then
failed to say why. Per the consolidated strictness decision, each gate keeps the
protection that matters and drops the part that was refusing legitimate users —
and every refusal now names what it refused and how to proceed.
**Daemon image gate: npx and every ephemeral install path (#1539, #1383).**
The admission check treated "the peer's image hashes differently" and "the
peer's image cannot be examined at all" as one failure. The second is what
`npx codebase-memory-mcp` always produces (ephemeral cache path,
unfingerprintable), so every npx-invoked client was rejected — and, because the
client never reported it, agents saw a transport that closed mid-handshake with
zero bytes on stdout. Reported by @wassolles with the admission path already
read and the fix space mapped.
An unverifiable image is now admitted: the rendezvous HELLO immediately above it
has already proven semantic version, build fingerprint, and protocol/store/
feature ABI, and the image check was trading that real proof for an unavailable
one. It logs daemon.client_image_unverifiable_admitted so the weaker check is
never invisible. A fingerprint MISMATCH — the tamper case the gate exists for —
still rejects hard. Separate test seams keep the two modes testable apart.
**Client bootstrap failures are no longer silent (#1539).**
An MCP client that cannot reach the daemon now emits a JSON-RPC error on stdout
naming the reason, plus the same text on stderr. Previously the reason sat in
bootstrap_result.message and the process exited having written nothing at all.
**POSIX activation: group-writable ancestors (#1535, discussion #1526).**
activation_directory_secure required no group or other write bit on the install
directory AND every ancestor. WSL2 ships ~ and ~/.local at 0775, as do several
distro skeletons and any site using a shared primary group, so install.sh failed
for a large fraction of Linux users — reporting a policy refusal as "activation
transaction I/O failed", which sent reporters after disk errors and filesystem
types. Root-caused by @AmirF194 in a clean ubuntu container; @shochdoerfer and
@iandol confirmed independently.
World-writable ancestors are still refused (any local user could swap a path
component mid-transaction). Group-writable ancestors are now warned about and
admitted. The LEAF directory stays strictly owner-private — that is where the
binary is published, and group write there would let another account replace the
executable between validation and exec. Refusals now name the directory, its
mode, and which rule refused.
**The obsolete ui/standard chooser (#1538, from discussion #1526).**
v0.10.0 consolidated to one archive per platform with the UI always embedded,
but `update` still offered a variant choice: "ui" could only 404, and "standard"
quietly WAS the UI build. Reported by @iandol upgrading 0.9.0 -> 0.10.0. The
chooser, its --standard/--ui flags, and the ui- URL plumbing are removed, along
with the CBM_VARIANT=ui remnant in the npm installer.
Already-released 0.9.x binaries cannot be fixed retroactively, so the release
workflow now publishes byte-identical ui-*-named alias assets — their updaters
work again with no user action. The aliases are uploaded AFTER the VirusTotal
gate: they are the same bytes as archives it already cleared, and uploading them
earlier would duplicate every object in the scan set and the provenance manifest.
**macOS install noise and attribution (#1537).**
install.sh silenced the "No such xattr: com.apple.quarantine" line, which is
what happens when a curl-downloaded archive carries no quarantine attribute —
harmless, and it became the title of a bug report about an unrelated failure.
The session-stop refusal now points at `daemon status` to list the client
processes actually holding the daemon, instead of asserting sessions exist and
leaving the reader to guess. Reported by @listepo.
**Riders.** hatchling is pinned in pkg/pypi (an unpinned backend resolved fresh
inside `python -m build` is what emitted Metadata-Version 2.5 and broke the
v0.10.1 publish); SECURITY.md's supported-versions table moves to 0.10.x.
Tests: separate seams for unverifiable vs mismatched peer images with a test per
outcome; activation refusal must name directory + mode + rule; a group-writable
ancestor must stage successfully. The update tests drop the flag that no longer
exists. Verified against each reporter's environment shape.
**Open security alerts (all three, OSSF Scorecard).**
- HIGH, binary artifact: an 8.8 MB compiled Go ELF wrapper had been committed at
pkg/go/codebase-memory-mcp by accident. Removed, and both it and its .exe
sibling are gitignored so `go build` in that directory cannot repeat it.
- HIGH, GHSA-2v37-7h3g-55p8: nanoid < 3.3.17 loops forever when a custom
generator is called with size 0. It reaches us transitively (postcss -> vite),
so it is pinned through the existing graph-ui overrides block rather than
promoted to a direct dependency; the lockfile resolves 3.3.18.
- MEDIUM, unpinned pip command: the publish step installed build/twine by
version only, leaving the whole transitive graph resolved at run time.
pkg/pypi/requirements-publish.txt now hash-pins the complete toolchain (316
hashes), generated on a linux/amd64 python:3.12 image so the wheels match what
ubuntu-latest resolves, and the step runs pip with --require-hashes. Verified
by installing from it in that same image.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Three regressions shipped in 0.10.0 share one failure shape: an empty result
with a success status, indistinguishable from "nothing found" for the LLM
clients that are cbm's primary consumers.
1) structuredContent {} on the whole tree-format surface (#1522 bug 1).
#1488 replaced the duplicated payload with an EMPTY structuredContent
object while every tool still declared a blanket permissive outputSchema.
Spec-honoring clients (Claude Code among them) treat structuredContent as
THE result when a schema is declared, so search_graph, trace_path,
query_graph, get_architecture, search_code, and detect_changes all rendered
as literally "{}" on their DEFAULT format, on every platform. The corrected
contract: no tool declares an outputSchema (tool output is
format-parameter-polymorphic — no static schema is truthful), JSON-object
payloads keep their parsed structuredContent, errors keep
structuredContent.error, and text-shaped payloads carry NO structuredContent
key at all — which also preserves #1375's no-duplication win.
2) Frontend queue overflow killed the session (found by the #1522 sweep).
Any 7+ requests pipelined in one stdin burst — an agent issuing parallel
tool calls does exactly this — overflowed the 8-frame frontend queue, which
failed the whole session: rc=1 with ZERO bytes of output, every buffered
response lost. A full queue is now backpressure: the stdin reader blocks
until the worker drains (bounded by the same stop/fail flags every teardown
path already sets); only a single frame larger than the entire 12 MiB byte
budget — which could never be admitted — remains a hard failure.
3) config get printed "" with exit 0 for every unset and every unknown key
(#1522 bug 2). list printed stored-or-DEFAULT while get printed
stored-or-EMPTY, and no subcommand validated key names, so a typo was
indistinguishable from a correctly-read setting. One config-key table now
drives help, list, get, set, and reset: get prints the stored value or the
key's real default (the same fallback the runtime readers use), and unknown
keys error with exit 1 on get, set, and reset alike.
Tests — each RED on the pre-fix tree and RED again on revert:
* test_mcp.c: text results carry no structuredContent key; tools/list
declares no outputSchema; the tool-table guard now binds all three
branches (absent / parsed-object / error) for every registered tool.
* test_daemon_frontend.c: the over-capacity contract flips from
"session fails" to "backpressure without loss" — the held first request
plus all 32 over-capacity frames are answered and the run closes cleanly.
* test_cli.c: the config command contract — defaults, round-trip, reset,
and unknown-key rejection on all three subcommands.
* smoke-test.sh: Phase 3z rewritten to the corrected structuredContent
contract, new Phase 3z1 (default-format replies usable in schema-honoring
clients, no outputSchema advertised), 3z2 (24 pipelined calls all
answered), 3z3 (config defaults + unknown-key rejection) — all asserted
against the SHIPPED artifact, where #1488's smoke phase previously pinned
the empty-object behavior as correct.
Verified end-to-end on the locally built production binary: all 15 tools
declare no schema; text tools return ABSENT structuredContent, object tools
populated, error envelopes intact; 7/24/64-deep pipelined bursts all answered
with rc=0; config get prints real defaults and exits 1 on unknown keys.
hook_augment (structuredContent.projects) and index_resilience
(structuredContent.status) consume object payloads and are unaffected.
Fixes#1522.
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 product binary embedded nine complete shebang'd shell scripts, their
PowerShell/.cmd twins, and two node:child_process client-adapter modules as C
string literals, and wrote them into agent config dirs at 0755 on install. A
block of script-shaped bytes inside a large unsigned executable is exactly the
surface Microsoft Defender's ML scored Trojan:Script/Wacatac.B!ml — the
detection that has recurred across the last several release attempts, on the
Linux ELF and darwin-arm64 standard binaries while every UI and Windows variant
stayed clean.
This moves every integration template out of the binary into a single shipped
data file, assets/cbm-integrations.json (compact: one asset, because each
shipped file is itself scanned). The binary now carries only the templates'
identity — one embedded SHA-256, generated at build time from the JSON so it
can never drift — plus the code to load, verify, and render them.
Why a hash and not just a loose file: content compiled into the binary was
tamper-evident with the binary. Moving it to disk must not turn it into
unauthenticated code the installer blindly executes. install verifies the asset
against the embedded SHA-256 and fails closed on mismatch ("integration assets
missing or modified - reinstall from the release archive") before writing
anything. Verified end to end: a one-byte edit to the asset makes install
refuse.
Ownership, for safe upgrade/removal: the verified copy is published to
~/.cbm/assets/<version>/ — a sibling of the DB cache, never inside it, so
clearing the cache cannot strip a user's hooks. A deployed integration file is
ours to remove only if it matches a template rendered from that stored copy;
foreign files and user-edited files at the reserved paths are preserved. The
historical template bodies travel in the JSON's released[] arrays so files
written by older versions are still recognised and cleaned up on uninstall.
Removed from src/: cmm_gate/session/subagent script-prefix and suffix
constants, cbm_build_released_gate_script, the shell/PowerShell/.cmd bodies in
cli.c, and the JS/TS generators in client_adapter.c. Confirmed on the built
standard binary: 0 occurrences of '#!/usr/bin/env bash', '#!/bin/sh',
'node:child_process', and 'ExecutionPolicy' (was non-zero for each).
test_no_embedded_scripts_contract.sh makes the removal a build property, not a
reachability assumption: no production C source may contain a shebang or
node:child_process literal, and the built binary is scanned directly. It strips
comments first (a contract a comment can satisfy is a false guard), and is
revert-checked — reintroducing one shebang literal makes it fail naming the
file:line. check-binary-composition.sh (A6) enforces the same needles on every
release artifact post-strip.
This does not, by itself, prove the detection is cured: the clean UI binary is a
strict superset of the flagged standard one, so no single passage is a
deterministic trigger. It removes the most coherent malware-shaped composite
from every shipped binary, aligns with how gh/starship/zoxide externalise
integration material, and makes the templates maintainable as data rather than
escaped C literals. Causal confirmation still needs a VirusTotal ablation.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The dry run claimed EVERY hook group as installable because gate_ok /
session_ok / subagent_ok were simply set to `true` when dry_run was on. When
the on-disk hook script is not ours - user-modified, or written by a manual
install pointing at another binary - the real install refuses to rewrite it,
so the preview promised what the run could not deliver. That is the second
half of #1387: the reporter had no way to see the loss coming, and said so.
- New read-only predicate cbm_text_owned_document_status() mirrors
text_migrate_owned_document's decision WITHOUT writing: absent / already
current / an exact released document => a write would proceed; anything else
=> it would be refused.
- The dry run uses it per script, so the three 'hooks:' lines now reflect what
will actually happen, and each refused script is NAMED with the reason.
Silence was the original defect: an unmentioned group reads as 'nothing to
do', not 'this will be skipped'.
Test: cli_dry_run_predicts_refused_hook_script_issue1387 runs the real
dry-run path over an unowned gate script, capturing stdout, and asserts the
warning names the script. RED before, GREEN after, RED again on revert.
cli+agent_profiles+config_text_edit: 292 passed; lint-ci clean.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The end-to-end shell regression could not run on CI (recorded in the
previous commit), which left the fix without a gating test. Move the
notice DECISION out of main.c - which is not linked into the test
runner, and is why no C test could reach it - into src/cli/
hook_augment.c, which is. main.c now calls the shared API for both the
absent-daemon and build-conflict cases instead of carrying its own
copies of the strings, so the test binds the real production path
rather than a parallel one.
cli_hook_conflict_emits_stdout_notice_issue1388 asserts what the bug
actually broke: a build conflict yields a stdout systemMessage naming a
different build and pointing at 'daemon stop'; the absent-daemon notice
stays distinct and keeps pointing at 'daemon start' (which cannot heal
a conflict); and non-Claude dialects receive no bare stdout JSON, whose
channel it would corrupt.
RED-on-revert verified: restoring the pre-fix behaviour (conflict emits
nothing on stdout) fails this test alone, for the right reason
('conflict is NULL'). cli suite 258 passed, the local end-to-end shell
test still green against a seam-bearing binary, lint-ci clean.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Windows users hit 'active CBM sessions and operations could not be
stopped safely; no activation was committed' from install, uninstall
and doctor with ZERO CBM processes running - they rebooted, killed
phantom handles, and deleted runtime folders chasing sessions that did
not exist. The real failure was the activation transaction's Windows
ACL safety check refusing a directory carrying cross-account mutation
grants (commonly the stock Authenticated Users:(M) inheritance from a
drive root - reproduced on the Windows VM with a plain mkdir under C:\
and with an icacls-granted parent), and the refusal detail was recorded
internally but never shown.
cli_activation_diagnostic now prefixes the recorded refusal note - the
predicate, SID and path - plus one remediation line (remove the flagged
grant or use an owner-private directory). The sessions wording remains
for genuine stop/reservation failures, which record no note.
Adds a CBM_ENABLE_TEST_SEAMS setter for the refusal note so the
attribution is testable portably; the regression test asserts the note
reaches the diagnostic (and that the no-note path keeps the sessions
text). RED before the fix, GREEN after, RED again on revert. Verified
end-to-end on the Windows VM: uninstall/doctor against an ACL-tainted
tree now name the ACL check instead of sessions.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Installing 0.9.1-rc.1 over an existing setup deleted the user's working
Claude Code hook entries: when the on-disk hook script is not byte-owned
(a manual install embedding another binary path, or a user-modified
reminder), cbm_write_owned_hook_script_with_legacy correctly refuses the
rewrite (TEXT_UNOWNED) - but the failure branch then called
cbm_remove_claude_hooks / cbm_remove_session_hooks /
cbm_remove_claude_subagent_hooks, destroying the registrations for
scripts that still exist and work. The reporter lost their PreToolUse
Grep|Glob entry and all four SessionStart entries.
A refused script write now records the error and leaves settings.json
untouched; entry removal belongs to uninstall only (which keeps its own
removal path, cli.c:9622).
Regression test builds the reporter's state - unowned gate script with a
foreign BIN path, user-modified session reminder, 0.9.0-form settings
entries plus a foreign Bash hook - runs the full install, and asserts
every pre-existing entry and both script bodies survive byte-identically
while the failure is still reported. RED before this fix (entries
deleted), GREEN after, RED again on revert.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_agent_installed_binary_path renders the expected uninstall content
with ~/.local/bin/codebase-memory-mcp.exe on Windows, so installing the
test fixture with a suffix-less path made exact-content removal of the
migrated auditor profile miss and fail test-windows shard 2/2.
Signed-off-by: Ivan Dergachev <dergachoff@gmail.com>
Codex deserializes ~/.codex/agents/*.toml standalone: an
[mcp_servers.codebase-memory-mcp] table with only enabled_tools fails
with "invalid transport" and the whole role is dropped, so all three
generated roles have been dead on Codex since they first shipped in
v0.9.1-rc.1.
Render command = <installed binary> plus
args = ["--tool-profile=scout|analysis"] in direct Codex profiles,
mirroring the Kiro renderer, and keep enabled_tools. Recognize the
rc.1 transport-less rendering as a released shape so install migrates
those files in place and uninstall still removes them; pass the
installed binary path in the Codex uninstall set so exact-content
ownership keeps matching.
Fixes#1391
Signed-off-by: Ivan Dergachev <dergachoff@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>
The first full Windows x64 CI run of this branch (earlier contract-step
walls had always aborted the leg) surfaced 40 failures: the managed
launcher's fail-closed install/update/uninstall gates and the suite's
POSIX-modeled activation tests were both new in this branch and had
never been reconciled, and they hid three real production bugs.
Production fixes:
- ui: config publishes with POSIX rename semantics. MoveFileExW's legacy
replace cannot supersede a destination a reader holds open (the name
lingers until the last handle closes, even with FILE_SHARE_DELETE), so
a config save racing an open reader failed. The temp file is opened
with DELETE access and renamed in-hand (bare-path target, NUL-padded
rename record) before close, falling back to MoveFileExW on any
rename-by-handle failure so non-NTFS config homes keep the previous
behavior.
- subprocess(win): the child's redirected log handle now shares write
access. POSIX places no lock on the redirected log, so a second
producer may append while the child runs; the missing FILE_SHARE_WRITE
mandatory-locked every other writer out, which the index-supervisor
callback-isolation test legitimately exercises.
- cli(win): the portable uninstall path composes the managed binary name
with its .exe suffix; it could never find (or remove) the installed
binary before.
- activation: security-predicate refusals are now diagnosable. The
transaction's predicates refuse without a usable OS last-error, which
collapsed five distinct refusal classes into one blind "status -3,
os 0" at every caller. Each refusing predicate records what refused —
predicate, untrusted SID, and object path — first-wins so the deepest
cause survives, and the staging error line appends it. This is what
identified every Windows refusal in this batch, including the real
install-failure class on drive-root-rooted payload directories
(Authenticated Users holds inherited Modify there; profile-rooted
directories do not).
Windows test dispatch (release binaries unchanged):
- Test builds (CBM_CLI_ENABLE_TEST_API, the existing test-object macro)
compile the portable activation flow on Windows, reachable solely
through the installed activation test seam and announced loudly on
stderr; release binaries compile exclusively the managed transaction,
so the dispatch polarity fails closed. The suite's install/update/
uninstall calls route through a seam-if-absent wrapper, keeping the
shared activation and agent-config semantics unit-covered on Windows;
POSIX arms keep exercising the real activation machinery.
- Two new release-gate tests assert the fail-closed contract with the
seam OFF: a portable payload may not self-update/self-uninstall, and
install refuses to adopt or replace a foreign file at the canonical
launcher path even under --force.
- Test-bug fixes the first valid Windows runs exposed: the version-
cohort crash test reused one conflict record across the probe and the
turnover acquire (whose entry memset erased the probe's detail before
the assertions read it); the activation staged-path assertion now
checks containment rather than a separator (the \\?\ namespace
performs no forward-slash translation); the ui atomic test captures,
cleans up, then asserts so a failure cannot poison later tests; the
PATH-visibility guard is POSIX-only (Windows configures PATH through
the user registry, not a shell rc).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
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>
The macOS legs went green with the prior calibration but the 3-core
ubuntu-24.04-arm runner is slower still: each spawned worker re-hashes
the entire ASan test-runner image (hundreds of MB) for the exact-build
fingerprint before signalling readiness, which there takes tens of
seconds. Raise the hang-guard deadlines generously - index_supervisor
60s ready / 90s terminal, frontend 30-90s, cli 45s. These are hang
guards, not benchmarks: large values only slow genuine-hang detection,
never mask a regression. (Root-cause fix - a test-only fingerprint
injection seam that skips the hash - is tracked for follow-up.)
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Same class as the index-supervisor calibration, proven by the serial
tail: the daemon_frontend backpressure/EOF tests and the cli forced-
install quiesce child failed on the 4-core ubuntu leg even running
alone on a quiet machine. Their 2 s request/cleanup/acquire budgets are
below real startup cost there (identity capture, frontend spawn, lock
acquisition on cold IO). Raised to 10-30 s; still hang guards - a
wedged run fails loudly.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The parallel runner's isolated-process execution exposed three cli tests
that were passing for the wrong reason on Linux: cbm_zed_config_dir (and
the VS Code/vendor path resolvers) prefer $XDG_CONFIG_HOME over
$HOME/.config, CI runners export it, and an isolated `test-runner cli`
process therefore resolved config paths OUTSIDE the test fixture — the
tests only stayed green in the all-suites single process because an
earlier suite's env mutations leaked into them (hidden cross-suite
coupling, deterministic failure in isolation, macOS unaffected since its
resolvers do not consult XDG).
- cli_detect_agents_finds_zed: save/set XDG_CONFIG_HOME to the fixture's
.config for the detection call, restore after.
- cli_vscode_profile_mcp_uninstall: same pin alongside its existing
HOME/PATH/APPDATA handling.
- cli_durable_profiles_follow_current_vendor_paths: XDG_CONFIG_HOME joins
its unset-and-restore env list.
All 222 cli tests pass locally with a hostile XDG_CONFIG_HOME exported.
No production code changed; no assertion weakened.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The PR CI caught two consumers the format migration missed — both parsed
tool output by its old shape and failed silently or with a wrong count:
- hook-augment built its PreToolUse additionalContext by reading the
legacy 'results' object-array from search_graph format:json; after the
json-tree reshape that key no longer exists, so the hook emitted nothing
(caught end-to-end by tests/windows/test_hook_augment.py). The parser
now walks groups -> qn_prefix/file + column-ordered rows. A new local
test feeds the parser the LIVE server envelope, so any future drift
between response shape and hook parser fails in the normal suite on
every platform — not only in the Windows CI guard. Reproduced RED
(ctx NULL, the exact CI failure) before the fix, GREEN after.
- smoke-test.sh carried six old-shape parsers (callers[N], rows[N]{,
clusters[N], semantic[N]{, and the assert_toon_table helper); the tree
headers never matched, so trace verification read 0 callers. All six
updated to the tree contract; the full smoke script passes end-to-end
locally.
cli suite 222 passed; lint clean; scripted smoke ALL PASSED.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Extensionless bash shims under %USERPROFILE%\.claude\hooks triggered
the Windows 'How do you want to open this file?' dialog whenever an
editor (Cursor) scanned the hooks dir, and could not execute without
bash anyway. All three Claude hook scripts (discovery gate, session
reminder, subagent reminder) now install as .cmd files with cmd syntax
on Windows - @echo off bodies, caret-escaped pipes in the reminder
text, 2>/dev/null redirects - and the install removes the pre-existing
extensionless twin so upgrades stop triggering the dialog. The
settings.json command paths follow automatically since they are built
from the same script-name macros. POSIX behavior is byte-identical and
guarded (no .cmd twin).
Closes#929
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Aider has no MCP support - it reads CONVENTIONS.md but can only run
shell commands. The installer wrote the shared MCP-tool-centric
instructions there, telling the model to call search_graph(...) etc.,
tools Aider cannot invoke; a reporter reasonably asked how that was
ever supposed to work (#1032).
Aider now gets a dedicated variant: the same discovery priority
expressed as runnable codebase-memory-mcp cli commands (usable via
/run), including first-index and list_projects bootstrapping, with the
no-MCP constraint stated explicitly.
Closes#1032
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The 300ms in-process SIGALRM budget in hook-augment self-terminated
with a silent _exit(0) on real cold starts (SQLite/mmap open under
load), so PreToolUse augmentation never appeared in real sessions
(reporter: 0/24 observed) while warm manual invocations worked - and a
fired deadline was indistinguishable from a legitimate no-match run.
Default budget is now 2000ms (the settings.json hook timeout stays the
outer backstop), overridable via CBM_HOOK_DEADLINE_MS (clamped
50..10000). When the deadline fires, the handler write()s a
pre-formatted breadcrumb (deadline, pid, the env knob to raise) to
~/.cache/codebase-memory-mcp/logs/hook-augment-timeouts.log before
exiting - fd and message are prepared at arm time so the handler stays
async-signal-safe; CBM_HOOK_TIMEOUT_LOG overrides the path for tests.
Windows keeps relying on the settings.json timeout (unchanged no-op).
Deterministic reproduction: stdin is a pipe whose writer never sends,
so the read blocks past a 60ms deadline - the timer must fire, exit 0
(fail-open), and leave the breadcrumb. RED before (no breadcrumb),
GREEN after.
Closes#858
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_cli_build_args_json typed unknown flags as strings and shipped them
as JSON args the server ignores: 'trace_path --max-depth 1' succeeded
but traced at the default depth 3 — silently-wrong output that inflated
an agent evaluation by thousands of tokens of unwanted hop-2/3 rows.
Unknown flags for a known tool now fail loudly with a Levenshtein
nearest-flag suggestion:
unknown flag --max-depth for this tool (did you mean --depth?) — run
'cli trace_path --help' for the supported flags
Unknown-tool (no schema), raw-JSON, --args-file, stdin, and supervisor
worker argv paths are unaffected — none of them route through the flag
converter.
Reproduce-first: cli_build_args_json_unknown_flag_rejected is RED on
the permissive code (JSON produced) and GREEN with the rejection.
Closes#997
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Four refinements to the coverage signal:
- extraction: subtract DEFINITE recovery before flagging. Tree-sitter
error recovery plus the ERROR-descending def walker often re-extract
constructs inside a failed region (verified: `def broken(:` comes
back as a def); a region whose every line is covered by definitions
that START inside it is not a miss, and a fully recovered file is not
flagged at all. Container defs (Module/Package) don't count as
evidence, and partially covered regions stay flagged — the
#ifdef-split case keeps its flag because the first branch's function
is genuinely lost
- naming: the query_graph option is graph="missed" (the graph shows
ONLY misses — "coverage" was misleading); shadow project renamed to
"<name>::missed"; tool descriptions updated so agents discover both
the option and its semantics
- hook: the CLI-installed PreToolUse augmenter now also matches Read
and injects a coverage note when the file being read is listed as not
fully indexed ("line ranges X-Y could not be parsed — the file
content you are reading is ground truth"). Safe against the old
issue-362 hazard: the augmenter is structurally non-blocking (always
exit 0, additionalContext only), mirroring the Gemini matcher that
already includes read_file; matcher upgrade bookkeeping updated
- ui: "Missed files" toggle in the graph sidebar renders the miss
graph as a second graph option — /api/layout gains graph=missed
(same db file, shadow-project scoping; base project name validated
as before)
Tests: recovery-subtraction cases (recovered def unflagged, garbage
region flagged, trailing recovered defs keep the flag), CLI matcher
tests updated, e2e resilience fixtures switched to an unrecovered
miss; verified end-to-end: hook emits the note for a flagged file and
stays silent otherwise, /api/layout?graph=missed serves the miss graph
and the code graph is unchanged, frontend build + tests green.
Refs #963
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_cli_sha256_file shelled out to sha256sum/shasum, which is unportable:
the tool may be absent, and under cmd.exe the single-quoted path is not
dequoted so the hash of a downloaded release could never be computed on
Windows. Add a small in-process SHA-256 (FIPS 180-4, src/foundation/sha256.c)
and use it directly — no external tool, no per-OS branching, no popen. The
checksum test now covers three NIST vectors (empty, "abc", and a 56-byte
input that forces padding into a second block).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The macOS digest path invoked `shasum -a CBM_SZ_256` — a literal string
(not a macro; string literals do not expand) naming an invalid algorithm, so
the digest always failed on macOS, and the self-update path then treated a
could-not-verify result as non-fatal and installed the download regardless.
Use the correct algorithm, and install only a positively-verified download:
a mismatch, a missing checksum entry, or an unavailable hash tool now all
abort. Adds a known-vector digest regression test.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>