Skip to content

Commit c8e7f7e

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. Both keys reach an operator through the existing config get/set/list/reset with no new subcommand. `set` suppresses its own generic message for them because the policy writer names the precise reason -- so that writer speaks on every failure it can return, including a validated value whose write then fails on a database that cannot be written. Exiting non-zero in silence is not an acceptable answer from a CLI. 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 aa44c28 commit c8e7f7e

25 files changed

Lines changed: 1371 additions & 44 deletions

‎Makefile.cbm‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ FOUNDATION_SRCS = \
251251
src/foundation/profile.c \
252252
src/foundation/dump_verify.c \
253253
src/foundation/limits.c \
254+
src/foundation/index_policy.c \
254255
src/foundation/subprocess.c \
255256
src/foundation/sha256.c \
256257
src/foundation/secure_random.c \
@@ -532,6 +533,7 @@ TEST_FOUNDATION_SRCS = \
532533
tests/test_str_intern.c \
533534
tests/test_log.c \
534535
tests/test_str_util.c \
536+
tests/test_index_policy.c \
535537
tests/test_workspace.c \
536538
tests/test_platform.c \
537539
tests/test_diagnostics.c \

‎README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,9 +728,15 @@ codebase-memory-mcp config set auto_index true # auto-index on session
728728
codebase-memory-mcp config set auto_index_limit 50000 # max files for auto-index
729729
codebase-memory-mcp config set auto_watch false # don't register background git watcher (default: true)
730730
codebase-memory-mcp config set watcher_enabled false # stop the watcher thread entirely (default: true)
731+
codebase-memory-mcp config set index_max_files 250000 # optional per-index source-file limit
732+
codebase-memory-mcp config set index_max_source_mb 16384 # optional per-index source-size limit
731733
codebase-memory-mcp config reset auto_index # reset to default
732734
```
733735

736+
The two `index_max_*` settings default to `off`. Exceeding one fails the complete
737+
index attempt rather than publishing a partial graph; an existing serving index
738+
is preserved. See [Index resource limits](docs/INDEX_RESOURCE_LIMITS.md).
739+
734740
### Environment Variables
735741

736742
| Variable | Default | Description |

‎docs/CONFIGURATION.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ Current keys:
8989
| `auto_index_limit` | `50000` | Maximum file count allowed for automatic indexing of a new project. |
9090
| `auto_watch` | `true` | Register the session's project with the background git watcher on connect. Set `false` to keep a session from registering its project (the watcher still runs for other projects). |
9191
| `watcher_enabled` | `true` | Master switch for the background watcher subsystem. Set `false` to stop the watcher from starting at all — no poll thread and no project registration. Reindex manually with `index_repository` when disabled. |
92+
| `index_max_files` | `off` | Optional maximum number of accepted source files in one discovery run. |
93+
| `index_max_source_mb` | `off` | Optional maximum accepted source size in MiB in one discovery run. |
9294

9395
> **`watcher_enabled` vs `auto_watch`.** `watcher_enabled` controls whether the
9496
> watcher *subsystem* starts at all (the background poll thread). `auto_watch` is
@@ -116,6 +118,14 @@ Current keys:
116118
> `auto_index` still runs, and `index_repository` stays available for manual
117119
> reindexing.
118120
121+
The two `index_max_*` settings are independent and disabled by default. They
122+
apply to explicit indexing, automatic indexing, and watcher re-indexing, but not
123+
to `cross-repo-intelligence`, which does not scan repository source files.
124+
Equality is allowed; exceeding either setting fails the complete index request
125+
and preserves any previously serving database. See
126+
[Index resource limits](INDEX_RESOURCE_LIMITS.md) for counting, validation, and
127+
error-response details.
128+
119129
## 3. UI Settings
120130
121131
The optional built-in graph UI stores its settings in:

‎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: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7007,6 +7007,25 @@ bool cbm_config_watcher_enabled(cbm_config_t *cfg) {
70077007
return cbm_config_get_bool(cfg, CBM_CONFIG_WATCHER_ENABLED, true);
70087008
}
70097009

7010+
bool cbm_config_load_index_policy(cbm_config_t *cfg, cbm_index_resource_policy_t *policy,
7011+
char *error, size_t error_size) {
7012+
if (!cfg || !policy) {
7013+
if (error && error_size > 0) {
7014+
(void)snprintf(error, error_size, "index resource configuration is unavailable");
7015+
}
7016+
return false;
7017+
}
7018+
cbm_index_policy_init(policy);
7019+
for (size_t index = 0; index < cbm_index_policy_key_count(); index++) {
7020+
const char *key = cbm_index_policy_key_at(index);
7021+
const char *value = cbm_config_get(cfg, key, cbm_index_policy_default_value(key));
7022+
if (!cbm_index_policy_set(policy, key, value, error, error_size)) {
7023+
return false;
7024+
}
7025+
}
7026+
return true;
7027+
}
7028+
70107029
/* ── Config CLI subcommand ────────────────────────────────────── */
70117030

70127031
/* THE config-key table. list, get, help, and key validation all read this one
@@ -7031,6 +7050,8 @@ static const config_key_def_t CONFIG_KEYS[] = {
70317050
{CBM_CONFIG_UI_LANG, "auto", "Pin graph UI language: en, zh, or auto"},
70327051
{CBM_CONFIG_UI_ENABLED, "false", "Serve the graph UI on a loopback HTTP port"},
70337052
{CBM_CONFIG_UI_PORT, "9749", "Port for the graph UI listener when enabled"},
7053+
{CBM_INDEX_CONFIG_MAX_FILES, "off", "Max accepted source files per index, or off"},
7054+
{CBM_INDEX_CONFIG_MAX_SOURCE_MB, "off", "Max accepted source MiB per index, or off"},
70347055
};
70357056

70367057
/* #1558: ui_enabled and ui_port were reachable ONLY by hand-editing
@@ -7056,6 +7077,31 @@ static bool config_key_is_ui(const char *key) {
70567077
return key && (strcmp(key, CBM_CONFIG_UI_ENABLED) == 0 || strcmp(key, CBM_CONFIG_UI_PORT) == 0);
70577078
}
70587079

7080+
static bool config_key_is_index_policy(const char *key) {
7081+
return key && (strcmp(key, CBM_INDEX_CONFIG_MAX_FILES) == 0 ||
7082+
strcmp(key, CBM_INDEX_CONFIG_MAX_SOURCE_MB) == 0);
7083+
}
7084+
7085+
static int config_index_policy_write(cbm_config_t *config, const char *key, const char *value) {
7086+
cbm_index_resource_policy_t candidate;
7087+
cbm_index_policy_init(&candidate);
7088+
char error[CLI_BUF_256];
7089+
if (!cbm_index_policy_set(&candidate, key, value, error, sizeof(error))) {
7090+
(void)fprintf(stderr, "error: %s\n", error);
7091+
return CLI_ERR;
7092+
}
7093+
int rc = cbm_config_set(config, key, value);
7094+
if (rc != 0) {
7095+
/* The caller suppresses its own message for policy keys because this
7096+
* helper names the precise reason. That is only true if the helper
7097+
* speaks on every failure it can return: a validated value whose write
7098+
* then fails -- a locked or read-only _config.db -- used to exit
7099+
* non-zero having printed nothing at all. */
7100+
(void)fprintf(stderr, "error: failed to set %s\n", key);
7101+
}
7102+
return rc;
7103+
}
7104+
70597105
static void config_ui_read(const char *key, char *out, size_t out_sz) {
70607106
cbm_ui_config_t ui;
70617107
cbm_ui_config_load(&ui);
@@ -7193,10 +7239,16 @@ int cbm_cmd_config(int argc, char **argv) {
71937239
rc = CLI_TRUE;
71947240
}
71957241
} else {
7196-
if (cbm_config_set(cfg, argv[CLI_SKIP_ONE], argv[CLI_PAIR_LEN]) == 0) {
7242+
int set_rc =
7243+
config_key_is_index_policy(argv[CLI_SKIP_ONE])
7244+
? config_index_policy_write(cfg, argv[CLI_SKIP_ONE], argv[CLI_PAIR_LEN])
7245+
: cbm_config_set(cfg, argv[CLI_SKIP_ONE], argv[CLI_PAIR_LEN]);
7246+
if (set_rc == 0) {
71977247
printf("%s = %s\n", argv[CLI_SKIP_ONE], argv[CLI_PAIR_LEN]);
71987248
} else {
7199-
(void)fprintf(stderr, "error: failed to set %s\n", argv[CLI_SKIP_ONE]);
7249+
if (!config_key_is_index_policy(argv[CLI_SKIP_ONE])) {
7250+
(void)fprintf(stderr, "error: failed to set %s\n", argv[CLI_SKIP_ONE]);
7251+
}
72007252
rc = CLI_TRUE;
72017253
}
72027254
}

‎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 ──────────────────────────────────────────────────── */
@@ -420,6 +422,11 @@ int cbm_config_set(cbm_config_t *cfg, const char *key, const char *value);
420422
/* Delete a config key. Returns 0 on success. */
421423
int cbm_config_delete(cbm_config_t *cfg, const char *key);
422424

425+
/* Load and validate the operator-controlled discovery policy. Invalid stored
426+
* values fail closed instead of silently disabling a guard. */
427+
bool cbm_config_load_index_policy(cbm_config_t *cfg, cbm_index_resource_policy_t *policy,
428+
char *error, size_t error_size);
429+
423430
/* Well-known config keys */
424431
#define CBM_CONFIG_AUTO_INDEX "auto_index"
425432
#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
@@ -208,7 +208,8 @@ static bool application_unique_recovery_file(char out[APPLICATION_PATH_CAP], con
208208
static bool application_update_reap(cbm_daemon_application_t *application, bool wait,
209209
uint32_t timeout_ms);
210210
static void *application_job_thread(void *opaque);
211-
static char *application_auto_index_args(const char *root_path);
211+
static char *application_auto_index_args(cbm_daemon_application_t *application,
212+
const char *root_path);
212213
static cbm_daemon_application_job_t *application_job_subscribe_locked(
213214
cbm_daemon_application_t *application, const char *project_key, const char *root_path,
214215
const char *args_json, application_job_subscribe_status_t *status_out);
@@ -1430,7 +1431,7 @@ static void application_auto_index_retry_pending_locked(cbm_daemon_application_t
14301431
application_refresh_watch_locked(session);
14311432
continue;
14321433
}
1433-
char *args = application_auto_index_args(root_path);
1434+
char *args = application_auto_index_args(application, root_path);
14341435
if (!args) {
14351436
continue;
14361437
}
@@ -1964,15 +1965,36 @@ static bool application_update_reap(cbm_daemon_application_t *application, bool
19641965
}
19651966
}
19661967

1967-
static char *application_auto_index_args(const char *root_path) {
1968+
static bool application_index_args_add_policy(cbm_daemon_application_t *application,
1969+
yyjson_mut_doc *document, yyjson_mut_val *root) {
1970+
cbm_config_t *owned_config = NULL;
1971+
cbm_config_t *config = application ? application->config : NULL;
1972+
if (!config) {
1973+
owned_config = cbm_config_open(cbm_resolve_cache_dir());
1974+
config = owned_config;
1975+
}
1976+
cbm_index_resource_policy_t policy;
1977+
char error[CBM_SZ_256] = {0};
1978+
bool loaded = cbm_config_load_index_policy(config, &policy, error, sizeof(error));
1979+
cbm_config_close(owned_config);
1980+
if (!loaded) {
1981+
cbm_log_error("daemon.index.policy", "error", error);
1982+
return false;
1983+
}
1984+
return cbm_mcp_index_policy_add_to_args(document, root, &policy);
1985+
}
1986+
1987+
static char *application_auto_index_args(cbm_daemon_application_t *application,
1988+
const char *root_path) {
19681989
yyjson_mut_doc *document = yyjson_mut_doc_new(NULL);
19691990
yyjson_mut_val *root = document ? yyjson_mut_obj(document) : NULL;
19701991
if (!document || !root) {
19711992
yyjson_mut_doc_free(document);
19721993
return NULL;
19731994
}
19741995
yyjson_mut_doc_set_root(document, root);
1975-
char *args = yyjson_mut_obj_add_strcpy(document, root, "repo_path", root_path)
1996+
char *args = yyjson_mut_obj_add_strcpy(document, root, "repo_path", root_path) &&
1997+
application_index_args_add_policy(application, document, root)
19761998
? yyjson_mut_write(document, 0, NULL)
19771999
: NULL;
19782000
yyjson_mut_doc_free(document);
@@ -2019,7 +2041,7 @@ static void application_background_initialize_impl(cbm_daemon_application_sessio
20192041
files);
20202042
}
20212043
bool args_required = auto_index_candidate && within_auto_index_limit;
2022-
char *args = args_required ? application_auto_index_args(root_path) : NULL;
2044+
char *args = args_required ? application_auto_index_args(application, root_path) : NULL;
20232045
application_jobs_reap_completed(application);
20242046
cbm_mutex_lock(&application->mutex);
20252047
if (application->stopping || application_request_cancelled_locked(session)) {
@@ -3416,7 +3438,8 @@ static int application_background_index(cbm_daemon_application_t *application,
34163438
return -1;
34173439
}
34183440
yyjson_mut_doc_set_root(document, root);
3419-
bool encoded = yyjson_mut_obj_add_strcpy(document, root, "repo_path", canonical_root);
3441+
bool encoded = yyjson_mut_obj_add_strcpy(document, root, "repo_path", canonical_root) &&
3442+
application_index_args_add_policy(application, document, root);
34203443
char *default_project = cbm_project_name_from_path(canonical_root);
34213444
bool custom_project =
34223445
project_name[0] && (!default_project || strcmp(default_project, project_name) != 0);

0 commit comments

Comments
 (0)