Skip to content

Windows: index always fails with generic 'Pipeline failed' — protected-DACL cache dir breaks MoveFileExW(REPLACE_EXISTING) on this host #1620

Closed

Description

@roosteer

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:

  1. Since v0.9.1-rc.1, every CBM process start re-applies a protected, owner-only DACL to the cache directory (win_runtime_directory_secure in src/daemon/ipc.c, cbm_windows_stamp_dir_owner in src/foundation/compat_fs.c; re-applied unconditionally on each process start, not only at creation).
  2. 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 denied in 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.
  3. CBM publishes the index via rename-replace (publish_writer_output in src/sqlite_writer.c → cbm_rename_replace → MoveFileExW; also finalize_staged_generation). The failure is silent (CBM_PIPELINE_PERSIST_FAILED with no log), which produces the misleading generic hint.
  4. Proof: keeping the cache dir ACL in the healthy (inherited) state during a run (background loop re-applying icacls /inheritance:e while the pipeline executes) makes the exact same index succeed: status:"indexed", .db published.

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

Suggested fixes

  1. Fallback publish path when rename-replace fails (e.g. copy+delete, or write the final name directly) instead of silent PERSIST_FAILED.
  2. Make the DACL hardening opt-out (env var) for hosts where it conflicts with local security policy.
  3. 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

  1. DeusData commented on Aug 15, 2026

    @DeusData
    Owner

    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 .db is ever published — is the signature of the publish step failing, not the indexing. And you identified the mechanism: our own protected cache-directory DACL breaks MoveFileExW(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 errno with the Win32 error discarded, and one path that returned PERSIST_FAILED with 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 MoveFileEx publish 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_DIR for 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.

  2. DeusData commented on Aug 17, 2026

    @DeusData
    Owner

    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.

  3. roosteer commented on Sep 1, 2026

    @roosteer
    Author

    Additional sanitized reproduction on v0.10.8 from a Windows x64 host:

    • Both full and fast indexing fail through the MCP tool and the one-shot CLI with the exact generic Pipeline failed. Check repo_path exists... response.
    • The daemon records index.supervisor.reap outcome=clean exit_code=0, immediately followed by the index_repository request completing with status=error. No project database is published and list_projects remains empty.
    • The cache directory has a protected, owner-only DACL: one explicit FullControl rule and no inherited rules.
    • The failure reproduces for the repository root, a source-containing subtree, and a minimal directory containing only three source files with a short explicit project name. This makes repository scale, the derived project-name length, and a large-graph-only failure unlikely explanations.
    • As a diagnostic, inheritance was temporarily re-enabled repeatedly while indexing ran. This did not unblock publication. The original protected owner-only ACL was restored and verified afterward.

    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.

  4. roosteer commented on Sep 3, 2026

    @roosteer
    Author

    Additional sanitized evidence from the affected Windows x64 host, using v0.10.8.

    Reproduction

    The failure remains deterministic across:

    • MCP and the one-shot CLI;
    • fast and full modes;
    • the original source tree and a minimal repository containing one source file;
    • the default cache and a newly created cache.

    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 and list_projects remains 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=debug is active and produces the detailed indexing-pass events, but it still emits neither finalize.rename_failed nor finalize.sidecar_removal_failed.

    The v0.10.8 source explains why. gbuf.dump is emitted after cbm_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():

    if (cbm_rename_replace(w->temp_path, w->final_path) != 0) {
        (void)cbm_unlink(w->temp_path);
        return ERR_WRITE_FAILED;
    }

    https://github.com/DeusData/codebase-memory-mcp/blob/v0.10.8/internal/cbm/sqlite_writer.c#L1750-L1796

    That path returns ERR_WRITE_FAILED without 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:

    MoveFileExW(REPLACE_EXISTING | WRITE_THROUGH)
    

    Inside the owner-only CBM cache it returned:

    ERROR_ACCESS_DENIED (Win32 error 5)
    

    The result was unchanged when:

    • WRITE_THROUGH was removed;
    • the destination file did not already exist.

    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.
    • Stopping every daemon and using the one-shot CLI: same failure.
    • A fresh cache: same failure.
    • 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.
    • Repeatedly restoring inheritance while indexing: did not unblock publication.
    • A temporary-directory cache: correctly rejected because an ancestor grants mutation rights to an untrusted identity.
    • The workaround reported by @junk151516, setting both CBM_CACHE_DIR and CBM_RUNTIME_DIR under the user profile, is not portable to this host. The cache override is accepted, but the profile-based runtime override fails before startup with secure 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:

    1. On this host, MoveFileExW is denied specifically in the owner-only cache environment, including when no daemon or concurrent DACL rewrite is present.
    2. The failing inner writer-publication path is still silent, so the v0.10.8 diagnostics cannot expose the actual Win32 error to the user.

    Instrumenting publish_writer_output() around flush, commit, close, and its inner cbm_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.

  5. junk151516 commented on Sep 3, 2026

    @junk151516
    Contributor

    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 FullControl ACE 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:

    variant real cache cloned DACL
    REPLACE_EXISTING | WRITE_THROUGH, no destination OK OK
    REPLACE_EXISTING | WRITE_THROUGH, destination exists OK OK
    REPLACE_EXISTING only, destination exists OK OK

    And 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 MoveFileExW needs, 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:

    1. Process Monitor on the failing probe. Filter on the source path; the SetRenameInformationFile operation will show ACCESS 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.
    2. Retry the probe after a short delay. A file that another process holds open without FILE_SHARE_DELETE makes MoveFileEx fail with ERROR_ACCESS_DENIED, not ERROR_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_replace in src/foundation/compat_fs.c already translates GetLastError() into errno (its comment cites #1620 by name), and finalize.rename_failed at pipeline.c:1878 logs it — but that is the second rename (stage → final). The first rename (writer temp → stage) is the one in publish_writer_output() at sqlite_writer.c:1787, which returns ERR_WRITE_FAILED and drops the errno the foundation just filled in. A single cbm_log_error in 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 at pipeline.c:1690-1695 (dump_rc != 0 in cbm_pipeline_publish_generation); L1721-1726 is the store-open failure inside cbm_pipeline_publish_staged, a different path.

    Since this is the re-measurement asked for in #1649 (no daemon, the already_correct fast 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.c and ipc.c have no commits on main since v0.10.8, so on this path v0.10.8 is current main.

  6. added
    bugSomething isn't working
    stability/performanceServer crashes, OOM, hangs, high CPU/memory
    securitySecurity vulnerabilities, hardening
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    on Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.securitySecurity vulnerabilities, hardeningstability/performanceServer crashes, OOM, hangs, high CPU/memoryux/behaviorDisplay bugs, docs, adoption UXwindowsWindows-specific issues

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions