mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-11 09:25:35 +08:00
57f30e28e30ffc1fbc5fc8e00ed0a4dc9824844b
156
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
568e613141 |
test-infra: overlap the local ladder, open the serial tail, keep container I/O off virtiofs
Local runs left most of the machine idle: the serial tail ran sixteen suites one at a time on otherwise-idle cores, the three platform legs were launched by hand (usually sequentially), and every container-leg object write crossed the virtiofs bind mount. - run-tests-parallel.sh tail scheduling in two phases: the FLEX suites (timing-shaped but free of the shared per-account daemon runtime namespace) run CBM_TAIL_JOBS-wide (default 2), then the EXCL group — daemon-family plus the suites that drive daemon one-shots or supervisor rendezvous — runs strictly sequentially on a machine exactly as quiet as the old fully-serial tail gave it. The wave was already fed longest-first by the shard dealing order, so the drain-out no longer ends on a heavy straggler. - ladder.sh: one maintained entry point for the full local push gate with the legs overlapped — lint, the Linux container suite, and the Windows VM suite in the background, the macOS suite in the foreground, one verdict per leg, logs kept per leg. A missing prerequisite fails its leg loudly instead of silently skipping. - win.sh test-par now runs through vm-run-tests.sh (--par mode): the full parallel harness under the CI-shaped protected temp root with complete output. It previously ran under the MSYS-shared /tmp and piped through `tail -25` — the same truncated-blindness class that hid 40 Windows failures from the `test` command. - docker-compose: build artifacts and the incremental fixture cache move to named volumes on the container VM's native filesystem. Object writes over the virtiofs bind mount are the container legs' largest avoidable I/O cost, and the fixture cache now survives across container runs. - test_mem(win): the first valid full-parallel VM run proved working-set trimming beats the re-touch mitigation (19 MB resident of a 256 MB double-touch at 18 parallel suites). The RSS probe now VirtualLocks a 64 MB span — locked pages are exempt from trimming, making the measurement pressure-immune — with bounded touch-and-sample retries when the lock is unavailable. Red-to-green under the same 18-job load. - cli: the portable install's staging error now appends the activation refusal note (predicate, SID, object) like the managed path already does — a bare "activation transaction I/O failed" on a CI-only failure is undiagnosable without it. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
f51745533c |
fix(win): reconcile the C suite with the managed-launcher contract
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> |
||
|
|
34f9ad1b3d |
fix(daemon,win): CI-green fixes and a daemon-stability guard
Fixes surfaced while bringing the shared-coordination-daemon branch green across macOS, the Linux containers, and the real Windows-ARM64 VM, plus a new stability guard that caught one of them. - daemon(win): the CLI teardown transition latched a failed release as permanent, but a Windows participant-state release must briefly try-hold the shared startup/legacy gates and legitimately collides with a concurrent one-shot's teardown; the release is retriable by contract (it always retains the transition), so parallel one-shot commands no longer report "CLI coordination cleanup failed" despite succeeding. Found by the new stability guard's churn section. - cli/main(win): install/update/uninstall and the Augment hook-script removal derive the managed launcher's identity from its plain drive-form path (cli_windows_plain_utf8), not the \\?\ extended-length form, so agent-config ownership matches on Windows and uninstall no longer leaves the MCP entry or owned hook scripts behind. - security: the `daemon start --open` browser launch is now shell-free (ShellExecuteW on Windows, cbm_exec_no_shell elsewhere) instead of system(); the CORS origin check spells out its two literal loopback URLs so the static URL audit sees a complete value. Both clear the Layer-1 allow-list audit. - coordination: version-cohort lock retries now sleep a per-process jittered interval — fixed-period retries can phase-lock two participants so one starves. The activation-quiesce test's observation window now covers worst-case candidate staging (three tamper-defense hashes over a ~1 GB sanitizer binary exceed the old 30 s budget on container I/O). - smoke/soak: the agent-config smoke matches Windows config paths through their escaped-backslash quoted form; the soak parser skips the update-available banner before the JSON summary and gained a one-shot CLI admission-churn phase inside the RSS/FD leak window. The build-dir safety fixture creates a real symlink/junction (MSYS2 ln -s otherwise deep-copies) so its traversal-refusal contract is genuinely exercised, and the symlinked-agent-roots test demotes its planted links to an unprivileged owner when run as root so the refusal is really tested. - tests(win): a new daemon-stability guard exercises the parameter surface, hook fail-open with its rate-limited notice, start-twice and occupied-port handling, busy-stop refusal against a live MCP session, kill -9 crash recovery, and sequential/parallel one-shot churn. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
0baddf7887 |
daemon: permanent lifecycle, daemon-backed CLI/hooks, real-Windows hardening, long-path launcher transactions
Daemon lifecycle and Windows correctness, verified on a real Windows 11 ARM64 VM through the maintained test-infrastructure/vm drivers, plus the macOS and Linux arm64 suites and the container lint gate. Daemon lifecycle: - daemon start/stop/status subcommands. `daemon start` launches a PERMANENT daemon (spawn shape is byte-exact argv; survives idle periods and session ends) and reports an already-active daemon instead of failing. Permanence is honored at every stop latch: last-committed-client disconnect, host initial-client window, coordinator release, and application final-session close — a permanent daemon also keeps admitting new sessions after its last one closes. - daemon stop refuses while sessions are active and lists the blocking peers (pid/role) that must finish first; an idle daemon drains through the activation-shutdown machinery with the ACK ordered after connection interrupts. A second stop is idempotent. The wire ops are no-cohort first-frame requests with peer fingerprint authentication, so stop/status never conflict with an exact-build admission gate. - One-shot CLI commands now execute through the daemon (index workers keep their local supervised path). A cold CLI run that had to spawn a temporary daemon prints a hint that `daemon start` removes the per-command startup tax; a warm daemon is recycled silently. - Hooks are connect-only fail-open: with no daemon present the hook emits a visible, rate-limited notice (Claude-dialect systemMessage plus stderr for other dialects) and always exits 0 — augmentation is never allowed to block the caller's tool use. - Version skew: a newer-build client automatically drains an older-build permanent daemon (strict semantic-version triples only; dev builds never auto-drain) and the build-conflict message names `cbm daemon stop` as the manual escape hatch. Windows IPC/runtime (real-VM verified): - ipc(win): persistent pending overlapped ConnectNamedPipe. The accept path used to destroy its listening pipe instance on every 20 ms poll timeout; a client attaching in the teardown window was severed or left on an orphaned pipe object whose HELLO no server handle could ever read, absorbing the connect until the client's own timeout expired. The pending connect now survives poll timeouts and nothing is destroyed while a client could be attaching. - ipc(win): drain-before-close for final responses. Closing a named-pipe server handle can discard a just-sent response before the peer reads it (POSIX stream sockets never lose buffered data on close). A bounded cbm_daemon_ipc_connection_drain (read-until-peer-EOF; no-op on POSIX, immediate on interrupted connections) now precedes close in runtime_worker_finish and runtime_reject_inline, so hello-conflict, capacity and disconnect acknowledgements reliably reach the peer. - runtime: CLOSE_INTENT wire frame. A Windows named-pipe client has no transport half-close, so close_begin now announces departure with an explicit frame (ordered after APPLICATION_CANCEL, before the local interrupt); the server releases the client's admission on receipt instead of waiting for the handle to close. Admission-drop timing is now identical to POSIX shutdown() semantics on every platform. - runtime(win): client close cancellation. close_begin serializes with request publication under the send lock, best-effort sends the active token's APPLICATION_CANCEL frame, then interrupts local I/O; the server cancels MCP/subprocess work promptly. Contract tests accept both correct outcomes (interrupted transport or decoded CANCELLED). - runtime: activation acknowledgement ordering. The activation ACK is the requester's license to act on "snapshotted and draining", so every connection interrupt is now initiated before the ACK is sent; a session could previously get one more request serviced after the requester observed the ACK. - service(win): deadline-bounded private-file prepare. The conflict-log prepare retry loop (100 x Sleep(2), which rounds up to the ~16 ms timer granularity) burned ~1.6 s against permanently obstructed paths, stalling hello rejections past the client's timeout. The retry budget is now a 250 ms deadline; transient share collisions still retry. - subprocess(win): cmd.exe /C payload encoder quotes metacharacters correctly (root cause of the git-on-Windows failure cluster). - watcher: SHA-256 buffer sizing (CBM_SZ_64 -> CBM_SZ_128) and a native Windows stop/unwatch cancellation test with exact-image verification. - httpd: send_all writes in bounded 64 KiB slices. A single giant nonblocking send() on Windows is absorbed wholesale into AFD kernel buffering regardless of SO_SNDBUF, so send deadlines and interrupts could never engage against a slow peer (and the full payload was pinned in nonpaged pool). Slicing restores a deterministic backpressure point; a test hook pins SO_SNDBUF for the deadline and interrupt tests. - ui/http: shutdown lifecycle — interrupt checks, response-wide send deadline, explicit connection states, refusal to free a server while a listener-owned connection is active. Windows long-path support: - Central path-aware wide conversion (canonicalize via GetFullPathNameW and prepend the extended-length prefix for absolute paths >=240) at the compat chokepoints (cbm_fopen/compat_fs/mkstemp/mkdtemp), sqlite store opens, and the daemon build-fingerprint/log paths. Deep managed installs (a 64-hex generation directory routinely exceeds MAX_PATH) now index, stage and activate correctly. - activation transaction: its own file APIs and the component-walking ancestry validators now operate in the extended-length namespace; the launcher path is canonicalized (and prefixed when deep) once at entry so every downstream exact-string comparison stays form-consistent. - Executable self-resolution uses the wide APIs (GetModuleFileNameW, GetFileAttributesW) so non-ASCII install paths survive argv[0] resolution. Windows launcher install/uninstall transaction: - FileRenameInfoEx names are NUL-terminated in an over-allocated buffer. FileNameLength governs per the contract, but filter drivers read FileName as NUL-terminated and appended adjacent heap bytes to created names — a flaky, garbage-suffixed rename target. Both the CLI and the launcher rename helpers are fixed. - Uninstall retires state via rename-aside (.cbm -> .cbm-retired-v1-<tag>-<pid>) with the retired tag shortened to 16 hex chars so the bare rename target stays under the FileRenameInfoEx NT-conversion ceiling at guard depths; 64 bits still uniquely identify the generation. - When the running launcher's mapped generation backings pin .cbm against rename, the backings are relocated to activation-<pid>-N .retired tombstones beside the install (a mapped image may be renamed, never deleted; the launcher's liveness-guarded sweep reclaims stale tombstones). Every relocation is recorded, and a FAILED uninstall reverses the moves after restoring .cbm — via MoveFileExW with extended-length paths on both arguments, since the deep generation target is beyond the handle-based rename's bare-path reach — so a restored install keeps its generation backings and stays runnable. - After a committed uninstall the retired tree's backings are relocated out so the tree is shallow enough for the detached cleanup's rd, and the cleanup's working directory strips the extended-length prefix (CreateProcessW lpCurrentDirectory silently ignores prefixed paths). - Files created under Administrators-default-owner directories (CopyFileW destinations, CREATE_NEW tombstones, probe directories) are explicitly owner-stamped so the exact-owner validators hold on runner images; guard fixtures stamp hand-built trees the same way. Diagnostics, tests and infra: - diagnostics: discovery is now an always-delivered JSON control record (new cbm_log_control) that survives CBM_LOG_LEVEL suppression and paths containing spaces; placement honors $TMPDIR with /tmp fallback via a diagnostics-local helper; the soak parser reads the JSON record; documented in docs/CONFIGURATION.md. Red-first coverage for suppressed log levels, TMPDIR-with-spaces, and native Windows output-contract assertions. - tests(win): daemon_ipc/daemon_frontend fixtures now build endpoint parents with production-shaped ancestry (LocalAppData on Windows, via th_secure_runtime_parent_new) — the runtime ancestry validation correctly refuses temp roots whose ancestors grant mutation rights to Authenticated Users (C:/msys64/tmp, GitHub-runner work dirs) — and drive the documented startup-owner publication flow before reading generation-bound endpoint addresses. This turns the 26 Windows failures previously visible in CI's full-test job green without weakening any validation. - tests(win): the launcher guard covers the full permanent-launcher contract including failed-uninstall restore and immediate reinstall after uninstall; new daemon lifecycle and reworked hook-augment guards run the start/recycle/stop flow end to end. - tests: CBM_SKIP_PERF is now actually consumed by the test runner (it was set by CI but never read, so perf suites ran everywhere); four throughput/bench suites are classified as perf, the heavy store_arch suite moved to the slow-timeout tier, and two wall-clock-sensitive assertions were rewritten as invariant checks with coarse hang-detector backstops. - build/test infra: build-dir safety contract, UI dev-proxy security contract, soak daemon-recovery contract, path-safety helper, the Windows VM worktree-sync contract wired into scripts/test.sh, and vm/win.sh guards building its clean embedded-UI product in an isolated BUILD_DIR so it cannot clobber the incremental test build. provision-windows.sh now installs Node.js for the guards UI build. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
ba91c6c396 |
fix(cli): resolve msys-form profile paths and create the staging-root chain
cli_windows_staging_root builds the per-user install staging root from
LOCALAPPDATA/USERPROFILE, but msys and CI harnesses export those in POSIX
drive form ("/c/Users/..."), which the CRT cannot resolve, and the target's
AppData\Local ancestors may not exist yet on a fake-profile fixture. Rewrite
a leading "/c/" to native "C:/" before use, create the whole ancestor chain
(a single mkdir left the root missing), and fall back to the temp root only
after verifying the directory exists. Also name the failing sub-step when
private staging cannot even create its directory, so the opaque "did not
remain runnable" message is no longer the only signal.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
||
|
|
aa90491792 |
fix(cli): stage Windows install transactions under a per-user root
The activation transaction and the release-descriptor probe validate the
full ancestor chain of everything they stage: every path component must
have a trusted owner and grant mutation rights only to trusted identities.
That walk is deliberate TOCTOU defense and stays strict. The staging
directory, however, came from the process temp root, which routinely lives
in a tree that legitimately fails the walk through no fault of ours: under
msys2 TMP resolves inside the msys install tree (C:\msys64\tmp), and on
GitHub runners inside the runner work directory, both of which carry broad
inherited grants such as Authenticated Users modify on an upper component.
The staged pair then failed validation ("launcher descriptor candidate is
unsafe" / the smoke Phase 8 --version probe refusal) even though the staged
files themselves were private.
Add cli_windows_staging_root(): stage install and update transactions under
%LOCALAPPDATA%\codebase-memory-mcp, whose ancestor chain is user-owned end
to end, so the strict walk passes by construction instead of by environment
luck. Falls back to USERPROFILE\AppData\Local and finally the process temp
root when the environment is degraded.
Also make the staging observable where it previously failed blind:
cli_windows_stage_private_file reports which transaction step failed with
the OS error code, and cbm_mkdtemp warns once if the private security
descriptor cannot be applied and it falls back to default directory
security. A blind boolean here cost several blind CI round-trips.
win.sh: scp targets need native C:/ paths on Windows OpenSSH.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
|
||
|
|
dc72a5b639 |
style(cli): clang-format the private-staging diagnostic lines
The Phase 8 staging diagnostic added two long lines that clang-format-20 (the CI formatter) wraps differently. Reformat them so lint passes; no behavior change. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
0a8b77c3f2 |
fix(activation): stamp staged-file owner to the token user
activation_windows_security_init built the creation security descriptor with a protected current-user DACL but never set an owner, so a file created with these attributes inherited the token's default owner. Under an Administrators-default-owner policy -- standard on Windows Server and on GitHub's elevated Windows runners -- that default is BUILTIN\Administrators, not the calling user. Every staged file is then validated with the strict activation_windows_owner_is_current check, which rejected the Administrators-owned file and made private staging fail with "launcher/payload source pair did not remain runnable after private staging" (smoke Phase 8). Stamp the owner explicitly to the token user in the creation descriptor, the same fix the daemon side already applies. This keeps the strict owner check intact -- the staged file genuinely becomes current-user-owned -- so it is not a validation relaxation; setting the owner to our own token user needs no extra privilege. Complements the launcher's trusted-owner acceptance, which covers the installed binary that a non-admin user cannot re-stamp. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
9ef0f3168c |
diag(cli): name which private-staging sub-step made the pair unrunnable
cli_windows_prepare_install_pair folded launcher staging, payload staging, the payload fingerprint match, and the staged-launcher --version probe into one bool, so the caller could only report "launcher/payload source pair did not remain runnable after private staging" without saying which sub-step failed. Windows cbm_exec_no_shell does not inherit the child's stderr, so the launcher's own refusal reason never reaches the log either. Split the checks so each failing sub-step prints a distinct error, turning the opaque Phase 8 smoke failure into a precise signal on the next run. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
218438927e |
fix(cli): Windows managed install --dry-run states dry-run mode
The Windows managed-install path (cli_windows_managed_install_activate) printed its plan as "Would install managed launcher -> ..." but never the words "dry-run", unlike the POSIX path's "(dry-run — no files were modified)" - so `install --dry-run` did not clearly indicate dry-run mode (smoke Phase 6a, confirmed by echoing the captured output). Append the same dry-run indicator to the Windows plan line. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
30b5f12eef |
fix(cli): install --dry-run always reports dry-run, even on a plan check
A dry-run mutates nothing (every install mutation is guarded by !dry_run), but on Windows the activation plan's PATH/agent-config probe can return non-OK, and the caller then returned early WITHOUT the dry-run summary - so `install --dry-run` neither indicated dry-run mode nor exited success (smoke Phase 6a). For a dry-run, emit the "(dry-run — no files were modified)" indicator and return CLI_OK on that path (the plan simply noted something), and log the underlying status to stderr for triage. POSIX is unaffected (its dry-run activation returns OK, so the branch is never taken). Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
87a0e3f74f |
fix: preserve semantic graph relationships
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
5d8a9421fb |
Merge remote-tracking branch 'origin/main' into feat/shared-coordination-daemon
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> # Conflicts: # src/mcp/mcp.c # tests/test_cli.c |
||
|
|
89c7c98f00 |
fix: support standard Windows profile ACLs
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
cc5f71bdff |
fix(hook,smoke): teach the last two consumers the tree output shape
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>
|