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>
This commit is contained in:
Martin Vogel
2026-08-14 20:04:08 +02:00
parent b9cde7e025
commit 7f3e30e1e1
3 changed files with 156 additions and 3 deletions
+38
View File
@@ -121,8 +121,46 @@ These environment variables affect runtime behavior:
| `CBM_DIAGNOSTICS` | `false` | Enable periodic `snapshot.json` and retained `trajectory.ndjson` below a fresh owner-private directory in the system temp directory. The daemon records the randomized paths in the `diagnostics.start` discovery record (a single JSON line) in `${CBM_CACHE_DIR}/logs/cbm-daemon.log`; that one record is emitted even when `CBM_LOG_LEVEL` suppresses ordinary logging, so the paths always remain discoverable. |
| `CBM_DOWNLOAD_URL` | GitHub releases | Override the update download URL. |
| `CBM_LOG_LEVEL` | `info` | Set the log level to `debug`, `info`, `warn`, `error`, or `none` (or `0`-`4`). Thin-frontend messages use that session's stderr; detached daemon events use `${CBM_CACHE_DIR}/logs/cbm-daemon.log`. |
| `CBM_RUNTIME_DIR` | `%LOCALAPPDATA%` (Windows), `/private/tmp` (macOS), `/tmp` (other) | Parent directory for the daemon/CLI rendezvous directory, which CBM creates inside it as `cbm-daemon-<uid>` (`cbm-daemon-<key>` on Windows). Set it when the default ancestry cannot pass the private-directory check — see below. `CBM_CACHE_DIR` does **not** move the rendezvous. |
| `CBM_WORKERS` | auto-detected | Override the indexing worker count. |
### Relocating the daemon rendezvous directory
Before it is used, the rendezvous directory and every ancestor of it are checked:
each ancestor must be owned by you or by root, must not be world-writable (unless
it is the standard root-owned sticky directory such as `/tmp`), and must carry no
allow-ACL — on Windows, no ACE granting mutation rights to another identity. The
rendezvous directory itself is then forced to owner-only (`0700`, no extended ACL
/ an owner-only DACL).
That ancestry is not always acceptable in the default location. A Windows profile
that has acquired a capability-SID ACE with `WRITE_DAC` / `WRITE_OWNER` / `DELETE`
on `%LOCALAPPDATA%` — something an installed packaged app can add — fails the walk,
and so can an unusual `/tmp` or home directory on POSIX. When that happens *every*
command fails, `config list` included, so the settings surface cannot be reached
either:
```text
codebase-memory-mcp: secure daemon endpoint could not be created
```
`CBM_RUNTIME_DIR` points the rendezvous at an ancestry you choose:
```bash
export CBM_RUNTIME_DIR="$HOME/cbm-runtime" # any directory you own
```
```powershell
$env:CBM_RUNTIME_DIR = "D:\cbm-runtime"
```
The check is not relaxed for the directory you name: it goes through exactly the
same validation as the default, and a value that fails it is refused rather than
silently ignored. Because the rendezvous is how sessions find each other, every
process that should share one daemon must see the same value — set it in the
environment of your MCP client and your shell alike, or a CLI invocation without
it will coordinate through the default location instead.
Environment used by daemon-owned components—such as diagnostics, daemon logging, and process-wide indexing resource limits—is captured from the first daemon-backed session that starts the daemon. Later sessions join the existing process and cannot replace those values. To change them, close every daemon-backed session, update the relevant agent configurations consistently, and restart a session. `CBM_ALLOWED_ROOT` remains session-specific, a conflicting `CBM_CACHE_DIR` is rejected, and one-shot CLI commands use their own current environment without starting the daemon.
+32 -1
View File
@@ -208,12 +208,43 @@ bool cbm_daemon_process_role_requires_client(cbm_daemon_process_role_t role) {
return role == CBM_DAEMON_PROCESS_MCP_CLIENT || role == CBM_DAEMON_PROCESS_HOOK_CLIENT;
}
/* #1574/#1621: the rendezvous directory is created under %LOCALAPPDATA%
* (Windows) or /tmp — /private/tmp on macOS — and that ancestry is not always
* acceptable to the private-directory walk. A profile that carries a
* mutation-granting ACE for an untrusted identity (an AppContainer capability
* SID, for instance) fails it, and the binary then cannot start at all: every
* command needs this endpoint, `config list` included, so the operator cannot
* even reconfigure their way out. The only relocation hook was
* CBM_TEST_DAEMON_RUNTIME_PARENT, compiled out unless CBM_ENABLE_TEST_SEAMS is
* defined, so a test build started while the shipped build did not. CBM_CACHE_DIR
* does not help either — it moves the cache, never the rendezvous.
*
* CBM_RUNTIME_DIR does NOT relax the check. The directory it names goes through
* exactly the same validation as the default; the operator only chooses an
* ancestry that passes, and a value that fails is refused rather than ignored.
* cbm_safe_getenv never truncates: a value too long for the buffer is reported
* as absent, so no half a path can ever become a runtime parent. */
static const char *bootstrap_runtime_parent_override(char *buffer, size_t capacity) {
const char *value = cbm_safe_getenv("CBM_RUNTIME_DIR", buffer, capacity, NULL);
return value && value[0] != '\0' ? value : NULL;
}
cbm_daemon_ipc_endpoint_t *cbm_daemon_bootstrap_endpoint_new(const char *runtime_parent) {
char key[CBM_DAEMON_KEY_SIZE];
if (!cbm_daemon_rendezvous_key(key)) {
return NULL;
}
return cbm_daemon_ipc_endpoint_new(key, runtime_parent);
/* An explicit parent keeps precedence: it carries the compile-time test
* seam and the lifecycle guards' isolated namespace. The override is
* resolved HERE, the one function every product endpoint goes through
* (daemon, MCP client, local CLI, index worker, activation), so no call
* site can silently keep the default. */
char override_parent[BOOTSTRAP_PATH_CAP];
const char *parent =
runtime_parent
? runtime_parent
: bootstrap_runtime_parent_override(override_parent, sizeof(override_parent));
return cbm_daemon_ipc_endpoint_new(key, parent);
}
bool cbm_daemon_bootstrap_launch_spec_init(const char *executable_path,
+86 -2
View File
@@ -89,6 +89,15 @@ static bool bootstrap_endpoint_fixture_start(bootstrap_endpoint_fixture_t *fixtu
return written > 0 && written < (int)sizeof(fixture->runtime_dir);
}
/* Compare against canonical parents only: the endpoint canonicalizes its parent
* before building the runtime path (/var/folders/... becomes /private/var/... on
* macOS), so a raw prefix compare would miss a correct relocation. */
static bool bootstrap_path_has_parent(const char *path, const char *parent) {
size_t length = parent ? strlen(parent) : 0;
return path && length > 0 && strncmp(path, parent, length) == 0 &&
(path[length] == '/' || path[length] == '\\');
}
static void bootstrap_endpoint_fixture_finish(bootstrap_endpoint_fixture_t *fixture) {
cbm_daemon_ipc_endpoint_free(fixture->endpoint);
if (fixture->runtime_dir[0] != '\0') {
@@ -220,8 +229,7 @@ static bool bootstrap_fake_spawn(void *opaque, const cbm_daemon_bootstrap_launch
bootstrap_fake_ops_t *fake = opaque;
/* Client bootstrap must only ever spawn the EPHEMERAL two-argument
* shape; the permanent shape belongs exclusively to `daemon start`. */
bool exact = spec && spec->argc == 2U && spec->argv[0] &&
spec->argv[1] && !spec->argv[2] &&
bool exact = spec && spec->argc == 2U && spec->argv[0] && spec->argv[1] && !spec->argv[2] &&
strcmp(spec->argv[1], CBM_DAEMON_INTERNAL_ARG) == 0 && spec->detached &&
!spec->inherit_standard_handles && !spec->use_shell &&
atomic_load(&fake->handoff_count) > 0 && atomic_load(&fake->lock_held) == 1;
@@ -365,6 +373,81 @@ TEST(daemon_bootstrap_uses_one_stable_per_account_endpoint) {
PASS();
}
/* #1574/#1621: the shipped build must be able to relocate the rendezvous when
* the default ancestry (%LOCALAPPDATA%, /private/tmp) cannot pass the
* private-directory walk — otherwise every command fails, `config list`
* included, and the operator cannot reconfigure their way out. CBM_RUNTIME_DIR
* moves WHERE the rendezvous lives; it never relaxes HOW it is checked, so a
* value that cannot be a private runtime parent must be refused rather than
* silently replaced by the default. An explicit parent — the compile-time test
* seam, the lifecycle guards' isolated namespace — keeps precedence over it. */
TEST(daemon_bootstrap_runtime_dir_env_relocates_rendezvous) {
char override_parent[BOOTSTRAP_TEST_PATH_CAP] = {0};
char canonical_override[BOOTSTRAP_TEST_PATH_CAP] = {0};
char canonical_explicit[BOOTSTRAP_TEST_PATH_CAP] = {0};
char relocated_runtime[BOOTSTRAP_TEST_PATH_CAP] = {0};
char explicit_runtime[BOOTSTRAP_TEST_PATH_CAP] = {0};
char unusable[BOOTSTRAP_TEST_PATH_CAP] = {0};
int written = snprintf(override_parent, sizeof(override_parent),
"%s/cbm-bootstrap-runtime-env-XXXXXX", cbm_tmpdir());
if (written <= 0 || written >= (int)sizeof(override_parent) || !cbm_mkdtemp(override_parent)) {
FAIL("could not create the override runtime parent");
}
written = snprintf(unusable, sizeof(unusable), "%s/absent/nested", override_parent);
bool prepared =
written > 0 && written < (int)sizeof(unusable) &&
cbm_canonical_path(override_parent, canonical_override, sizeof(canonical_override)) != 0 &&
cbm_setenv("CBM_RUNTIME_DIR", override_parent, 1) == 0;
/* NULL parent == every product call site: daemon, MCP client, local CLI,
* index worker, activation. */
cbm_daemon_ipc_endpoint_t *relocated =
prepared ? cbm_daemon_bootstrap_endpoint_new(NULL) : NULL;
const char *relocated_dir = relocated ? cbm_daemon_ipc_endpoint_runtime_dir(relocated) : NULL;
if (relocated_dir) {
(void)snprintf(relocated_runtime, sizeof(relocated_runtime), "%s", relocated_dir);
}
/* Same environment, explicit parent: the caller still wins. */
bootstrap_endpoint_fixture_t fixture = {0};
bool explicit_started = prepared && bootstrap_endpoint_fixture_start(&fixture, "runtime-env");
bool explicit_canonical =
explicit_started &&
cbm_canonical_path(fixture.parent, canonical_explicit, sizeof(canonical_explicit)) != 0;
if (explicit_started) {
(void)snprintf(explicit_runtime, sizeof(explicit_runtime), "%s", fixture.runtime_dir);
}
/* A named parent that cannot pass validation is refused, never ignored. */
bool unusable_set = prepared && cbm_setenv("CBM_RUNTIME_DIR", unusable, 1) == 0;
cbm_daemon_ipc_endpoint_t *refused =
unusable_set ? cbm_daemon_bootstrap_endpoint_new(NULL) : NULL;
/* Restore before asserting: a failed assertion returns immediately, and a
* leaked CBM_RUNTIME_DIR would follow every later suite in this process. */
(void)cbm_unsetenv("CBM_RUNTIME_DIR");
cbm_daemon_ipc_endpoint_free(refused);
cbm_daemon_ipc_endpoint_free(relocated);
if (relocated_runtime[0] != '\0') {
(void)cbm_rmdir(relocated_runtime);
}
if (explicit_started) {
bootstrap_endpoint_fixture_finish(&fixture);
}
(void)cbm_rmdir(override_parent);
ASSERT_TRUE(prepared);
ASSERT_TRUE(explicit_started);
ASSERT_TRUE(explicit_canonical);
ASSERT_TRUE(unusable_set);
ASSERT_NOT_NULL(relocated);
ASSERT_TRUE(bootstrap_path_has_parent(relocated_runtime, canonical_override));
ASSERT_TRUE(bootstrap_path_has_parent(explicit_runtime, canonical_explicit));
ASSERT_FALSE(bootstrap_path_has_parent(explicit_runtime, canonical_override));
ASSERT_NULL(refused);
PASS();
}
TEST(daemon_bootstrap_launches_only_exact_detached_hidden_role) {
cbm_daemon_bootstrap_launch_spec_t spec;
ASSERT_TRUE(cbm_daemon_bootstrap_launch_spec_init("/tmp/cbm exact", &spec));
@@ -774,6 +857,7 @@ SUITE(daemon_bootstrap) {
RUN_TEST(daemon_bootstrap_internal_roles_never_take_client_leases);
RUN_TEST(daemon_bootstrap_rejects_ambiguous_internal_daemon_argv);
RUN_TEST(daemon_bootstrap_uses_one_stable_per_account_endpoint);
RUN_TEST(daemon_bootstrap_runtime_dir_env_relocates_rendezvous);
RUN_TEST(daemon_bootstrap_launches_only_exact_detached_hidden_role);
RUN_TEST(daemon_bootstrap_permanent_daemon_argv_is_byte_exact);
RUN_TEST(daemon_bootstrap_daemon_ctl_token_routes_after_cli);