Skip to content

Commit 2a24781

Browse files
committed
feat(index): add opt-in discovery resource limits
Indexing accepts whatever a repository contains. A tree carrying a vendored monorepo, a generated dump, or a runaway build directory is discovered in full, and the first sign of trouble is a host under memory pressure with nothing that attributes it to indexing. Add two opt-in limits evaluated during discovery against accepted source files only: index_max_files and index_max_source_mb. Both default to off, so nothing changes until an operator sets one. Crossing a limit fails the whole attempt with a structured resource_limit_exceeded result naming the resource, the observed value and the limit; no partial graph is published, and an existing serving index keeps answering. Limits are read from the CLI-managed _config.db and are not MCP request arguments. A supervised parent replaces any caller-supplied policy before spawning its worker, and the worker rejects a missing or incomplete contract, so the CLI, the daemon and the supervised worker all enforce the same decision. The two shell regressions that hand-roll the supervisor's worker argv carry that contract as well. Without it the worker exits before either guard can observe anything, and the guard would go quietly vacuous. Signed-off-by: 刘冲 <mail@liuchong.dev>
1 parent 46ae198 commit 2a24781

25 files changed

Lines changed: 1276 additions & 44 deletions

‎Makefile.cbm‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ FOUNDATION_SRCS = \
224224
src/foundation/profile.c \
225225
src/foundation/dump_verify.c \
226226
src/foundation/limits.c \
227+
src/foundation/index_policy.c \
227228
src/foundation/subprocess.c \
228229
src/foundation/sha256.c \
229230
src/foundation/secure_random.c \
@@ -502,6 +503,7 @@ TEST_FOUNDATION_SRCS = \
502503
tests/test_str_intern.c \
503504
tests/test_log.c \
504505
tests/test_str_util.c \
506+
tests/test_index_policy.c \
505507
tests/test_workspace.c \
506508
tests/test_platform.c \
507509
tests/test_diagnostics.c \

