Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


Expand Down
33 changes: 32 additions & 1 deletion src/daemon/bootstrap.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
88 changes: 86 additions & 2 deletions tests/test_daemon_bootstrap.c
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
Expand Down
Loading