Repository navigation
Windows: index always fails with generic 'Pipeline failed' — protected-DACL cache dir breaks MoveFileExW(REPLACE_EXISTING) on this host #1620
Description
Summary
On this Windows host, every index_repository run fails with the generic hint:
{"project":"...","status":"error","hint":"Pipeline failed. Check repo_path exists and contains source files. Try mode='fast' for a quicker diagnostic run."}The pipeline itself runs all passes successfully (including gbuf.dump), the supervised worker exits 0 (index.supervisor.reap outcome=clean), no error-level log line is emitted anywhere, and no .db is ever published. A 1-file Go repo fails identically, so it is host-specific, not repo-specific. ui.config.write_fail ... reason=atomic_publish appears repeatedly in the daemon log as well.
Root cause (verified end-to-end)
The failure is an interaction between the Windows cache-DACL hardening and MoveFileExW on this host:
- Since v0.9.1-rc.1, every CBM process start re-applies a protected, owner-only DACL to the cache directory (
win_runtime_directory_secureinsrc/daemon/ipc.c,cbm_windows_stamp_dir_ownerinsrc/foundation/compat_fs.c; re-applied unconditionally on each process start, not only at creation). - On this machine,
MoveFileExW(src, dst, MOVEFILE_REPLACE_EXISTING)returns ERROR_ACCESS_DENIED (5) inside any directory whose DACL is protected (inheritance disabled), while plain rename, create and delete all work. Reproduced independently of CBM:cmd /c "move /y r2.txt r1.txt"→Access is deniedin the protected dir, works in%TEMP%and in sibling dirs with inherited ACLs.- Synthetic dirs:
icacls /inheritance:r+ single owner ACE → fails;icacls /inheritance:e→ works.
- CBM publishes the index via rename-replace (
publish_writer_outputinsrc/sqlite_writer.c→cbm_rename_replace→MoveFileExW; alsofinalize_staged_generation). The failure is silent (CBM_PIPELINE_PERSIST_FAILEDwith no log), which produces the misleading generic hint. - Proof: keeping the cache dir ACL in the healthy (inherited) state during a run (background loop re-applying
icacls /inheritance:ewhile the pipeline executes) makes the exact same index succeed:status:"indexed",.dbpublished.
So on this host the hardening itself makes the cache dir unusable for the rename-replace the pipeline depends on. No config switch exists to disable it (a non-conforming DACL is re-hardened; denying WRITE_DAC fails the daemon closed; UNC paths are refused; CBM_CACHE_DIR elsewhere gets hardened the same way).
Environment
| Version | 0.10.4 |
| OS | Windows (domain-joined, UAC enabled, user in Administrators) |
| Cache dir | C:\Users\...\.cache\codebase-memory-mcp (default) |
Related
- Windows x64: v0.9.1-rc.1 daemon leaves existing cache files unreadable (empty ACL) #1351, fix(daemon): make private directory DACL inheritable #1531 — the DACL hardening chain this builds on
- 0.10.2: cache ACL grants an ephemeral logon-session SID (S-1-5-5-X-Y) instead of the user SID, so the cache becomes unreadable after reboot (likely incomplete fix for #1351) #1601 — another symptom of the same hardening code (ephemeral logon-session SID in the cache ACL)
Suggested fixes
- Fallback publish path when rename-replace fails (e.g. copy+delete, or write the final name directly) instead of silent
PERSIST_FAILED. - Make the DACL hardening opt-out (env var) for hosts where it conflicts with local security policy.
- At minimum: log the real OS error (which operation failed with which code) instead of the generic "Pipeline failed" hint — the current silence cost a full day of investigation.
Activity
fix(windows): re-stamp the runtime DACL only when it is actually wrong
DeusData commentedon Aug 15, 2026
Thank you for this, and I'm sorry: you root-caused a blocker, wrote the fix, and this issue itself has had no reply at all until now. That is not the reception that work deserved.
Your diagnosis holds up. The chain you described — the pipeline runs every pass, the supervised worker exits 0, and no
.dbis ever published — is the signature of the publish step failing, not the indexing. And you identified the mechanism: our own protected cache-directory DACL breaksMoveFileExW(REPLACE_EXISTING)on that host.Where things stand, honestly:
Not fixed in v0.10.5. I want to say that plainly rather than let you upgrade and re-hit it. What is in v0.10.5 is the diagnosis half — the publish failure no longer reports a meaningless
errnowith the Win32 error discarded, and one path that returnedPERSIST_FAILEDwith no log line at all now says what happened. That is why you were told to "check repo_path exists" for what was actually an ACL problem. It will not fix your host, but it should stop misdirecting the next person.Two changes are open against this. #1628 covers the reporting, and your own #1649 is the configurability piece. There is also #1634, which stops us re-stamping the runtime DACL on every process start — 11 "Security change" USN records per day on your machine, and a window a
MoveFileExpublish can lose to. I pulled #1634 from v0.10.5 because it broke daemon startup in testing: I had conflated "grants nothing untrusted" with "protected against inheritance", and shipping it half-right would have traded your bug for a worse one.On #1649 specifically — it is a real decision rather than a review backlog, which is why it has not simply been merged. An env-var opt-out for the DACL hardening weakens a boundary that protects the coordination endpoint for everyone, so we would rather first exhaust "give the operator a location that legitimately passes". v0.10.5 ships
CBM_RUNTIME_DIRfor exactly that: it relocates the rendezvous directory without relaxing any check — you choose an ancestry that passes, rather than switching the rule off. It would genuinely help to know whether that unblocks your host, because if there is no reachable location that passes on a normal Windows machine, that is the argument for your flag, and your report becomes the evidence for it. The maintainer decision on the opt-out is pending and I am not going to pre-empt it.Thank you again — for the report, for chasing it to the actual Win32 call, and for writing the patch. Two of the three open changes here exist because of your work.
fix(windows): report why an atomic publish failed instead of blaming …
DeusData commentedon Aug 17, 2026
Fixed on main in c1a5de3 (PR #1685), shipping in v0.10.6: the per-start DACL re-stamp no longer fires when the runtime directory is already correct, which removes the re-protect window your MoveFileExW(REPLACE_EXISTING) publish kept losing to (the destination's DELETE grant was being yanked mid-rename by a rewrite that changed nothing). The dead-path repair contract stays: a genuinely wrong descriptor is still repaired exactly as before. The #1649 opt-out proposal remains open as a separate policy question, but on this class the hardening simply stopped being in your way. Please re-test on v0.10.6.
fix(windows): conditional DACL re-stamp + damaged-children repair
roosteer commentedon Sep 1, 2026
Additional sanitized reproduction on v0.10.8 from a Windows x64 host:
fullandfastindexing fail through the MCP tool and the one-shot CLI with the exact genericPipeline failed. Check repo_path exists...response.index.supervisor.reap outcome=clean exit_code=0, immediately followed by theindex_repositoryrequest completing withstatus=error. No project database is published andlist_projectsremains empty.This has the same post-worker/publish signature documented here, despite running a version that includes the v0.10.6 fix. It may be an unresolved variant of the protected-DACL/Windows publish interaction rather than the exact per-start re-stamp race fixed by #1685.
roosteer commentedon Sep 3, 2026
Additional sanitized evidence from the affected Windows x64 host, using v0.10.8.
Reproduction
The failure remains deterministic across:
fastandfullmodes;The supervised worker completes every indexing pass, emits
gbuf.dump, exits normally with code 0, and the request returns the generic pipeline failure. No project database is published andlist_projectsremains empty.Stopping every CBM process and repeating the test with a fresh one-shot daemon produced the same result, so this is not caused by a stale daemon or an OpenCode/MCP transport problem.
Why debug logging still does not identify the failure
CBM_LOG_LEVEL=debugis active and produces the detailed indexing-pass events, but it still emits neitherfinalize.rename_failednorfinalize.sidecar_removal_failed.The v0.10.8 source explains why.
gbuf.dumpis emitted aftercbm_writer_finalize()regardless of the writer return code:https://github.com/DeusData/codebase-memory-mcp/blob/v0.10.8/src/graph_buffer/graph_buffer.c#L1745-L1759
If that writer result is unsuccessful, the pipeline discards the stage and returns before
cbm_pipeline_finalize_staged_generation()is called:https://github.com/DeusData/codebase-memory-mcp/blob/v0.10.8/src/pipeline/pipeline.c#L1721-L1726
The narrowest source path consistent with the observed events is the inner atomic publication in
publish_writer_output():https://github.com/DeusData/codebase-memory-mcp/blob/v0.10.8/internal/cbm/sqlite_writer.c#L1750-L1796
That path returns
ERR_WRITE_FAILEDwithout logging the translated Win32 error. The diagnostics added by #1628 cover the later outer finalization, which is never reached in this case.Native filesystem discriminator
A standalone probe called the same Windows operation used by CBM:
Inside the owner-only CBM cache it returned:
The result was unchanged when:
WRITE_THROUGHwas removed;The same API call succeeded in a normal working directory. This confines the failure to the atomic move operation in the owner-only cache environment. It does not establish which specific managed-host policy or security component produces the denial.
Workarounds tested
CBM_LOG_LEVEL=debug: provides pass-level logs but not the failing writer error.CBM_CACHE_DIR=<managed-user-profile>\.codebase-memory-mcp: override is applied, but CBM gives the new cache the same owner-only DACL and publication still fails.CBM_CACHE_DIRandCBM_RUNTIME_DIRunder the user profile, is not portable to this host. The cache override is accepted, but the profile-based runtime override fails before startup withsecure daemon endpoint could not be created. This matches the caveat in that report about managed profiles:feat(config): make Windows cache-dir DACL hardening configurable (env var + CLI config key) #1649 (comment)
Conclusion
The new evidence confirms two separate gaps:
MoveFileExWis denied specifically in the owner-only cache environment, including when no daemon or concurrent DACL rewrite is present.Instrumenting
publish_writer_output()around flush, commit, close, and its innercbm_rename_replace()would distinguish the exact writer failure without relying on the outer finalizer.All account names, domain names, SIDs, PIDs, cache fingerprints, and real filesystem/repository paths have been omitted.
junk151516 commentedon Sep 3, 2026
Control data point from a host where the same publish path works, offered because it isolates the variable your probe could not: the DACL shape is not what denies the rename.
Host: Windows 11 Pro 10.0.26200 x64, workgroup (not domain-joined, no MDM), v0.10.8, default cache under the profile. The cache directory carries exactly the descriptor you describe — owner = the user, protected (no inheritance), one explicit
FullControlACE for the owner, files inheriting(I)(F).Same standalone probe, three variants, two locations (the real CBM cache, and a fresh directory stamped with an identical protected owner-only DACL via
icacls /inheritance:r /grant:r), run with no CBM process involved:REPLACE_EXISTING | WRITE_THROUGH, no destinationREPLACE_EXISTING | WRITE_THROUGH, destination existsREPLACE_EXISTINGonly, destination existsAnd the real publish works on this host: a 61k-node / 380k-edge reindex published cleanly on 23 Aug, and the store survived a reboot this morning (31 files, zero empty DACLs,
list_projects= 10) — which, for what it is worth, is the post-reboot confirmation #1601 was closed without.So on an unmanaged host the owner-only protected DACL grants everything
MoveFileExWneeds, including with no daemon around to re-stamp. Your host denies the same call under the same descriptor. That leaves the managed-host layer, and two cheap discriminators may name it:SetRenameInformationFileoperation will showACCESS DENIED, and the stack of that event shows whether the denial comes from NTFS (a real ACL check) or from a minifilter above it (Defender / EDR / anti-ransomware "protected folders"). That answers "which security component" directly.FILE_SHARE_DELETEmakesMoveFileExfail withERROR_ACCESS_DENIED, notERROR_SHARING_VIOLATION— an on-access scanner holding a freshly written file looks exactly like an ACL denial. If a 2-second sleep before the move turns the failure into success, it is a handle, not a policy.Your reading of the source holds, with one anchor to fix.
cbm_rename_replaceinsrc/foundation/compat_fs.calready translatesGetLastError()intoerrno(its comment cites #1620 by name), andfinalize.rename_failedatpipeline.c:1878logs it — but that is the second rename (stage → final). The first rename (writer temp → stage) is the one inpublish_writer_output()atsqlite_writer.c:1787, which returnsERR_WRITE_FAILEDand drops theerrnothe foundation just filled in. A singlecbm_log_errorin that branch, mirroring the pipeline one, would expose the Win32 error on your host without touching the outer finalizer. The pipeline early-return you mean is atpipeline.c:1690-1695(dump_rc != 0incbm_pipeline_publish_generation); L1721-1726 is the store-open failure insidecbm_pipeline_publish_staged, a different path.Since this is the re-measurement asked for in #1649 (no daemon, the
already_correctfast path present, still deterministic), it may be worth a pointer there so it is not missed on a closed issue. For that comparison:compat_fs.c,sqlite_writer.candipc.chave no commits onmainsince v0.10.8, so on this path v0.10.8 is currentmain.fix(graph-buffer): log why an atomic publish failed instead of a succ…
fix(graph-buffer): log why an atomic publish failed instead of a succ…
fix(graph-buffer): log why an atomic publish failed instead of a succ…
fix(graph-buffer): log why an atomic publish failed instead of a succ…
fix(graph-buffer): log why an atomic publish failed instead of a succ…
fix(graph-buffer): log why an atomic publish failed instead of a succ…