mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-11 09:25:35 +08:00
bd11088fe30d41cde107a71e6d8ef4a4b673c976
171
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bd11088fe3 |
Merge pull request #1585 from DeusData/fix/installer-scope
fix(cli): stop relocating a binary a package manager owns (#1566) |
||
|
|
5e5dcd8ebc |
Merge pull request #1423 from PR9000/fix/freebsd-cli-self-path
fix(cli): add native FreeBSD self-executable path resolution |
||
|
|
786c5864c7 |
Merge pull request #1421 from PR9000/fix/freebsd-activation-transaction
fix: guard activation_finish_absent_publish behind its call-site conditions |
||
|
|
0e89ef87ba |
fix(cli): stop relocating a binary a package manager owns (#1566)
`install`/`update` bundle two separable jobs: place the binary (and put its
directory on PATH), and configure the agents. For someone who installed through
mise, Homebrew or nix, the first job is not redundant — it is harmful. It drops
a SECOND copy into ~/.local/bin that shadows the managed one depending on PATH
order, and appends to a shell rc file no package manager asked us to touch:
Would install binary -> /Users/user/.local/bin/codebase-memory-mcp
Added /Users/user/.local/bin to PATH in /Users/user/.zshrc
They wanted the agent configs refreshed and nothing else. Reported by @SC-CTS.
`--skip-config` (binary, no configs) has existed for a long time; this adds its
missing mirror. Per the user's decision:
- **Infer by default.** A recognised manager path means the binary is not ours;
we configure the agents and leave the binary and PATH alone.
- **`--skip-binary` / `--force-binary`** override the inference either way.
- **`update` REFUSES** on a foreign binary and names the command that will work
(`mise upgrade`, `brew upgrade`), rather than reporting success while changing
nothing — the dishonest-success pattern this project keeps having to fix.
- **PATH is only touched when WE placed the binary**, which is a bug in its own
right regardless of the rest.
Detection is POSITIVE evidence — the path must look like a known manager's — and
the first implementation got this wrong in the tempting way: "anything outside
our install dir". The test suite failed immediately and was right to. A test
binary runs from build/, and an ordinary `install --dir=/opt/cbm` also lives
outside the default; both would be misread as foreign, and `update` would then
refuse to update an installation we own. The asymmetry settles it: a false
positive costs a working update, while a false negative merely leaves today's
behaviour for an unrecognised manager, which --skip-binary covers explicitly.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
||
|
|
4c793c4ded |
fix(windows): accept the owner Windows actually stamps on what we create
An elevated install created its own staging file and then refused it:
error: failed to stage install candidate: activation transaction I/O
failed: owner-not-current-user
We compared the object's owner against TokenUser. Windows stamps newly created
objects with TokenOwner, and for a member of the Administrators group that
defaults to BUILTIN\Administrators ("System objects: Default owner for objects
created by members of the Administrators group" — default on Server, common on
hardened clients). So on any elevated account the check rejected a file written
seconds earlier by the same process. Reported by @spam0115 (#1580); @gotspatel
hit the same wall from the other side (#1582) — a fresh install whose MCP server
exits during initialize, from `C:\Users\Administrator`.
The fix reads TokenOwner and accepts that SID too. This is stricter-or-equal,
never looser: TokenOwner is the ONE SID this process stamps on its own
creations, so the check still refuses anything a different account owns. It does
NOT accept "whatever an administrator owns" — that would be the loose version,
and the neighbouring activation_windows_owner_is_trusted already covers the
pre-existing-directory case where a broader rule is appropriate. On a
non-elevated account TokenOwner == TokenUser and nothing changes at all.
Windows-only code, so it is unverifiable on the macOS host: this needs the
Windows leg to compile and exercise, which is the next step before merge.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
||
|
|
1e6825f912 |
fix(cli): install into an existing opencode.jsonc instead of a second file (#1560)
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> |
||
|
|
63f26f4ce1 |
fix(cli): forward CBM_CACHE_DIR to the Codex-spawned server (#1562)
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> |
||
|
|
2bb94372a4 |
fix(cli): quote the skill description so strict YAML readers accept it (#1554)
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> |
||
|
|
42e320a6cb |
docs(pi): name the loader mechanism behind the default-export requirement
Two contributors independently sent the same fix for #1550 while ours was already in CI (#1553 by @musichen, who also reported the bug, and #1564 by @henri-edh). Rather than close both as duplicates, this takes the part of theirs that ours was missing. @henri-edh's comment names the actual mechanism: Pi loads an extension via jiti.import(path, { default: true }) and rejects anything that is not a function, so a named-only export hands the loader the module namespace object and the install fails with "Extension does not export a valid factory function". Ours said "Pi calls the default export" — true, but it does not tell the next reader why a named export cannot simply be added alongside, which is exactly the change someone would try. Their wording is now ours. The assertion is tightened the same way, from `export default function` to `export default function (pi)`, so a default export with the wrong shape cannot satisfy it. Co-Authored-By: musichen <musichen@users.noreply.github.com> Co-Authored-By: henri-edh <henri-edh@users.noreply.github.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
ff853ac14b |
fix(cli): stop blaming sessions for a failure that has none (#1537)
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> |
||
|
|
a154d18a2e |
fix(cli): stop breaking commands people already type (#1544, #1550)
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> |
||
|
|
8eabe191d2 |
fix(install,daemon): unbreak npx clients, group-writable homes, and legacy updaters
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> |
||
|
|
f59d24fd10 |
fix(mcp): make every tool reply client-usable — structuredContent, pipelining, config get (#1522)
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>
|
||
|
|
57f30e28e3 |
style(cli): clang-format the uninstall --dir target resolution
Ran the format gate before adding --dir rather than after, so the ternary landed unformatted and lint/lint went red. Reformatted with the pinned Homebrew LLVM clang-format; the diff is confined to that block and cli.c still compiles. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
a3ae04571a |
fix(cli): restore uninstall --dir so a custom install stays removable
The Windows long-path guard failed with: error: unknown uninstall option: --dir=C:\...\install_Ωμέγα\...\bin Restoring cli.c from before the externalization dropped --dir from uninstall while install kept it. That is worse than a test failure: anyone who installed outside ~/.local/bin had no supported uninstall path at all — uninstall would look in the default location, find nothing, and leave the real install in place. Parsing mirrors cbm_cmd_install (--dir=PATH and --dir PATH, both rejecting an empty value), and the resolved directory now feeds the target path with an explicit truncation check instead of an unchecked snprintf into a fixed buffer. Confirms the earlier cascade read: with the extended-path probes fixed, daemon_stability, hook_augment, cli_non_ascii_arg and ui_drive_listing all went green on their own. non_ascii_path was the only real failure, and it was hiding two distinct product regressions behind one red guard. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
1ba668cded |
fix(cli): keep the install/uninstall existence probes extended-path safe
test_non_ascii_path.py caught a real product regression I introduced:
[FAIL] long-path existing target was overwritten despite --no
(target existence probe is not extended-path safe)
Restoring cli.c from before the externalization also reverted three probes from
cbm_path_info_utf8() back to plain stat(). On Windows stat() resolves through
the ANSI code page, so an extended-length or non-ASCII target reports "absent" —
and a non-force `install --no` then overwrites the exact bytes the user asked to
keep. Silent data loss, not a cosmetic difference.
That fix rode along in the externalization commit but is orthogonal to it: the
wide-path helpers it depends on (win_utf8.h, compat_fs.c) were kept. Only their
call sites in cli.c were lost.
Two of main's five cbm_path_info_utf8 sites lived in asset code that is
correctly gone; the two restored here are the install target probe and the
uninstall binary probe.
Not yet explained: test_daemon_stability.py also went red in the same run
("daemon did not accept the UI configuration"). Its code path is identical to
main's apart from the intended capability predicate, and the guard that failed
before it completes an install into a long path it should have refused, so this
may be downstream state. Pushing this fix to find out rather than guessing.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
||
|
|
d58afe562d |
revert(release): re-embed runtime assets into the single shipped binary
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>
|
||
|
|
8018561cfe |
fix(release): externalize runtime assets and harden VT verification
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
a6f132bc85 |
harden: ship integration templates as a verified JSON asset, not embedded bytes
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>
|
||
|
|
1b8beb266f |
harden(cli): stop shipping a policy-bypass invocation inside the binary
`cbm update` printed the Windows update command with an execution-policy override baked in as a string literal. That put one of the most heavily weighted malicious-script patterns there is inside every Windows artifact we distribute — to save the user a single documented step. The command now prints without it. Functionality is unchanged: the user still gets a runnable command, and the failure it was pre-empting (PowerShell refusing a file that arrived from the internet) is answered by Unblock-File, which the message now names. The restricted-policy case is documented in the README's Windows install section, which is the right home for that incantation — a shipped binary is not. Scope note, so this is not oversold: the 2026-08-07 dry-run detections are NOT attributable to this string. It is `#ifdef _WIN32`, and every Windows artifact scanned clean; the three flagged artifacts were Linux and macOS. This is margin hygiene, not a fix for that regression — see the analysis in the PR. Verification limit worth stating: the built-artifact check for this string is vacuous on a non-Windows host, since the literal was Windows-only to begin with. What is verified here is the source (no occurrence outside a comment) plus lint and the cli suite; a real artifact check needs the Windows build leg in CI. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
d35589cf1b |
fix(cli): make --dry-run predict a refused hook-script rewrite (#1387)
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> |
||
|
|
856ec69f84 |
Merge remote-tracking branch 'origin/main' into fix/1388-hook-conflict-notice
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> # Conflicts: # tests/test_cli.c |
||
|
|
ea466487e8 |
test(hook): bind the conflict notice with a portable reproduce-first test
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>
|
||
|
|
c9bbd880c7 |
Merge pull request #1449 from DeusData/fix/1416-activation-refusal-attribution
fix(cli): attribute activation refusals to the recorded safety check (#1416) |
||
|
|
2044c5c657 |
fix(cli): attribute activation refusals to the recorded safety check (#1416)
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> |
||
|
|
55c2994b08 |
fix(install): never remove existing hook entries on a refused script rewrite (#1387)
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> |
||
|
|
e512c80e58 |
Merge pull request #1408 from dergachoff/fix/codex-agent-toml-transport
fix(install): declare a transport in generated Codex agent role TOMLs |
||
|
|
66e9aec28d |
Merge pull request #1392 from DeusData/feat/generated-client-adapters
feat(install): generate pi and OpenCode extensions from the tool registry |
||
|
|
80afcd6bbe |
fix: close the memory-error paths the clang-analyzer lane surfaced
The memory-diagnostics report's priority-4 lane (path-sensitive clang-analyzer, memory checks only) run over all 111 production files. 21 findings triaged; the real ones, all cold-path (none can explain #581's per-query residual): LEAKS - mcp get_architecture: scope_path leaked on the missing-store early return (REQUIRE_STORE frees only `project`); allocate after the gate. - pass_definitions: cancellation mid-extraction leaked the pass-owned result cache including already-extracted entries; mirror the end-of-pass cleanup. - store package-boundary scan: the row-scan abort path freed the node arrays but not the boundary accumulators or their duplicated package strings. - cbm quarantine set: a duplicate path line leaked the replaced value (and a fresh key copy -- the table borrows key pointers); a partial strdup failure leaked the surviving half. Reuse the stored key for duplicates. - pass_githistory: unchecked malloc/strdup -- an OOM dereferenced NULL and a failed strdup leaked the index cell. Allocate before claiming the slot. NULL/UB - cli config subcommand: NULL argv with nonzero argc slipped the guard (the inner `argv &&` shielded only the help comparison) into argv[0]. - store bfs_multi: a negative max_results broke out before any row was written, then freed fields of an unwritten negative-index slot. Clamp. - pass_calls emit_http_async_edge: the service-pattern call sites pass a NULL target behind a hand-duplicated URL predicate; a drift between the copies turned target->id into a null deref. The callee is now total. - sqlite_writer: both leaf-array OOM paths left leaf_count stale with a NULL array, walking pb_finalize_* into leaves[0]; consistent empty state routes them to the existing root=0 failure return. HARDENED (invariants true but invisible to path-sensitive analysis) - Leiden CSR + aggregate arrays, SCC adjacency: calloc + endpoint guards, so a future degree/collection miscount degrades benignly instead of UB. - SCC cycle fill: the ncyc==0 no-slot invariant made local. RECORDED FALSE POSITIVES (no code change) - yaml sequence starts (loop bound == alloc bound), cypher agg arrays (same count both sides), mcp read_message ch (assigned by fgetc each iteration), pkgmap clean buffer, mcp csize (Tarjan: ncomp>=1 when nverts>=1), vendored verstable x2. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> GATE + LANES (user decision: runner cost accepted) - make lint-mem (local triage) and lint-mem-ci (gating: vendored-filtered, any remaining finding fails). The gate is green because every false positive above was restructured for provability -- calloc'd fill-cursor arrays, explicit Tarjan invariant, zeroed buffer tails, min-1-element allocations -- never suppressed. - make diag: pinned newest-LLVM ASan/UBSan lane with straighter stacks. - CI: lint-mem job (_lint.yml) and test-diag job (_test.yml), both on the pinned LLVM 22 apt toolchain. Cost disclosure: roughly +25-40 min and +25-60 min (ccache-warm) per push respectively. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
305b89bfa8 |
fix(cli): add native FreeBSD self-executable path resolution
cbm_detect_self_path had no branch for FreeBSD, falling through to the generic #else that reads /proc/self/exe. In this environment /proc is a native FreeBSD procfs mount left empty (only /compat/linux/proc is populated, for Linux-ABI processes under the compat layer), so the readlink always failed silently and the function fell back to the expected install path instead of the actual running binary's path. This broke install: with no valid self path, the activation transaction could never confirm what to publish and aborted early with "activation transaction I/O failed" before attempting any linkat/rename. Add sysctl(CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1) for FreeBSD, matching the pattern already used in the test suites, and include <sys/sysctl.h> guarded by __FreeBSD__. Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> |
||
|
|
a84ef75b5b |
fix: guard activation_finish_absent_publish behind its call-site conditions
Building on FreeBSD after syncing with upstream failed with -Werror,-Wunused-function on activation_finish_absent_publish in src/cli/activation_transaction.c. The function is only invoked from three platform-specific branches: Windows (_WIN32, MoveFileExW), macOS (__APPLE__, renameatx_np), and modern Linux (__linux__ with SYS_renameat2, via syscall). On every other POSIX target — including FreeBSD and other BSDs — control falls through to the portable linkat(2) fallback, so the helper was defined but never called, tripping -Wunused-function under -Werror. Rather than excluding FreeBSD by name (which would still break on OpenBSD, NetBSD, DragonFly, Solaris, or any Linux without SYS_renameat2), wrap the function definition in the same #if condition as its call sites. This keeps the guard mechanically tied to actual usage instead of enumerating platforms, so it stays correct as new targets are added. Signed-off-by: Pedro Ramos <131530838+pr9000@users.noreply.github.com> |
||
|
|
07885ef6a9 |
fix(install): declare a transport in generated Codex agent role TOMLs
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> |
||
|
|
029416fda6 |
Merge remote-tracking branch 'origin/main' into qa/call-reference-rebase
# Conflicts: # Makefile.cbm # internal/cbm/extract_defs.c # src/foundation/compat_fs.c # src/foundation/compat_fs.h # src/pipeline/pipeline.c # src/pipeline/pipeline_incremental.c # tests/test_main.c # tests/test_pipeline.c # tests/test_store_checkpoint.c |
||
|
|
3a5790b075 |
feat(install): install generated pi and OpenCode extensions
Wires the registry-driven generators into install, plan and uninstall. pi gets ~/.pi/agent/extensions/cbmem.ts, its only route to the graph -- pi has no MCP client. OpenCode gets ~/.config/opencode/plugins/cbm-augment.ts, which adds no tools (it already reaches all of them over MCP) and supplies only the automatic graph lookup before a grep/glob that other clients get from their own hook configuration. OpenCode has none; its plugin system is the only extension point, verified against their plugin documentation. Both go in as a MARKED BLOCK rather than a whole-file write. Those directories are auto-loaded, so a user may legitimately keep their own module there, and an install routine that clobbers it is destroying user content. Uninstall removes only our block and leaves the rest of the file, matching every other uninstall path here. A generator returning NULL is a hard error rather than a skip. A silently absent extension is precisely the failure that left the earlier proposal a no-op for six weeks, so it must surface as an install error instead of a quiet success. Note for reviewers: the generated body deliberately carries NO ownership markers. cbm_text_upsert_managed_block adds them itself and rejects content that already contains them -- an existing guard test (test_cli.c:6018) caught that integration mistake before CI did, which is the test doing exactly its job. Reimplements the ideas from two community PRs, both closed as superseded by this work, with credit to their authors: - #534 (@Tensorboyalive) proposed the pi extension. It embedded 380 lines of TypeScript as C string literals and registered 7 of the 15 registry tools; this generates from the registry so the surface cannot drift, and ships no .ts in the repository. Their marker-delimited editing, their uninstall test asserting the markers are GONE, and their defensive stdout parsing (take the last line that parses, not the first) are all carried forward here. - #616 (@ZeR020) proposed the OpenCode plugin. Its payload omitted hook_event_name, which hook-augment requires, so it produced zero bytes; and its path template rejected the double quote but not the backslash, so a Windows home like C:\Users\urs\bin produced an invalid unicode escape and the auto-loaded plugin failed to parse. Both are closed by construction here and pinned by tests. Their hook-point research is what made the OpenCode side possible at all. Tests: install writes both extensions and the dry run does not; uninstall removes our block while preserving a user's own content in the same file. Co-Authored-By: Tensorboyalive <Tensorboyalive@users.noreply.github.com> Co-Authored-By: ZeR020 <ZeR020@users.noreply.github.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
2a353b2a33 |
feat(cli): generate client extension modules from the tool registry
pi has no MCP client, and OpenCode has no declarative hook configuration -- verified against OpenCode's own plugin documentation, which states hooks are available only through JavaScript/TypeScript plugin modules. For those two clients a module is the only extension point. We do not want to ship such a module as a repository asset. #534 proposed one embedding 380 lines of TypeScript as C string literals and registering 7 of the 15 registry tools; every tool added afterwards would have been silently missing for that client. Hand-maintained copies of the tool list have already produced defects here (#1361, and the smoke-invariants count). So the module is GENERATED from the live registry instead. cbm_client_adapter_pi walks cbm_mcp_tool_count()/cbm_mcp_tool_name() and registers every tool, which makes drift structurally impossible rather than merely discouraged: adding a tool to TOOLS[] adds it to every generated adapter with no second edit. Nothing in the repository is a .ts file. Both emitters wrap their output in ownership markers so a caller can rewrite its own block and leave a user-authored file alone. Two defects from the prior proposals are closed by construction: - Path escaping. #616's template rejected the double quote but not the backslash, so a Windows home like C:\Users\urs\bin produced an invalid unicode escape and the whole auto-loaded plugin failed to parse, which is worse than an absent plugin. cbm_client_adapter_escape_js escapes backslash, quote, newline and CR, and fails closed rather than emitting a truncated literal; generation aborts if the path cannot be escaped. - The silent no-op. #616's payload omitted hook_event_name, which hook-augment requires and without which it accepts nothing, so the plugin emitted zero bytes for six weeks. The generated payload carries it and a test pins it. Recorded honestly: the OpenCode emitter hooks tool.execute.after, whose ability to modify a tool's output is NOT part of OpenCode's documented plugin contract (only tool.execute.before's argument mutation is). If they change it the augmentation stops with no error. That risk is accepted deliberately and the emitter says so in a comment, so a future reader does not have to rediscover it. This commit adds the generator and its tests only; wiring into the install and uninstall routines follows separately so the two are reviewable apart. Tests: every registry tool appears in the pi module (revert-checked by simulating the 7-of-15 subset, which fails); Windows/quote/newline escaping and its truncation boundary; the OpenCode payload carries hook_event_name and registers no tools; NULL/empty binary paths generate nothing. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
958a511345 |
docs+cli: finish the install-script update contract
The update handoff moved out of the product on every platform, but two loose ends were left behind. README was actively WRONG, not merely incomplete. It still documented in-place updates for macOS/Linux, and it promised "The MCP server also checks for updates on startup and notifies on the first tool call" -- a claim that died with the MCP update thread and then the daemon's curl check. The section now states the one contract that holds everywhere (`update` validates flags and prints the install script command), why it works that way (on Windows a running image cannot replace itself; on POSIX a deliberate choice, because an in-process updater is structurally a downloader), and the affirmative property that replaced it: cbm makes no network request of its own accord and the archives carry no download URLs at all. `uninstall` now REPORTS the install script it finds beside the binary, printing the path and the `rm` command, and says "would leave" under --dry-run. It does not delete it. install.sh places itself there so `update` has something to hand off to, but that file may equally be the user's own copy, a symlink into a checkout, or package-manager managed -- an uninstaller must not delete a file it cannot prove it owns, and one printed line leaves the decision where it belongs. Windows checks install.ps1 too and tolerates either path separator. 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> |
||
|
|
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>
|
||
|
|
0b31a5dcdf |
fix(security): declare the printed install-script URL on the allow-list
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>
|
||
|
|
0a22fb14df |
fix(windows): absorb transient file locks when retiring a running image
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
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>
|
||
|
|
e5c79418ca |
revert(install): restore the activation-transaction diagnostics to main
The refusal notes changed the error text the Windows launcher guards match against, and test-windows-guards went red on this PR after they landed. They were investigation instrumentation and they succeeded -- they turned 'staging transaction open failed (status -3, os 0)' into ERROR_DISK_FULL and named the exact site -- but a diagnostic that shifts a matched error string does not belong in a release-blocking change. Worth redoing deliberately on its own, with the guards' expectations updated in the same commit. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
d2dee7b8ff |
diag(install): report the OS error from a failed staging copy
The copy loop's failure was reported as 'os 0' because the cleanup and close calls after the loop clobber the thread's last error. Captures it at the failure point, and separates a failed read from a failed write -- copying a payload that another process still holds is a different problem from a full disk. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
536d228631 |
diag(install): split the staging copy failure into named causes
The copy loop's five distinct failures -- short read/write, an empty source, a failed fsync, a failed close, and a failed directory sync -- shared one branch and one unnamed IO status. Exhaustive labelling identified this as the path the Windows launcher guard's immediate reinstall takes; this names which of the five actually fires. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
6d95d4f8df |
diag(install): name every unlabelled staging IO refusal
Fourteen paths returned CBM_ACTIVATION_TRANSACTION_IO without setting a refusal note, so the caller could only ever print 'status -3, os 0' -- an I/O error with no cause. Identifying which one fires cost a ten-minute Windows guard run per hypothesis, and two targeted guesses missed. Labels every site so a single run names the path. Temporary labels; once the guard's path is known they become descriptive. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
15cbbc505a |
fix(install): name the target-directory refusal when preparing a transaction
activation_transaction_prepare refused an insecure target directory and returned IO with no note, while destroy() on the way out clobbered GetLastError -- so the caller printed 'staging transaction open failed (status -3, os 0)': an I/O error with neither a cause nor an OS code. That is the path the Windows launcher guard's immediate reinstall takes, and the missing note is why each hypothesis about it cost a ten-minute guard run. A target directory failing the exact-owner rule is a different bug from an unreadable source, and now says so. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
e65e8ee42d |
fix(install): name which source refusal failed when staging
Three distinct refusals -- an insecure source directory, a failed snapshot, and a missing source file -- collapsed into one boolean, and the frees on the way out clobber GetLastError, so the caller reported 'staging transaction open failed (status -3, os 0)': an I/O error with no cause and no refusal note. The Windows launcher guard's immediate-reinstall step fails through exactly this path, and with no cause reported each hypothesis cost a full ten-minute guard run. Captures the OS error before the frees and names the predicate that refused. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
62821375c4 |
fix: harden daemon release paths and verification
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |