Skip to content

feat(daemon): let a product build relocate the runtime directory - #1576

Closed
tmonestudio wants to merge 5 commits into
DeusData:mainfrom
tmonestudio:fix/product-runtime-dir-override
Closed

feat(daemon): let a product build relocate the runtime directory#1576
tmonestudio wants to merge 5 commits into
DeusData:mainfrom
tmonestudio:fix/product-runtime-dir-override

Conversation

@tmonestudio

Copy link
Copy Markdown
Contributor

Closes the blocker reported in #1574.

Problem

The daemon/CLI rendezvous directory is created under %LOCALAPPDATA% (Windows)
or the POSIX runtime/home directory, and a product build has no way to point it
elsewhere. When that ancestry carries a mutation-granting ACE for an untrusted
identity, it fails cbm_daemon_ipc_private_directory_secure() and the binary
cannot start at all — config list included, since it needs the same endpoint,
so the operator cannot even inspect settings to diagnose it.

The only relocation hook, CBM_TEST_DAEMON_RUNTIME_PARENT, is compiled out
unless CBM_ENABLE_TEST_SEAMS is defined. That is why a test build starts on an
affected machine while the product build does not.

Approach

CBM_RUNTIME_DIR does not relax the check. The named directory goes through
exactly the same validation, and an unusable value is refused rather than
silently ignored. The operator only chooses an ancestry that passes.

Two details worth reviewing:

  • The test seam keeps precedence over the product override, so existing
    lifecycle tests keep their isolated rendezvous namespace.
  • The override is applied at every product endpoint call site — daemon,
    local CLI, tool execution and index worker. Wiring only
    main_daemon_endpoint_new() would have left the cli path on the default and
    looked fixed while it was not.

Evidence

Measured on the affected machine (Windows 11, single user), same environment,
same cache, only the patch differing:

binary config list
current main secure CLI coordination could not be created (endpoint)
this branch, CBM_RUNTIME_DIR set to a clean-ancestry parent starts and prints the configuration

Two non-obvious properties this surfaced, which may be worth documenting
regardless of this PR:

  • The directory must be created by the tool, not pre-created by the
    operator. A pre-created directory inherits the volume's ACEs and then fails as
    an ancestor; the tool hardens the final directory itself.
  • Consequently the override should name a parent whose own ancestry is clean —
    a volume root works.

Tests

tests/test_product_runtime_dir_override.py, a process-level guard in the style
of tests/test_daemon_open_readiness.py. It asserts that the rendezvous is
created under the named directory, that leaving the variable unset creates
nothing there, and that an unusable value is refused rather than ignored.

Honest disclosure about what I could and could not run locally:

  • The behavior above was verified by hand against a build of this branch.
  • The new guard could not go green on my machine, because its temporary
    directory lives under the very %LOCALAPPDATA% whose ACE causes the bug, so
    the parent it creates is itself rejected. On a clean runner it should pass;
    your CI will be the first to actually execute it.
  • I could not run the C suite locally at all: make -f Makefile.cbm test-focused
    fails to compile tests/test_daemon_ipc.c with
    -Werror=stringop-overflow under w64devkit GCC 2.8.0. I verified this is
    pre-existing and unrelated by building the same target on the unpatched
    base commit — identical failure, same line. Your CI uses LLVM 20, which does
    not raise it.

Happy to adjust the variable name, move the guard into the C suite, or rework
the approach entirely if you would rather solve this differently.

A live checkout SHA is not proof of the generation that produced graph
content, so the freshness verdict must come from the indexed-checkout
identity recorded with the DB. Legacy DBs record no such identity: verbose
index_status now exposes the graph generation separately (indexed_generation)
and fails closed with verdict=unknown, indexed_checkout_sha=null and
reason=indexed_checkout_unavailable.

The default status call stays lean and the freshness block is report-only —
it never triggers indexing. Add focused coverage for the fail-closed verdict
and for its omission from the default output.