‎README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,9 +667,15 @@ codebase-memory-mcp config list # show all settings
667667
codebase-memory-mcp config set auto_index true # auto-index on session start
668668
codebase-memory-mcp config set auto_index_limit 50000 # max files for auto-index
669669
codebase-memory-mcp config set auto_watch false # don't register background git watcher (default: true)
670+
codebase-memory-mcp config set index_max_files 250000 # optional per-index source-file limit
671+
codebase-memory-mcp config set index_max_source_mb 16384 # optional per-index source-size limit
670672
codebase-memory-mcp config reset auto_index # reset to default
671673
```
672674

675+
The two `index_max_*` settings default to `off`. Exceeding one fails the complete
676+
index attempt rather than publishing a partial graph; an existing serving index
677+
is preserved. See [Index resource limits](docs/INDEX_RESOURCE_LIMITS.md).
678+
673679
### Environment Variables
674680

675681
| Variable | Default | Description |

‎docs/CONFIGURATION.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ Current keys:
8686
|---|---|---|
8787
| `auto_index` | `false` | Automatically index new projects when an MCP session starts. |
8888
| `auto_index_limit` | `50000` | Maximum file count allowed for automatic indexing of a new project. |
89+
| `index_max_files` | `off` | Optional maximum number of accepted source files in one discovery run. |
90+
| `index_max_source_mb` | `off` | Optional maximum accepted source size in MiB in one discovery run. |
91+
92+
The two `index_max_*` settings are independent and disabled by default. They
93+
apply to explicit indexing, automatic indexing, and watcher re-indexing, but not
94+
to `cross-repo-intelligence`, which does not scan repository source files.
95+
Equality is allowed; exceeding either setting fails the complete index request
96+
and preserves any previously serving database. See
97+
[Index resource limits](INDEX_RESOURCE_LIMITS.md) for counting, validation, and
98+
error-response details.
8999

90100
## 3. UI Settings
91101

‎docs/INDEX_RESOURCE_LIMITS.md‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Index resource limits
2+
3+
Index resource limits are optional operator controls for repositories whose
4+
discovery breadth is not known in advance. They are disabled by default so
5+
existing large-repository workloads retain their current behavior.
6+
7+
## Discovery settings
8+
9+
| Key | Default | Accepted value | Protects |
10+
|---|---:|---:|---|
11+
| `index_max_files` | `off` | `off` or `1..10000000` | Accepted source-file count |
12+
| `index_max_source_mb` | `off` | `off` or `1..1048576` | Accepted source-file bytes |
13+
14+
Set or reset them with the normal configuration command:
15+
16+
```bash
17+
codebase-memory-mcp config set index_max_files 250000
18+
codebase-memory-mcp config set index_max_source_mb 16384
19+
codebase-memory-mcp config reset index_max_files
20+
```
21+
22+
Values use base-10 integers. MiB means 1,048,576 bytes. Empty values, zero,
23+
negative values, suffixes, trailing characters, and values outside the stated
24+
ranges are rejected without changing the stored value.
25+
26+
## Counting and failure semantics
27+
28+
`index_max_files` counts a file only after it passes directory pruning, ignore
29+
rules, filename and suffix filters, language detection, and the existing
30+
per-file size rule. `index_max_source_mb` sums the filesystem sizes of that same
31+
accepted set.
32+
33+
Equality is allowed. The first file or byte that makes an observed value greater
34+
than its limit stops discovery. CBM discards the partial file list and does not
35+
publish a partial graph as a complete index.
36+
37+
For an explicit MCP request the error payload contains:
38+
39+
```json
40+
{
41+
"status": "error",
42+
"code": "resource_limit_exceeded",
43+
"stage": "discovery",
44+
"resource": "files",
45+
"observed": 250001,
46+
"limit": 250000,
47+
"unit": "files",
48+
"retryable": true,
49+
"serving_index_preserved": true,
50+
"message": "Index discovery exceeded index_max_files"
51+
}
52+
```
53+
54+
The previous database remains available because publication occurs only after a
55+
complete discovery and successful staged build. If no previous database exists,
56+
`serving_index_preserved` is false.
57+
58+
## Trust and compatibility
59+
60+
Limits are read from the CLI-managed `_config.db`; they are not MCP request
61+
arguments. A supervised parent replaces any caller-supplied internal policy
62+
before spawning its worker, and the worker rejects a missing or incomplete
63+
parent policy.
64+
65+
These settings do not replace or increase `auto_index_limit`, change the 512 MiB
66+
single-file cap, alter workspace-root authorization, or affect
67+
`cross-repo-intelligence`. With both settings `off`, discovery follows the
68+
existing unbounded path.

‎scripts/test-runtime.sh‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ _cbm_test_runtime_daemon() {
105105
CBM_CACHE_DIR="$_CBM_TEST_RUNTIME_PRODUCT_CACHE" "$1" daemon "$2"
106106
}
107107

108+
# The supervisor resolves one resource policy per index and hands it to the
109+
# worker in argv; a worker that finds no complete policy refuses to start rather
110+
# than index unbounded. A shell test that spawns `cli --index-worker` itself
111+
# stands in for the supervisor and owes the worker the same object. Mirrors
112+
# cbm_mcp_index_policy_add_to_args: every key in cbm_index_policy_key_at, and
113+
# nothing else.
114+
cbm_test_index_worker_policy_json() {
115+
printf '%s' '"_cbm_index_policy":{"index_max_files":"off","index_max_source_mb":"off"}'
116+
}
117+
108118
cbm_test_runtime_cleanup() {
109119
local binary="${1:-}" root="$_CBM_TEST_RUNTIME_CREATED_ROOT"
110120
local name="${_CBM_TEST_RUNTIME_CREATED_ROOT##*/}" runtime_entry="" active=0

‎src/cli/cli.c‎

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6734,6 +6734,25 @@ int cbm_config_delete(cbm_config_t *cfg, const char *key) {
67346734
return rc;
67356735
}
67366736

6737+
bool cbm_config_load_index_policy(cbm_config_t *cfg, cbm_index_resource_policy_t *policy,
6738+
char *error, size_t error_size) {
6739+
if (!cfg || !policy) {
6740+
if (error && error_size > 0) {
6741+
(void)snprintf(error, error_size, "index resource configuration is unavailable");
6742+
}
6743+
return false;
6744+
}
6745+
cbm_index_policy_init(policy);
6746+
for (size_t index = 0; index < cbm_index_policy_key_count(); index++) {
6747+
const char *key = cbm_index_policy_key_at(index);
6748+
const char *value = cbm_config_get(cfg, key, cbm_index_policy_default_value(key));
6749+
if (!cbm_index_policy_set(policy, key, value, error, error_size)) {
6750+
return false;
6751+
}
6752+
}
6753+
return true;
6754+
}
6755+
67376756
/* ── Config CLI subcommand ────────────────────────────────────── */
67386757

67396758
/* THE config-key table. list, get, help, and key validation all read this one
@@ -6756,6 +6775,8 @@ static const config_key_def_t CONFIG_KEYS[] = {
67566775
{CBM_CONFIG_UI_LANG, "auto", "Pin graph UI language: en, zh, or auto"},
67576776
{CBM_CONFIG_UI_ENABLED, "false", "Serve the graph UI on a loopback HTTP port"},
67586777
{CBM_CONFIG_UI_PORT, "9749", "Port for the graph UI listener when enabled"},
6778+
{CBM_INDEX_CONFIG_MAX_FILES, "off", "Max accepted source files per index, or off"},
6779+
{CBM_INDEX_CONFIG_MAX_SOURCE_MB, "off", "Max accepted source MiB per index, or off"},
67596780
};
67606781

67616782
/* #1558: ui_enabled and ui_port were reachable ONLY by hand-editing
@@ -6781,6 +6802,22 @@ static bool config_key_is_ui(const char *key) {
67816802
return key && (strcmp(key, CBM_CONFIG_UI_ENABLED) == 0 || strcmp(key, CBM_CONFIG_UI_PORT) == 0);
67826803
}
67836804

6805+
static bool config_key_is_index_policy(const char *key) {
6806+
return key && (strcmp(key, CBM_INDEX_CONFIG_MAX_FILES) == 0 ||
6807+
strcmp(key, CBM_INDEX_CONFIG_MAX_SOURCE_MB) == 0);
6808+
}
6809+
6810+
static int config_index_policy_write(cbm_config_t *config, const char *key, const char *value) {
6811+
cbm_index_resource_policy_t candidate;
6812+
cbm_index_policy_init(&candidate);
6813+
char error[CLI_BUF_256];
6814+
if (!cbm_index_policy_set(&candidate, key, value, error, sizeof(error))) {
6815+
(void)fprintf(stderr, "error: %s\n", error);
6816+
return CLI_ERR;
6817+
}
6818+
return cbm_config_set(config, key, value);
6819+
}
6820+
67846821
static void config_ui_read(const char *key, char *out, size_t out_sz) {
67856822
cbm_ui_config_t ui;
67866823
cbm_ui_config_load(&ui);
@@ -6918,10 +6955,16 @@ int cbm_cmd_config(int argc, char **argv) {
69186955
rc = CLI_TRUE;
69196956
}
69206957
} else {
6921-
if (cbm_config_set(cfg, argv[CLI_SKIP_ONE], argv[CLI_PAIR_LEN]) == 0) {
6958+
int set_rc =
6959+
config_key_is_index_policy(argv[CLI_SKIP_ONE])
6960+
? config_index_policy_write(cfg, argv[CLI_SKIP_ONE], argv[CLI_PAIR_LEN])
6961+
: cbm_config_set(cfg, argv[CLI_SKIP_ONE], argv[CLI_PAIR_LEN]);
6962+
if (set_rc == 0) {
69226963
printf("%s = %s\n", argv[CLI_SKIP_ONE], argv[CLI_PAIR_LEN]);
69236964
} else {
6924-
(void)fprintf(stderr, "error: failed to set %s\n", argv[CLI_SKIP_ONE]);
6965+
if (!config_key_is_index_policy(argv[CLI_SKIP_ONE])) {
6966+
(void)fprintf(stderr, "error: failed to set %s\n", argv[CLI_SKIP_ONE]);
6967+
}
69256968
rc = CLI_TRUE;
69266969
}
69276970
}

‎src/cli/cli.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
#include <stddef.h>
1414
#include <stdint.h>
1515

16+
#include "foundation/index_policy.h"
17+
1618
typedef struct cbm_mcp_server cbm_mcp_server_t;
1719

1820
/* ── Version ──────────────────────────────────────────────────── */
@@ -417,6 +419,11 @@ int cbm_config_set(cbm_config_t *cfg, const char *key, const char *value);
417419
/* Delete a config key. Returns 0 on success. */
418420
int cbm_config_delete(cbm_config_t *cfg, const char *key);
419421

422+
/* Load and validate the operator-controlled discovery policy. Invalid stored
423+
* values fail closed instead of silently disabling a guard. */
424+
bool cbm_config_load_index_policy(cbm_config_t *cfg, cbm_index_resource_policy_t *policy,
425+
char *error, size_t error_size);
426+
420427
/* Well-known config keys */
421428
#define CBM_CONFIG_AUTO_INDEX "auto_index"
422429
#define CBM_CONFIG_AUTO_INDEX_LIMIT "auto_index_limit"

‎src/daemon/application.c‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,8 @@ static bool application_unique_recovery_file(char out[APPLICATION_PATH_CAP], con
207207
static bool application_update_reap(cbm_daemon_application_t *application, bool wait,
208208
uint32_t timeout_ms);
209209
static void *application_job_thread(void *opaque);
210-
static char *application_auto_index_args(const char *root_path);
210+
static char *application_auto_index_args(cbm_daemon_application_t *application,
211+
const char *root_path);
211212
static cbm_daemon_application_job_t *application_job_subscribe_locked(
212213
cbm_daemon_application_t *application, const char *project_key, const char *root_path,
213214
const char *args_json, application_job_subscribe_status_t *status_out);
@@ -1399,7 +1400,7 @@ static void application_auto_index_retry_pending_locked(cbm_daemon_application_t
13991400
application_refresh_watch_locked(session);
14001401
continue;
14011402
}
1402-
char *args = application_auto_index_args(root_path);
1403+
char *args = application_auto_index_args(application, root_path);
14031404
if (!args) {
14041405
continue;
14051406
}
@@ -1900,15 +1901,36 @@ static bool application_update_reap(cbm_daemon_application_t *application, bool
19001901
}
19011902
}
19021903

1903-
static char *application_auto_index_args(const char *root_path) {
1904+
static bool application_index_args_add_policy(cbm_daemon_application_t *application,
1905+
yyjson_mut_doc *document, yyjson_mut_val *root) {
1906+
cbm_config_t *owned_config = NULL;
1907+
cbm_config_t *config = application ? application->config : NULL;
1908+
if (!config) {
1909+
owned_config = cbm_config_open(cbm_resolve_cache_dir());
1910+
config = owned_config;
1911+
}
1912+
cbm_index_resource_policy_t policy;
1913+
char error[CBM_SZ_256] = {0};
1914+
bool loaded = cbm_config_load_index_policy(config, &policy, error, sizeof(error));
1915+
cbm_config_close(owned_config);
1916+
if (!loaded) {
1917+
cbm_log_error("daemon.index.policy", "error", error);
1918+
return false;
1919+
}
1920+
return cbm_mcp_index_policy_add_to_args(document, root, &policy);
1921+
}
1922+
1923+
static char *application_auto_index_args(cbm_daemon_application_t *application,
1924+
const char *root_path) {
19041925
yyjson_mut_doc *document = yyjson_mut_doc_new(NULL);
19051926
yyjson_mut_val *root = document ? yyjson_mut_obj(document) : NULL;
19061927
if (!document || !root) {
19071928
yyjson_mut_doc_free(document);
19081929
return NULL;
19091930
}
19101931
yyjson_mut_doc_set_root(document, root);
1911-
char *args = yyjson_mut_obj_add_strcpy(document, root, "repo_path", root_path)
1932+
char *args = yyjson_mut_obj_add_strcpy(document, root, "repo_path", root_path) &&
1933+
application_index_args_add_policy(application, document, root)
19121934
? yyjson_mut_write(document, 0, NULL)
19131935
: NULL;
19141936
yyjson_mut_doc_free(document);
@@ -1951,7 +1973,7 @@ static void application_background_initialize_impl(cbm_daemon_application_sessio
19511973
files);
19521974
}
19531975
bool args_required = auto_index_candidate && within_auto_index_limit;
1954-
char *args = args_required ? application_auto_index_args(root_path) : NULL;
1976+
char *args = args_required ? application_auto_index_args(application, root_path) : NULL;
19551977
application_jobs_reap_completed(application);
19561978
cbm_mutex_lock(&application->mutex);
19571979
if (application->stopping || application_request_cancelled_locked(session)) {
@@ -3351,7 +3373,8 @@ static int application_background_index(cbm_daemon_application_t *application,
33513373
return -1;
33523374
}
33533375
yyjson_mut_doc_set_root(document, root);
3354-
bool encoded = yyjson_mut_obj_add_strcpy(document, root, "repo_path", canonical_root);
3376+
bool encoded = yyjson_mut_obj_add_strcpy(document, root, "repo_path", canonical_root) &&
3377+
application_index_args_add_policy(application, document, root);
33553378
char *default_project = cbm_project_name_from_path(canonical_root);
33563379
bool custom_project =
33573380
project_name[0] && (!default_project || strcmp(default_project, project_name) != 0);

0 commit comments

Comments
 (0)