feat(index): add opt-in discovery resource limits - #1723
Conversation
|
Thanks for opening this — it has been seen, and it is queued. This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence. Current review status: working through a backlog. What that means for this PR, concretely:
Things that will genuinely speed it up whenever review does happen:
If this fixes a bug, a reproduction we can run is worth more than a description of the symptom. Thanks for contributing, and sorry in advance for the wait. |
7a9fbef to
2a24781
Compare
|
This fixes exactly what I blocked #1348 on, and I verified it rather than taking the claim. One small defect and I will merge. Default-off is now true by construction, not by convention. I traced all four gates:
Two config keys instead of eighteen. Three things you hardened beyond what I asked, which are worth naming:
And Worth noting: because every new test calls a symbol that does not exist on The one fix before merge — a silent failure.
if (!cbm_index_policy_set(&candidate, key, value, error, sizeof(error))) {
(void)fprintf(stderr, "error: %s\n", error);
return CLI_ERR;
}
return cbm_config_set(config, key, value);but the caller now suppresses the generic message for policy keys: if (!config_key_is_index_policy(argv[CLI_SKIP_ONE])) {
(void)fprintf(stderr, "error: failed to set %s\n", argv[CLI_SKIP_ONE]);
}So if validation passes and the Two things to confirm rather than change:
And one completeness check I would like your eye on, because it is the failure mode your own commit message warns about: the worker argv contract is now mandatory and fail-closed, so any harness that spawns Fix the CLI message and I will merge this. Thank you for taking the reshaping seriously instead of arguing for the original — this is a much better change than #1348 was, and the trust boundary is stronger than what I asked for. |
41097a2 to
3174313
Compare
|
Fixed, and the three checks confirmed. The silent write failure is fixed. int rc = cbm_config_set(config, key, value);
if (rc != 0) {
(void)fprintf(stderr, "error: failed to set %s\n", key);
}
return rc;
1. Fail-closed on the unattended path — deliberate, and specifically for that path. With no policy loadable there is no bound to enforce, and unattended is exactly where an unbounded index is least survivable. The cost is the one you named: nobody reads that log line. #1727 and #1728 are what close that gap — which is also why I understand judging them separately. 2. if (fl->max_files >= 0 && fl->count >= fl->max_files) {
fl->capped = true;
return;
}with 3. Worker argv contract — confirmed complete. Your enumeration matches mine. Every production spawn goes through the strip-and-inject in On the branch. Rebased onto current |
|
Maintainer decision: the concept is accepted. Opt-in indexing resource limits are something this project wants, and #1723–#1728 can proceed as a programme rather than needing to argue the direction again at each slice. Two things decided that, beyond the code: The problem is one we have actually hit, not a hypothetical. Indexing here has driven a host into OOM before, and "the first sign of trouble is a host under memory pressure with nothing that attributes it to indexing" is an accurate description of what that looks like from the outside. And the default-off, opt-in shape means the surface costs nothing until someone asks for it. A feature that changes no behaviour unconfigured is a very different proposition from one that imposes a policy. The trust boundary is the part that earned the most confidence. Reading limits from the CLI-managed What happens nextEach slice still gets reviewed on its own merits. Accepting the programme is not accepting six diffs in advance — and I would expect slices 3 through 6 (storage limits, resource profiles, attempt records, stale-index warnings) to get closer attention than this one, because that is where the configuration surface actually grows. Slice 1 is the cheapest and most self-contained of the six. Please rebase this onto current Do not chase the One process note so it does not surprise you later: this PR touches Thank you for splitting #1348 into six independently reviewable pieces and for saying plainly which ones depend on which. That is what makes accepting the direction possible without accepting everything sight-unseen. |
3174313 to
b769ad6
Compare
|
Rebased onto current No code changes beyond the rebase merge: the watcher keys that landed on |
|
Reviewed on merit. The concept was accepted previously; this is the review of the slice itself, and the safety properties are the part I checked hardest. Opt-in and default-off, so the no-config path is unchanged. That is what makes this safe to take at all — a limit that defaults to a value is a behaviour change for every existing user; one that defaults to Counting only accepted source files is the right denominator. Charging an operator's budget for paths their ignore rules already excluded would make the limit mean something different from what the key says, and would make it unpredictable as ignore rules change. Failing the whole attempt rather than publishing a partial graph is the decision I would have argued for, and your justification is exactly right:
That is the same principle behind several other fixes landing this week — a wrong answer that looks authoritative costs more than a refusal. Keeping an existing serving index answering while the new attempt fails is the other half of getting this right. The policy provenance is the part I looked at most closely, because "operator sets a limit" and "caller sets a limit" are very different things. Reading limits from the CLI-managed And the worker fails closed. The comment in MechanicsYour two clearance findings are You are 31 commits behind Merging on that green. Thank you — this is a carefully built change, and stating the operator-versus-caller distinction in the description saved me a great deal of tracing. |
|
Quick process question before I touch anything — mainly so I do not disrupt your merge cadence or burn CI for no reason. I can bring the branches up to current This branch is ~158 commits behind Separately, for |
83ca40c to
c8e7f7e
Compare
|
I noticed No intentional feature changes in this refresh — just conflict resolution against what landed on CI is restarting on the new heads. |
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>
c8e7f7e to
21ae021
Compare
|
@DeusData rebased One real conflict worth naming:
Quick process ask: what is the merge plan for this slice now? You had already reviewed it on merit and previously said you would merge on green, but |
|
You asked a direct process question on 2026-09-06 and got eleven days of silence while continuing to rebase this branch four times to keep it mergeable. That is the wrong way round, and I am sorry. Answering the question first, since it is the one that costs you time: please do not rebase proactively from here. I will tell you when a rebase is actually needed. You have been absorbing the cost of every move on Status: approved and queued, and the blocker is on our side. The slice was reviewed on merit on 2026-09-02, the concept was accepted for #1723–#1728 as a programme, and it currently reads MERGEABLE / CLEAN with 34 checks green. What is holding it is our build-clearance gate, not your code. This PR edits On the constant collision you named — you made the right call, and I verified it rather than taking your word for it. #define CBM_PIPELINE_ABORT_OVER_BUDGET (-5)and your branch now uses On stack order: #1723 is the base and lands first; #1724–#1728 follow in sequence once it does. Please leave the upper slices alone until then — rebasing them onto a base that has not landed is work that gets thrown away. Thank you for the patience and for the care taken on every one of those rebases. |
|
Merged as 92abefa. Before merging I built and tested the actual merge result against current Thank you for the four rebases you should never have had to do, and for the |
main fails scripts/test.sh at Step 5f on every platform:
=== Step 5f: worker request-scope regression ===
worker did not index the admitted repository
{"content":[{"type":"text","text":"missing or incomplete trusted
worker policy"}], ... "isError":true}
Second collision from the #1723 merge (92abefa), and the same kind as
the first. #1723 made an index worker refuse any request that does not
carry `_cbm_index_policy`: a worker never resolves the resource policy
from config or environment, the supervisor does and sends it along.
tests/test_worker_session_scope.sh, added on 2026-09-06 by a08b9ec,
plays the supervisor by hand -- it runs `cli --index-worker
index_repository '{"repo_path":...,"mode":"fast"}'` directly -- and
#1723's branch never contained that script, so its CI never ran the two
together. The merge was textually clean and the result was wrong. It
stayed hidden behind the lint failure the previous commit fixes, because
the test stage only runs once lint is green.
The worker is right and the script was incomplete: it now sends what the
supervisor sends, both limits off, which is the default
(`"index_max_files":"off","index_max_source_mb":"off"`, the same object
tests/test_index_policy.c uses). The fail-closed case carries it too, so
that a request without repo_path is still refused for its missing scope
("request workspace scope invalid") and not, by accident, for a missing
policy. No assertion changed.
RED, production binary built from this branch, script unmodified:
worker did not index the admitted repository
... "missing or incomplete trusted worker policy" ... rc=1
GREEN:
ok: index worker is scoped to the admitted request, not the daemon
environment rc=0
On the failed CI run of this PR, 12 of the 13 red test jobs carry exactly
that signature and nothing else. The 13th, test-windows-guards, is
test_daemon_stability.py section_cold_storm ("secure CLI coordination
could not be created (endpoint)", a racing cold start) -- a known
nondeterministic guard, unrelated to either commit here. Step 6, which
Step 5f's failure kept CI from reaching, passes locally:
security-strings allow-list 4 passed, destructive-ordering contract PASS.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
fix: restore main to green after #1723 — memory-core linter and worker-scope Step 5f
…rings
main fails the memory-core linter:
src/mcp/mcp.c: grew by 3 (814 -> 817)
PR DeusData#1723 (merged as 92abefa) added a new early-return path to
handle_index_repository and, following the function's convention, freed
its three argument strings there with raw free(). Its CI lint was green
on 2026-09-09. On 2026-09-19 the memwaste merge cut this file's raw sites
and ratcheted the baseline down to 814, so by the time DeusData#1723 landed its
green was measured against a rule that no longer existed. The linter is
right and the merge-result check that let it through was incomplete: it
built and ran the suites and did not run the linter. It does now.
The fix is not cbm_free. The three strings come from
cbm_mcp_get_string_arg and resolved_repo_path_from_project_arg and are
raw heap memory, released raw by every other path in the function;
routing three of them through the core would free raw memory through the
accounting layer. The actual defect is that the function had 41 raw
free() calls of the same three arguments across a dozen early returns,
and DeusData#1723 added one more block in the same shape.
One helper, index_args_free(repo_path, mode_str, name_override), now
replaces twelve of those blocks. free(NULL) is a no-op, so the one path
where repo_path is still NULL passes NULL. On the cross-repo branch
mode_str used to be freed before handle_cross_repo_mode and the other
two after it; mode_str is neither passed to nor read by that call, so
releasing all three after it is not observable. Five sites on the
normal-completion path are left as they were: they free the strings at
different points as ownership is handed to the pipeline, and collapsing
them would change that order for no gain.
Raw sites in this file: 817 -> 785. The baseline follows the improvement,
814 -> 785, as the linter asks; it can only tighten.
Build: clean, -Werror, ASan/UBSan. mcp cli daemon_application:
692 passed, 4 skipped, 0 failed -- index_repository_honors_allowed_root,
which runs through a replaced early return, included.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Related to #1347.
Problem
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. There is currently no way for an operator to say "not larger than this".
What this changes
Two opt-in limits, evaluated during discovery against accepted source files only, so ignored and excluded paths never count against an operator's budget:
index_max_filesindex_max_source_mbBoth default to
off; without configuration this PR changes no behaviour.Crossing a limit fails the whole attempt with a structured
resource_limit_exceededresult naming the resource, the observed value and the limit. No partial graph is published, and an existing serving index keeps answering. A partial graph would be worse than no graph: it looks complete and is silently wrong.Limits are read from the CLI-managed
_config.db, not from MCP request arguments. A supervised parent replaces any caller-supplied internal 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. A test pins the forged-override path.Testing
make -f Makefile.cbm testandmake -f Makefile.cbm lint-cion macOS. New coverage: discovery counting and rejection, policy parsing and validation boundaries, worker-contract propagation and forged-override rejection, end-to-end preservation of the previously published index.One pre-existing failure (
tests/test_cli.c:6484, agent-client registry) is present on the unmodified base commit as well and is unrelated to this change.Stack
This is the first of six PRs that replace #1348. Each is independently reviewable and mergeable in order; later ones build on earlier ones.