Signed-off-by: Leonardo trindade miranda <tmonestudio@gmail.com>
Record the frozen git HEAD at the same successful staged-generation boundary
as graph coverage. cbm_coverage_meta_t gains indexed_checkout_sha; new
writable DBs create a nullable indexed_checkout_sha column and existing
writable DBs are migrated idempotently in init_schema, while read-only
legacy DBs without the column still open, read and report no identity
instead of erroring. coverage_replace_ex writes/updates the SHA inside its
existing transaction, so failure/rollback retains the prior identity.

Full, delta-incremental and legacy-incremental generation templates set it
from the pipeline's refreshed git context (NULL/empty for non-git repos).

index_status freshness now derives from the recorded identity: no indexed
SHA -> unknown/indexed_checkout_unavailable; differs from the live git HEAD
-> stale/indexed_checkout_mismatch; equal -> current. Emits indexed_generation,
indexed_checkout_sha, checkout_sha, a stable reasons array and
recommended_action; stays verbose-only and read-only.

Add store round-trip/rollback coverage and MCP verdict tests for matching
current and mismatched stale (git fixtures, platform-skipped on Windows).

Signed-off-by: Leonardo trindade miranda <tmonestudio@gmail.com>
Add cbm_git_worktree_status: parse git status --porcelain=v1 -z
--untracked-files=all record-by-record over a bounded chunk buffer
(4K chunks, 16K field cap). Counts advance for the whole stream while
only max_samples paths per class (tracked/untracked) are retained;
exceeding the cap sets *_truncated. Rename/copy records consume their
second NUL-separated source path without counting it, so one rename
counts once. available is true ONLY when git exited 0 and every record
parsed cleanly; any shell-unsafe path, spawn failure, nonzero exit,
malformed/oversized/trailing record leaves available=false with zero
counts - callers must never read that as a clean worktree.

Index the snapshot into the verbose freshness block (status_available,
tracked_changes{count,paths,truncated}, untracked_source{...}). Verdict
composition: clean matching SHA -> current; tracked changes or SHA
mismatch -> stale; untracked-only, unavailable status or missing live
HEAD -> unknown. Reasons array keeps every applicable code in stable
order with the dominant reason first; recommended_action derives from
the verdict. Report-only, verbose-only, read-only: no indexing or
mutation is ever triggered.

Owner gates corrected: restored missing #endif around canonical_root
tests; rename/copy detection checks both XY columns; introduced
cbm_pipeline_indexed_checkout_sha accessor because cbm_pipeline_t is
opaque in pipeline_incremental.c (HEAD accessed git_ctx.head_sha
directly and could not compile); Windows tests force git add -f so a
user's global excludes cannot silently drop the tracked fixture;
rollback test now mutates row detail so the shadow rebuild actually
executes before the replace is rolled back.

Tests now shell out through cbm_popen (isolated spawn) instead of
system(), so the git-backed freshness/status tests run on Windows too;
only a genuinely missing git skips. Prior SHA persistence (staged-
generation boundary) is the anchor the new worktree status refines.

Signed-off-by: Leonardo trindade miranda <tmonestudio@gmail.com>
Signed-off-by: Leonardo trindade miranda <tmonestudio@gmail.com>
The daemon/CLI rendezvous is created under %LOCALAPPDATA% (Windows) or the
POSIX runtime/home directory, resolved with no override in a product build.
When that ancestry carries a mutation-granting ACE for an untrusted identity --
an AppContainer capability SID, for instance -- it fails
cbm_daemon_ipc_private_directory_secure() and the binary cannot start at all.
config list is unreachable too, because it needs the same endpoint, so the
operator cannot even inspect settings to diagnose it.

The only relocation hook, CBM_TEST_DAEMON_RUNTIME_PARENT, is compiled out
unless CBM_ENABLE_TEST_SEAMS is defined, so a test build starts while the
product build does not.

