mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge pull request #1645 from DeusData/fix/product-runtime-dir-override
fix(daemon): let a shipped build relocate the rendezvous via CBM_RUNTIME_DIR
This commit is contained in:
3 files changed
+156
-3
No files matched your search
@@ -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
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user