mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-12 09:47:20 +08:00
Merge pull request #2412 from DeusData/fix/activation-unknown-daemon-is-foreign
fix(cli): never stop a daemon whose cache is not confirmed as ours
This commit is contained in:
+60
-12
@@ -226,6 +226,7 @@ typedef struct {
|
|||||||
static cbm_cli_activation_ops_t g_cli_activation_test_ops;
|
static cbm_cli_activation_ops_t g_cli_activation_test_ops;
|
||||||
static bool g_cli_activation_test_ops_set = false;
|
static bool g_cli_activation_test_ops_set = false;
|
||||||
static const char *g_cli_activation_runtime_parent_for_test = NULL;
|
static const char *g_cli_activation_runtime_parent_for_test = NULL;
|
||||||
|
static bool g_cli_activation_scope_cache_unreadable_for_test = false;
|
||||||
|
|
||||||
static void cli_activation_diagnostic(const cbm_cli_activation_ops_t *ops, const char *message) {
|
static void cli_activation_diagnostic(const cbm_cli_activation_ops_t *ops, const char *message) {
|
||||||
const char *diagnostic = message ? message : CLI_ACTIVATION_REFUSED_MESSAGE;
|
const char *diagnostic = message ? message : CLI_ACTIVATION_REFUSED_MESSAGE;
|
||||||
@@ -335,6 +336,10 @@ const char *cbm_cli_activation_runtime_parent_for_test(void) {
|
|||||||
return g_cli_activation_runtime_parent_for_test;
|
return g_cli_activation_runtime_parent_for_test;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void cbm_cli_set_activation_scope_cache_unreadable_for_test(bool unreadable) {
|
||||||
|
g_cli_activation_scope_cache_unreadable_for_test = unreadable;
|
||||||
|
}
|
||||||
|
|
||||||
static const char *cli_activation_action_text(cbm_daemon_runtime_activation_action_t action) {
|
static const char *cli_activation_action_text(cbm_daemon_runtime_activation_action_t action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case CBM_DAEMON_RUNTIME_ACTIVATION_INSTALL:
|
case CBM_DAEMON_RUNTIME_ACTIVATION_INSTALL:
|
||||||
@@ -541,6 +546,7 @@ typedef enum {
|
|||||||
CLI_ACTIVATION_SCOPE_ACTIVE = 0,
|
CLI_ACTIVATION_SCOPE_ACTIVE = 0,
|
||||||
CLI_ACTIVATION_SCOPE_NOTHING_TO_REPLACE,
|
CLI_ACTIVATION_SCOPE_NOTHING_TO_REPLACE,
|
||||||
CLI_ACTIVATION_SCOPE_FOREIGN_COHORT,
|
CLI_ACTIVATION_SCOPE_FOREIGN_COHORT,
|
||||||
|
CLI_ACTIVATION_SCOPE_UNCONFIRMED,
|
||||||
CLI_ACTIVATION_SCOPE_ERROR,
|
CLI_ACTIVATION_SCOPE_ERROR,
|
||||||
} cli_activation_scope_t;
|
} cli_activation_scope_t;
|
||||||
|
|
||||||
@@ -566,9 +572,43 @@ static void cli_activation_log_guard_decision(const cli_activation_production_co
|
|||||||
* anything at all (a --skip-binary install without an index reset publishes
|
* anything at all (a --skip-binary install without an index reset publishes
|
||||||
* nothing), and whose cohort is active. Admission with an immediate deadline
|
* nothing), and whose cohort is active. Admission with an immediate deadline
|
||||||
* is the cheapest authoritative read of the active lifetime record: OK means
|
* is the cheapest authoritative read of the active lifetime record: OK means
|
||||||
* the cohort is ours or empty, CONFLICT names the active identity (its cache
|
* the cohort shares this exact identity (cache included) or is empty,
|
||||||
* fingerprint is filled for every conflict kind), BUSY means another
|
* CONFLICT names the active identity (its cache fingerprint is filled for
|
||||||
* activation holds maintenance and the barrier waits for it as before. */
|
* every conflict kind), BUSY means another activation or a session start held
|
||||||
|
* the group, so the active identity was never read.
|
||||||
|
*
|
||||||
|
* Only a POSITIVE match may drain (user decision 2026-09-28, "unknown =
|
||||||
|
* foreign, keep it"): the daemon's activation-shutdown handler verifies the
|
||||||
|
* requester's build, never its cache, so this decision is the only thing
|
||||||
|
* that keeps another HOME's install/update from stopping the account's live
|
||||||
|
* daemon. A busy group or an unreadable active cache used to fall through to
|
||||||
|
* "ours" and stop every session behind that daemon. */
|
||||||
|
static cli_activation_scope_t cli_activation_scope_unconfirmed(
|
||||||
|
cli_activation_production_context_t *context, const char *scope, const char *reason) {
|
||||||
|
const char *action = cli_activation_action_text(context->action);
|
||||||
|
(void)snprintf(context->scope_detail, sizeof(context->scope_detail),
|
||||||
|
"active cohort ownership unconfirmed (%s); scope=%s; nothing stopped", reason,
|
||||||
|
scope);
|
||||||
|
cli_activation_log_guard_decision(context, "unconfirmed_cohort", NULL);
|
||||||
|
/* Name the daemon being left: a read-only status probe (no cohort, no
|
||||||
|
* HELLO, no admission) that works across build skew. */
|
||||||
|
cbm_daemon_runtime_status_t daemon;
|
||||||
|
if (cbm_daemon_runtime_request_status(context->endpoint, &context->identity,
|
||||||
|
CLI_ACTIVATION_CONTROL_TIMEOUT_MS, &daemon)) {
|
||||||
|
printf("Leaving the running CBM daemon untouched (pid %lu, version %s): this %s could "
|
||||||
|
"not confirm that it serves the cache this %s targets (%s).\n",
|
||||||
|
(unsigned long)daemon.daemon_pid, daemon.semantic_version, action, action, reason);
|
||||||
|
} else {
|
||||||
|
printf("Leaving active CBM sessions untouched: this %s could not confirm that they "
|
||||||
|
"serve the cache it targets (%s).\n",
|
||||||
|
action, reason);
|
||||||
|
}
|
||||||
|
printf("If it is yours, close its CBM sessions and run `codebase-memory-mcp daemon stop` "
|
||||||
|
"to stop it yourself.\n");
|
||||||
|
(void)fflush(stdout);
|
||||||
|
return CLI_ACTIVATION_SCOPE_UNCONFIRMED;
|
||||||
|
}
|
||||||
|
|
||||||
static cli_activation_scope_t cli_activation_resolve_scope(
|
static cli_activation_scope_t cli_activation_resolve_scope(
|
||||||
cli_activation_production_context_t *context) {
|
cli_activation_production_context_t *context) {
|
||||||
const char *runtime_dir = cbm_daemon_ipc_endpoint_runtime_dir(context->endpoint);
|
const char *runtime_dir = cbm_daemon_ipc_endpoint_runtime_dir(context->endpoint);
|
||||||
@@ -594,20 +634,27 @@ static cli_activation_scope_t cli_activation_resolve_scope(
|
|||||||
if (lease) {
|
if (lease) {
|
||||||
return CLI_ACTIVATION_SCOPE_ERROR;
|
return CLI_ACTIVATION_SCOPE_ERROR;
|
||||||
}
|
}
|
||||||
|
if (g_cli_activation_scope_cache_unreadable_for_test) {
|
||||||
|
conflict.active_cache_fingerprint[0] = '\0';
|
||||||
|
}
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case CBM_VERSION_COHORT_OK:
|
case CBM_VERSION_COHORT_OK:
|
||||||
case CBM_VERSION_COHORT_BUSY:
|
|
||||||
return CLI_ACTIVATION_SCOPE_ACTIVE;
|
return CLI_ACTIVATION_SCOPE_ACTIVE;
|
||||||
|
case CBM_VERSION_COHORT_BUSY:
|
||||||
|
return cli_activation_scope_unconfirmed(
|
||||||
|
context, scope, "the session group was busy, so its cache identity could not be read");
|
||||||
case CBM_VERSION_COHORT_CONFLICT:
|
case CBM_VERSION_COHORT_CONFLICT:
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return CLI_ACTIVATION_SCOPE_ERROR;
|
return CLI_ACTIVATION_SCOPE_ERROR;
|
||||||
}
|
}
|
||||||
if (!conflict.active_cache_fingerprint[0] ||
|
if (!conflict.active_cache_fingerprint[0]) {
|
||||||
strcmp(conflict.active_cache_fingerprint, context->cache_fingerprint) == 0) {
|
return cli_activation_scope_unconfirmed(context, scope,
|
||||||
/* Same cache namespace, another build or version: that IS the daemon
|
"its cache identity could not be read");
|
||||||
* this activation replaces. An unreadable active cache stays in scope
|
}
|
||||||
* rather than silently exempting a same-namespace daemon. */
|
if (strcmp(conflict.active_cache_fingerprint, context->cache_fingerprint) == 0) {
|
||||||
|
/* Positively confirmed: same cache namespace, another build or
|
||||||
|
* version. That IS the daemon this activation replaces. */
|
||||||
return CLI_ACTIVATION_SCOPE_ACTIVE;
|
return CLI_ACTIVATION_SCOPE_ACTIVE;
|
||||||
}
|
}
|
||||||
(void)snprintf(context->scope_detail, sizeof(context->scope_detail),
|
(void)snprintf(context->scope_detail, sizeof(context->scope_detail),
|
||||||
@@ -636,9 +683,10 @@ static int cli_activation_production_reserve(void *opaque, cbm_cli_activation_lo
|
|||||||
}
|
}
|
||||||
if (scope != CLI_ACTIVATION_SCOPE_ACTIVE) {
|
if (scope != CLI_ACTIVATION_SCOPE_ACTIVE) {
|
||||||
/* Nothing in the target namespace is being replaced, or the only
|
/* Nothing in the target namespace is being replaced, or the only
|
||||||
* active cohort serves another namespace: hold no maintenance,
|
* active cohort serves another namespace, or its ownership could not
|
||||||
* admission, lifetime or startup lock (each wakes or blocks the other
|
* be confirmed: hold no maintenance, admission, lifetime or startup
|
||||||
* namespace's sessions) and send no drain request. */
|
* lock (each wakes or blocks the other namespace's sessions) and
|
||||||
|
* send no drain request. */
|
||||||
if (!cli_activation_log_event(context, "quiesce_skipped", context->scope_detail)) {
|
if (!cli_activation_log_event(context, "quiesce_skipped", context->scope_detail)) {
|
||||||
return CLI_ERR;
|
return CLI_ERR;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -505,6 +505,12 @@ void cbm_cli_set_activation_ops_for_test(const cbm_cli_activation_ops_t *ops);
|
|||||||
void cbm_cli_set_activation_runtime_parent_for_test(const char *runtime_parent);
|
void cbm_cli_set_activation_runtime_parent_for_test(const char *runtime_parent);
|
||||||
const char *cbm_cli_activation_runtime_parent_for_test(void);
|
const char *cbm_cli_activation_runtime_parent_for_test(void);
|
||||||
|
|
||||||
|
/* Internal integration-test seam: the activation scope read reports the active
|
||||||
|
* cohort's cache fingerprint as unreadable (blank), exactly what the scope
|
||||||
|
* decision sees when that field cannot be recovered. false restores the real
|
||||||
|
* read. Not a command-line or environment override. */
|
||||||
|
void cbm_cli_set_activation_scope_cache_unreadable_for_test(bool unreadable);
|
||||||
|
|
||||||
/* ── Subcommands (wired from main.c) ─────────────────────────── */
|
/* ── Subcommands (wired from main.c) ─────────────────────────── */
|
||||||
|
|
||||||
/* install: copy binary, install skills, install editor MCP configs, ensure PATH.
|
/* install: copy binary, install skills, install editor MCP configs, ensure PATH.
|
||||||
|
|||||||
@@ -18,11 +18,13 @@
|
|||||||
#include <cli/cli.h>
|
#include <cli/cli.h>
|
||||||
#include <cli/progress_sink.h>
|
#include <cli/progress_sink.h>
|
||||||
#include <daemon/bootstrap.h>
|
#include <daemon/bootstrap.h>
|
||||||
|
#include <daemon/ipc.h>
|
||||||
#include <daemon/runtime.h>
|
#include <daemon/runtime.h>
|
||||||
#include <daemon/version_cohort.h>
|
#include <daemon/version_cohort.h>
|
||||||
#include <foundation/constants.h>
|
#include <foundation/constants.h>
|
||||||
#include <foundation/log.h>
|
#include <foundation/log.h>
|
||||||
#include <foundation/platform.h>
|
#include <foundation/platform.h>
|
||||||
|
#include <foundation/private_file_lock.h>
|
||||||
#include <foundation/sha256.h>
|
#include <foundation/sha256.h>
|
||||||
#include <mcp/mcp.h>
|
#include <mcp/mcp.h>
|
||||||
#include <mcp/index_supervisor.h>
|
#include <mcp/index_supervisor.h>
|
||||||
@@ -2037,6 +2039,139 @@ TEST(cli_install_skip_binary_unchanged_in_host_namespace_quiesces_nothing) {
|
|||||||
ASSERT_TRUE(nothing_drained);
|
ASSERT_TRUE(nothing_drained);
|
||||||
PASS();
|
PASS();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* cli_scope_install with the activation's stdout captured into out. */
|
||||||
|
static int cli_scope_install_captured(cli_scope_fixture_t *fixture, const char *home,
|
||||||
|
const char *cache, const char *bin_dir, bool skip_binary,
|
||||||
|
char *out, size_t out_size) {
|
||||||
|
out[0] = '\0';
|
||||||
|
FILE *capture = tmpfile();
|
||||||
|
int saved_stdout = capture ? dup(STDOUT_FILENO) : -1;
|
||||||
|
bool redirected = false;
|
||||||
|
if (capture && saved_stdout >= 0) {
|
||||||
|
fflush(stdout);
|
||||||
|
redirected = dup2(fileno(capture), STDOUT_FILENO) >= 0;
|
||||||
|
}
|
||||||
|
int rc = redirected ? cli_scope_install(fixture, home, cache, bin_dir, skip_binary) : -1;
|
||||||
|
if (redirected) {
|
||||||
|
fflush(stdout);
|
||||||
|
(void)dup2(saved_stdout, STDOUT_FILENO);
|
||||||
|
}
|
||||||
|
if (saved_stdout >= 0) {
|
||||||
|
close(saved_stdout);
|
||||||
|
}
|
||||||
|
if (capture) {
|
||||||
|
rewind(capture);
|
||||||
|
size_t count = fread(out, 1, out_size - 1U, capture);
|
||||||
|
out[count] = '\0';
|
||||||
|
fclose(capture);
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The notice an activation prints when it leaves a daemon it could not
|
||||||
|
* confirm as its own: names the daemon by pid and tells the user how to stop
|
||||||
|
* it themselves. Never the "Stopping ..." banner of a drain. */
|
||||||
|
static bool cli_scope_kept_notice_names_host(const char *output, pid_t host) {
|
||||||
|
char pid_text[48];
|
||||||
|
snprintf(pid_text, sizeof(pid_text), "(pid %ld, version %s)", (long)host, CBM_VERSION);
|
||||||
|
return strstr(output, "Leaving the running CBM daemon untouched") != NULL &&
|
||||||
|
strstr(output, pid_text) != NULL &&
|
||||||
|
strstr(output, "codebase-memory-mcp daemon stop") != NULL &&
|
||||||
|
strstr(output, "Stopping active CBM sessions") == NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (e) USER DECISION 2026-09-28, "unknown = foreign, keep it": the scope read
|
||||||
|
* reaches the active cohort but cannot read its cache identity. Ownership is
|
||||||
|
* unconfirmed, so the sandbox install must leave the host daemon serving,
|
||||||
|
* stop no session, and say which daemon it left and how to stop it. The
|
||||||
|
* install publishes a binary (no --skip-binary): an activation that replaces
|
||||||
|
* nothing never consults the cohort at all (test d). */
|
||||||
|
TEST(cli_install_foreign_home_unreadable_cohort_cache_keeps_host_daemon) {
|
||||||
|
cli_scope_fixture_t fixture;
|
||||||
|
bool ready = cli_scope_fixture_start(&fixture, "unread");
|
||||||
|
char foreign_home[512];
|
||||||
|
char foreign_cache[576];
|
||||||
|
char foreign_bin[640];
|
||||||
|
char activation_log[704];
|
||||||
|
cli_scope_foreign_paths(&fixture, foreign_home, foreign_cache, foreign_bin, activation_log);
|
||||||
|
bool prepared = ready && cbm_mkdir_p(foreign_home, 0700);
|
||||||
|
char output[16384];
|
||||||
|
output[0] = '\0';
|
||||||
|
cbm_cli_set_activation_scope_cache_unreadable_for_test(true);
|
||||||
|
int install_rc = prepared
|
||||||
|
? cli_scope_install_captured(&fixture, foreign_home, foreign_cache,
|
||||||
|
foreign_bin, false, output, sizeof(output))
|
||||||
|
: -1;
|
||||||
|
cbm_cli_set_activation_scope_cache_unreadable_for_test(false);
|
||||||
|
bool host_serving = prepared && cli_scope_host_serving(&fixture);
|
||||||
|
bool notice = cli_scope_kept_notice_names_host(output, fixture.host);
|
||||||
|
const char *events = read_test_file(activation_log);
|
||||||
|
bool completed = events && strstr(events, "\"phase\":\"completed\"") != NULL;
|
||||||
|
bool nothing_drained = events && strstr(events, "cohort drained") == NULL;
|
||||||
|
int host_exit = cli_scope_fixture_finish(&fixture);
|
||||||
|
|
||||||
|
ASSERT_TRUE(ready);
|
||||||
|
ASSERT_TRUE(host_serving);
|
||||||
|
ASSERT_EQ(host_exit, 0);
|
||||||
|
ASSERT_TRUE(nothing_drained);
|
||||||
|
ASSERT_TRUE(notice);
|
||||||
|
ASSERT_EQ(install_rc, 0);
|
||||||
|
ASSERT_TRUE(completed);
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (f) Same decision, busy group: another activation holds the cohort
|
||||||
|
* maintenance gate, so the scope read cannot reach the active identity at
|
||||||
|
* all. Unconfirmed ownership keeps the host daemon; the sandbox install
|
||||||
|
* completes without draining anyone. The gate is held through the product's
|
||||||
|
* own lock-directory handle on the name version_cohort.c uses. */
|
||||||
|
TEST(cli_install_foreign_home_busy_cohort_keeps_host_daemon) {
|
||||||
|
cli_scope_fixture_t fixture;
|
||||||
|
bool ready = cli_scope_fixture_start(&fixture, "busy");
|
||||||
|
char foreign_home[512];
|
||||||
|
char foreign_cache[576];
|
||||||
|
char foreign_bin[640];
|
||||||
|
char activation_log[704];
|
||||||
|
cli_scope_foreign_paths(&fixture, foreign_home, foreign_cache, foreign_bin, activation_log);
|
||||||
|
cbm_private_lock_directory_t *lock_directory = NULL;
|
||||||
|
cbm_private_file_lock_t *maintenance = NULL;
|
||||||
|
bool held = ready &&
|
||||||
|
cbm_daemon_ipc_private_lock_directory_new(fixture.endpoint, &lock_directory) ==
|
||||||
|
CBM_PRIVATE_FILE_LOCK_OK &&
|
||||||
|
cbm_private_file_lock_try_acquire(
|
||||||
|
lock_directory, "cbm-version-cohort-maintenance-v1.lock",
|
||||||
|
CBM_PRIVATE_FILE_LOCK_EX, &maintenance) == CBM_PRIVATE_FILE_LOCK_OK;
|
||||||
|
bool prepared = held && cbm_mkdir_p(foreign_home, 0700);
|
||||||
|
char output[16384];
|
||||||
|
output[0] = '\0';
|
||||||
|
int install_rc = prepared
|
||||||
|
? cli_scope_install_captured(&fixture, foreign_home, foreign_cache,
|
||||||
|
foreign_bin, false, output, sizeof(output))
|
||||||
|
: -1;
|
||||||
|
bool host_serving = prepared && cli_scope_host_serving(&fixture);
|
||||||
|
bool released =
|
||||||
|
maintenance && cbm_private_file_lock_release(&maintenance) == CBM_PRIVATE_FILE_LOCK_OK;
|
||||||
|
if (lock_directory) {
|
||||||
|
cbm_private_lock_directory_close(lock_directory);
|
||||||
|
}
|
||||||
|
bool notice = cli_scope_kept_notice_names_host(output, fixture.host);
|
||||||
|
const char *events = read_test_file(activation_log);
|
||||||
|
bool completed = events && strstr(events, "\"phase\":\"completed\"") != NULL;
|
||||||
|
bool nothing_drained = events && strstr(events, "cohort drained") == NULL;
|
||||||
|
int host_exit = cli_scope_fixture_finish(&fixture);
|
||||||
|
|
||||||
|
ASSERT_TRUE(ready);
|
||||||
|
ASSERT_TRUE(held);
|
||||||
|
ASSERT_TRUE(host_serving);
|
||||||
|
ASSERT_EQ(host_exit, 0);
|
||||||
|
ASSERT_TRUE(nothing_drained);
|
||||||
|
ASSERT_TRUE(notice);
|
||||||
|
ASSERT_EQ(install_rc, 0);
|
||||||
|
ASSERT_TRUE(completed);
|
||||||
|
ASSERT_TRUE(released);
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
TEST(cli_install_force_quiesces_active_cohort_before_replacing_binary) {
|
TEST(cli_install_force_quiesces_active_cohort_before_replacing_binary) {
|
||||||
@@ -16504,6 +16639,8 @@ SUITE(cli) {
|
|||||||
RUN_TEST(cli_install_binary_into_foreign_home_never_drains_host_cohort);
|
RUN_TEST(cli_install_binary_into_foreign_home_never_drains_host_cohort);
|
||||||
RUN_TEST(cli_install_into_host_namespace_still_drains_host_cohort);
|
RUN_TEST(cli_install_into_host_namespace_still_drains_host_cohort);
|
||||||
RUN_TEST(cli_install_skip_binary_unchanged_in_host_namespace_quiesces_nothing);
|
RUN_TEST(cli_install_skip_binary_unchanged_in_host_namespace_quiesces_nothing);
|
||||||
|
RUN_TEST(cli_install_foreign_home_unreadable_cohort_cache_keeps_host_daemon);
|
||||||
|
RUN_TEST(cli_install_foreign_home_busy_cohort_keeps_host_daemon);
|
||||||
#endif
|
#endif
|
||||||
RUN_TEST(cli_install_force_quiesces_active_cohort_before_replacing_binary);
|
RUN_TEST(cli_install_force_quiesces_active_cohort_before_replacing_binary);
|
||||||
RUN_TEST(cli_install_dir_and_skip_config_stage_first_install_safely);
|
RUN_TEST(cli_install_dir_and_skip_config_stage_first_install_safely);
|
||||||
|
|||||||
Reference in New Issue
Block a user