CBM_RUNTIME_DIR does not relax the check: the named directory goes through
exactly the same validation, and an unusable value is refused rather than
ignored. The operator only chooses an ancestry that passes. The test seam keeps
precedence so existing lifecycle tests keep their isolation, and the override
is applied at every product endpoint call site -- daemon, local CLI, tool
execution and index worker -- so the CLI path cannot silently keep the default.

Refs DeusData#1574

Signed-off-by: Leonardo trindade miranda <tmonestudio@gmail.com>
@tmonestudio
tmonestudio requested a review from DeusData as a code owner August 12, 2026 21:18
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@DeusData DeusData added bug Something isn't working enhancement New feature or request security Security vulnerabilities, hardening editor/integration Editor compatibility and CLI integration windows Windows-specific issues priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. labels Aug 12, 2026
@DeusData

Copy link
Copy Markdown
Owner

Thank you @tmonestudio for turning #1574 into a fail-closed proposal rather than weakening the ancestry check. The issue is grounded on current main, and the runtime-parent direction is reviewable.

Before substantive review, this branch needs to be unstacked. Its first four commits (36d467dc, e49f7e57, 86fca027, 167783bb) are the complete #1561 index-freshness series; only 038194e7 is the runtime-directory change. As a result, GitHub currently shows 13 files and +1,482/-26 here, including store, git-context, and MCP changes unrelated to the stated claim.

Please rebase/cherry-pick the runtime commit onto current main so this PR contains one change, then let CI rerun on that standalone diff. The current lint and Windows smoke failures should be evaluated on that clean branch. The review queue is full, and this security-sensitive path will need careful review, but the contribution has been seen and is queued.

pull Bot pushed a commit to dongdonga11/codebase-memory-mcp that referenced this pull request Aug 15, 2026
…IME_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

DeusData#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". DeusData#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 DeusData#1576 by Leonardo trindade miranda, resolved one
layer lower so the activation path is covered too.

Refs DeusData#1574
Refs DeusData#1621

Co-Authored-By: Leonardo trindade miranda <tmonestudio@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you for this — your idea shipped, and you are named in the commit.

CBM_RUNTIME_DIR is on main as 7f3e30e (PR #1645), and the message credits you directly:

Approach and variable name from #1576 by Leonardo trindade miranda, resolved one layer lower so the activation path is covered too.

The one change is where it is resolved. Yours sat in the daemon path; the merged version resolves it in bootstrap_runtime_parent_override, the single function every product endpoint goes through — daemon, MCP client, local CLI, index worker and activation — so no call site can silently keep the default. That matters here because the failure it fixes hits install and config list too, not only the daemon.

It keeps your semantics exactly: it does not relax the check. The directory you name goes through the same validation as the default, and a value that fails is refused rather than ignored — the operator chooses an ancestry that passes, they do not get to switch the rule off.

It ships in v0.10.5 today, and it is the escape hatch several people have needed for weeks — #1537, #1574 and #1621 all reduce to "the rendezvous is not relocatable in a shipped build".

Two things I'm sorry about: this PR stayed open and unanswered while its idea went into the product, and your #1561 is still open and blocked on nothing but clang-format (src/store/store.c:3171-3172, src/mcp/mcp.c:4504-4506). If you run make lint-ci and push, that one should go green — it is not superseded by anything and I'd like to get it in.

@DeusData

Copy link
Copy Markdown
Owner

Closing as shipped: CBM_RUNTIME_DIR is on main in 7f3e30e (#1645), crediting you by name, resolved one layer lower so install and config list are covered too. It ships in v0.10.5 today. Your #1561 is still open and blocked on nothing but clang-format — I'd like to get that one in. Thank you.

@DeusData DeusData closed this Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working editor/integration Editor compatibility and CLI integration enhancement New feature or request priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. security Security vulnerabilities, hardening windows Windows-specific issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants