mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-06 08:35:21 +08:00
1130f577ee27385ca2e5449e9964b7221ee95ec8
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d61811d27e |
test(watcher): process-level regression for watcher_enabled + startup-read docs (#335)
Follow-up to the PR #1105 review: the unit test proves the config predicate but would still pass even if the daemon-side gate were removed. Add a process-level regression that drives the real binary and proves the gate actually prevents the watcher from being built, started or registered when disabled. - tests/test_watcher_disabled.sh (wired into scripts/test.sh as Step 5e) drives the binary against an isolated CBM_CACHE_DIR + tiny git fixture. With watcher_enabled=false it asserts: watcher.disabled reason=config emitted, watcher.start absent, no watcher.watch registration, auto_index still runs, and manual index_repository is served. A watcher_enabled=true positive control asserts watcher.start + watcher.watch appear, so the test fails if the gate is removed (verified by neutering the gate in host_state_prepare and observing the failure). No assertion is decided by a timeout. The watcher lives in the daemon, which logs to $CBM_CACHE_DIR/logs/cbm-daemon.log in a fixed order: watcher.disabled (host_state_prepare) -> daemon.start -> the thread's own watcher.start -> the join -> daemon.stop. Each run therefore retires its daemon and waits for that CLOSED lifecycle before asserting, so absence is a fact rather than a race. Every wait is a bounded poll on an asserted state following the soak harness's wait_for_daemon_stop doctrine, and an exhausted budget fails loudly with the reason instead of passing quietly; session stdin is held open through a FIFO until the last JSON-RPC response lands, not for an interval. Registration is asynchronous and unreachable until the project DB exists (application_refresh_watch_locked requires application_regular_db_exists), so a closed lifecycle alone would not carry the negative — the session could simply have ended first. The disabled+auto_index run waits for that DB, i.e. registration's own precondition; the enabled control waits for watcher.watch itself, so its absence in the disabled runs is evidence rather than noise. Honors CBM_TEST_BINARY like tests/test_worker_watchdog.sh, so the BUILD_DIR= sanitizer and container legs test the binary they built. - docs: CONFIGURATION.md + README.md document that watcher_enabled is read once when the daemon starts — the daemon outlives individual MCP sessions, so reconnecting a client is not enough; `daemon stop` retires it. This corrects the earlier claim that auto_watch is also startup-read: auto_watch is consulted per registration, and the two keys are now contrasted on read time as well as on scope (session registration vs whether the subsystem starts at all). No production code change in this commit: watcher.start and watcher.watch already exist as production logs. Signed-off-by: soren <nguiasoren@gmail.com> |
||
|
|
680bc72e75 |
feat(config): add watcher_enabled master switch for the background watcher (#335)
Add a persistent `watcher_enabled` config key (default true). When false, the daemon host skips building the background watcher entirely: the poll thread never starts and no projects are registered. Manual index_repository is unaffected, and the config wiring stays unconditional so auto_index / auto_watch keep working even when the watcher is off. This is distinct from auto_watch, which only gates per-session registration while the watcher IS running. watcher_enabled is the master switch that stops the subsystem from starting at all — the request in #335 (doc-heavy repos where every save churns the graph baseline). The key is read once, when the daemon builds its host state. Changing it takes effect when the daemon next starts; docs/CONFIGURATION.md says so explicitly. A disabled watcher is not a daemon-startup failure — the daemon still comes up and still owns IPC, HTTP/UI, project locks, and manual indexing; only the watcher and the in-memory store backing it go unbuilt. - cli.h/cli.c: CBM_CONFIG_WATCHER_ENABLED key + cbm_config_watcher_enabled() predicate (NULL-safe, default true), registered as a CONFIG_KEYS row next to auto_watch. The table is no longer only a print list: since #1522/#1558 it also backs key validation, so `config set/get/reset watcher_enabled` would be rejected as an unknown key without that row, and the default recorded there (true) is the one the table comment asks to keep in sync with the runtime reader's fallback. - daemon/host.c: gate watcher construction in host_state_prepare() and the thread create in host_background_start(); log watcher.disabled when skipped. Teardown needed no change — stop NULL-checks, join is gated on watcher_started, and cbm_watcher_free is NULL-safe. - test_cli.c: cli_config_watcher_enabled_default_and_persist pins the config layer — default on, disable/re-enable spellings, persistence across reopen, and a NULL-config default (a config-store failure must never silently disable the watcher). - docs: document watcher_enabled, and add the previously-undocumented auto_watch row to CONFIGURATION.md so the watcher knobs are described together; README example. Closes #335. Signed-off-by: soren <nguiasoren@gmail.com> |
||
|
|
7f3e30e1e1 |
fix(daemon): let a shipped build relocate the rendezvous via CBM_RUNTIME_DIR
The daemon/CLI rendezvous directory is created under %LOCALAPPDATA% (Windows) or
/tmp -- /private/tmp on macOS -- and every ancestor of it must pass the
private-directory walk. That ancestry is not always acceptable, and when it is
not, EVERY invocation fails, `config list` included, so the settings surface
cannot be reached either:
codebase-memory-mcp: secure daemon endpoint could not be created
#1623 narrowed the Windows side of this by admitting AppContainer package and
capability SIDs on ancestors, and named the remainder explicitly: a live local
group, Authenticated Users inherited from a secondary volume root, and orphaned
unresolvable SIDs still refuse, and "those need CBM_RUNTIME_DIR or a separate
change". #1621 is the POSIX shape of the same dead end -- /private/tmp/cbm-daemon-<uid>
refused with no way to move it.
There was no way to move it in a shipped build. The only relocation hook,
CBM_TEST_DAEMON_RUNTIME_PARENT, is compiled out unless CBM_ENABLE_TEST_SEAMS is
defined, so a test build started while the shipped build did not; CBM_CACHE_DIR
is no help either, because it moves the cache and never the rendezvous.
CBM_RUNTIME_DIR names the parent directory the rendezvous is created under. It
does NOT relax the check: the directory it names goes through exactly the same
validation as the default -- ancestors owned by you or root, not world-writable,
no allow-ACL; the rendezvous directory itself still forced to owner-only -- and a
value that fails is refused rather than silently replaced by the default. The
operator only chooses an ancestry that passes. cbm_safe_getenv never truncates,
so no half of an over-long value can become a runtime parent.
The override is resolved in cbm_daemon_bootstrap_endpoint_new(), the one function
every product endpoint goes through: the daemon, the MCP client, the local CLI,
the index worker, and the install/update/uninstall activation path in cli.c. No
call site can silently keep the default, and the detached daemon inherits the
value with the rest of its environment. An explicit parent still wins, so the
compile-time test seam and the lifecycle guards' isolated namespace behave
exactly as before.
Approach and variable name from #1576 by Leonardo trindade miranda, resolved one
layer lower so the activation path is covered too.
Refs #1574
Refs #1621
Co-Authored-By: Leonardo trindade miranda <tmonestudio@gmail.com>
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> |
||
|
|
7fa5b07718 |
feat: enforce one workspace boundary on every indexing entry point
Routes the MCP index_repository handler and the graph UI's POST /api/index through a single decision function. The UI route previously checked only that root_path was a directory, so an operator's configured boundary held on one entry point and not the other; it now canonicalizes first and applies the same policy, answering 403 with the reason. The decision is two-tier, because a bare default-deny would refuse every first run and a bare opt-in leaves the default open: - Breadth is always enforced, with nothing configured. Filesystem, drive and share roots, top-level system trees, the home directory itself and credential directories are refused as indexing roots out of the box. - Containment in a declared root applies once CBM_ALLOWED_ROOT is set or a grant exists, and is evaluated first so a path outside a configured root is reported as exactly that. Three things the tests caught, each a real defect rather than a test fix: - On macOS /etc, /tmp and /var are firmlinked under /private, so canonicalizing "/etc" yields "/private/etc" and counted two deep — sailing past a minimum of two, missing the very path being guarded. Depth now discounts a leading "private" component. - An earlier draft refused any root containing the cache directory. That was over-claimed: the indexer only parses recognised source files and a graph database is binary SQLite it would never extract. Refusing a whole root is also the wrong remedy where the concern does hold — not walking the cache is. Removed, with the reasoning recorded at the site. - Rewording the refusal to "outside every allowed root" broke an assertion matching "outside the allowed root", and that test's early return skipped its CBM_ALLOWED_ROOT cleanup, leaking the variable into every later test in the suite. The original wording is kept and guidance appended instead. Worth remembering: these contracts match strings, not properties. Docs updated in the same change, since both env-var tables said "unset imposes no restriction" and that is no longer true: CONFIGURATION.md and README.md describe the two tiers, and CONFIGURATION.md lists the always- refused roots along with the two limits that matter — this constrains scope rather than sensitivity, and the credential list is a denylist that raises the cost of a mistake rather than closing the class. SECURITY.md's supported- versions table was still on 0.8.x and now reads 0.9.x. 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> |
||
|
|
83c137d2a5 |
feat: complete shared daemon lifecycle
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
cc1608210d |
docs: document the CBM_ALLOWED_ROOT indexing boundary
Add CBM_ALLOWED_ROOT to the environment-variable tables in README.md and docs/CONFIGURATION.md: when set, index_repository refuses a repo_path that resolves outside it; unset imposes no restriction. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> |
||
|
|
e7182cd3fe |
docs(config): add configuration reference for existing config files
Signed-off-by: Andrew Barnes <bortstheboat@gmail.com> |