diff --git a/src/cli/cli.c b/src/cli/cli.c index c20b9b584..645331f19 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -5346,17 +5346,28 @@ bool cbm_config_get_bool(cbm_config_t *cfg, const char *key, bool default_val) { return default_val; } -int cbm_config_get_int(cbm_config_t *cfg, const char *key, int default_val) { - const char *val = cbm_config_get(cfg, key, NULL); - if (!val) { - return default_val; +static bool cbm_config_parse_decimal_int(const char *value, int *out) { + if (!value || !out) { + return false; } char *endptr; - long v = strtol(val, &endptr, CLI_STRTOL_BASE); - if (endptr == val || *endptr != '\0') { + errno = 0; + long parsed = strtol(value, &endptr, CLI_STRTOL_BASE); + if (errno == ERANGE || endptr == value || *endptr != '\0' || + parsed < INT_MIN || parsed > INT_MAX) { + return false; + } + *out = (int)parsed; + return true; +} + +int cbm_config_get_int(cbm_config_t *cfg, const char *key, int default_val) { + const char *val = cbm_config_get(cfg, key, NULL); + int parsed = 0; + if (!cbm_config_parse_decimal_int(val, &parsed)) { return default_val; } - return (int)v; + return parsed; } static bool cbm_config_value_matches_enum(const char *range, const char *value) { @@ -5378,7 +5389,19 @@ static bool cbm_config_value_matches_enum(const char *range, const char *value) return false; } +static bool cbm_config_decimal_integer_in_range(const char *value, long minimum, long maximum) { + int parsed = 0; + if (!cbm_config_parse_decimal_int(value, &parsed)) { + return false; + } + return parsed >= minimum && parsed <= maximum; +} + static bool cbm_config_value_is_valid(const char *key, const char *value) { + if (key && strcmp(key, CBM_CONFIG_AUTO_DEP_LIMIT) == 0 && + !cbm_config_decimal_integer_in_range(value, 0, CBM_MAX_AUTO_DEP_LIMIT)) { + return false; + } for (size_t i = 0; CBM_CONFIG_REGISTRY[i].key; i++) { if (strcmp(CBM_CONFIG_REGISTRY[i].key, key) == 0) { return cbm_config_value_matches_enum(CBM_CONFIG_REGISTRY[i].range, value); @@ -10538,7 +10561,7 @@ const cbm_config_entry_t CBM_CONFIG_REGISTRY[] = { "future automatic dependency indexing; it does not delete dependency projects already indexed."}, {CBM_CONFIG_AUTO_DEP_LIMIT, CBM_STRINGIFY(CBM_DEFAULT_AUTO_DEP_LIMIT), NULL, "Dependencies", "Max number of packages to auto-index", - "0-10000", + "0-" CBM_STRINGIFY(CBM_MAX_AUTO_DEP_LIMIT), "When more packages are installed than this limit, the most-imported packages are selected " "(ranked by project import references, ties broken by name). Raise to 100+ for comprehensive " "dependency analysis. 0 = unlimited (may be very slow for large dependency trees)."}, @@ -10590,12 +10613,11 @@ int cbm_config_get_effective_int(cbm_config_t *cfg, const char *key, int default if (!val || !val[0]) { return default_val; } - char *endptr; - long parsed = strtol(val, &endptr, CLI_STRTOL_BASE); - if (endptr == val || *endptr != '\0') { + int parsed = 0; + if (!cbm_config_parse_decimal_int(val, &parsed)) { return default_val; } - return (int)parsed; + return parsed; } /* ── Config CLI subcommand ────────────────────────────────────── */ diff --git a/src/depindex/depindex.c b/src/depindex/depindex.c index d73e09a8f..a39b44795 100644 --- a/src/depindex/depindex.c +++ b/src/depindex/depindex.c @@ -739,19 +739,27 @@ int cbm_discover_installed_deps(cbm_pkg_manager_t mgr, const char *project_root, /* ── Auto-Index ────────────────────────────────────────────────── */ +int cbm_dep_normalize_configured_limit(int limit, int default_limit) { + if (limit == 0) { + return -1; + } + if (limit > 0 && limit <= CBM_MAX_AUTO_DEP_LIMIT) { + return limit; + } + return default_limit > 0 && default_limit <= CBM_MAX_AUTO_DEP_LIMIT + ? default_limit + : CBM_DEFAULT_AUTO_DEP_LIMIT; +} + int cbm_dep_auto_index_effective_limit(cbm_config_t *cfg, int default_limit) { - if (!cbm_config_get_bool(cfg, CBM_CONFIG_AUTO_INDEX_DEPS, - CBM_DEFAULT_AUTO_INDEX_DEPS)) { + if (!cbm_config_get_bool(cfg, CBM_CONFIG_AUTO_INDEX_DEPS, CBM_DEFAULT_AUTO_INDEX_DEPS)) { return 0; } int limit = cbm_config_get_int(cfg, CBM_CONFIG_AUTO_DEP_LIMIT, default_limit); /* The direct API keeps max_deps=0 as disabled. The config registry documents * auto_dep_limit=0 as unlimited, so map configured callers to -1 here. */ - if (limit <= 0) { - return -1; - } - return limit; + return cbm_dep_normalize_configured_limit(limit, default_limit); } /* Auto-detect ecosystem, discover deps, index each via flush_to_store. diff --git a/src/depindex/depindex.h b/src/depindex/depindex.h index f37e4975c..39df0a6b7 100644 --- a/src/depindex/depindex.h +++ b/src/depindex/depindex.h @@ -48,6 +48,7 @@ static const char *CBM_MANIFEST_FILES[] = { #define CBM_DEFAULT_AUTO_INDEX_DEPS false #define CBM_DEFAULT_AUTO_INDEX_DEPS_STR "false" #define CBM_DEFAULT_AUTO_DEP_LIMIT 20 +#define CBM_MAX_AUTO_DEP_LIMIT 10000 #define CBM_DEFAULT_DEP_MAX_FILES 1000 /* Config key strings */ @@ -163,4 +164,8 @@ int cbm_dep_link_cross_edges(cbm_store_t *store, const char *project_name); * Return 0 to disable, -1 for unlimited, or a positive package count. */ int cbm_dep_auto_index_effective_limit(cbm_config_t *cfg, int default_limit); +/* Normalize an enabled auto_dep_limit value: 0 is unlimited, valid positive + * values are retained, and out-of-range values fall back to a bounded default. */ +int cbm_dep_normalize_configured_limit(int limit, int default_limit); + #endif /* CBM_DEPINDEX_H */ diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index b794103b4..cb012398e 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -1052,8 +1052,10 @@ static const tool_def_t TOOLS[] = { "Use [\\\"*\\\"] for all indexed projects. Run list_projects to see available projects.\"}," "\"auto_index_deps\":{\"type\":\"boolean\",\"description\":" "\"Set false to skip dependency package indexing for this call. Default follows config auto_index_deps.\"}," - "\"auto_dep_limit\":{\"type\":\"integer\",\"description\":" - "\"Dependency package cap for this call. Default follows config auto_dep_limit; 0 means unlimited.\"}," + "\"auto_dep_limit\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":" + CBM_STRINGIFY(CBM_MAX_AUTO_DEP_LIMIT) ",\"description\":" + "\"Dependency package cap for this call from 1 to " CBM_STRINGIFY(CBM_MAX_AUTO_DEP_LIMIT) + ". Default follows config auto_dep_limit; 0 means unlimited.\"}," "\"name\":{\"type\":\"string\",\"description\":" "\"Override the derived project name. Non-ASCII bytes are encoded and unsafe path characters " "are normalized.\"}," @@ -2572,7 +2574,7 @@ static int cbm_mcp_effective_auto_dep_limit(cbm_mcp_server_t *srv, const char *a if (cbm_mcp_has_arg(args_json, CBM_CONFIG_AUTO_DEP_LIMIT)) { limit = cbm_mcp_get_int_arg(args_json, CBM_CONFIG_AUTO_DEP_LIMIT, limit); } - return limit <= 0 ? -1 : limit; + return cbm_dep_normalize_configured_limit(limit, CBM_DEFAULT_AUTO_DEP_LIMIT); } /* Query routes deliberately cache read-only stores. Mutation routes must never diff --git a/tests/test_cli.c b/tests/test_cli.c index b98ab83b5..96d0e10d6 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -6879,6 +6879,10 @@ TEST(cli_config_get_int) { cbm_config_set(cfg, "limit", "abc"); ASSERT_EQ(cbm_config_get_int(cfg, "limit", 50000), 50000); + /* Values outside int range must not wrap into a valid limit. */ + cbm_config_set(cfg, "limit", "999999999999999999999999"); + ASSERT_EQ(cbm_config_get_int(cfg, "limit", 50000), 50000); + cbm_config_close(cfg); test_rmdir_r(tmpdir); PASS(); @@ -6962,6 +6966,9 @@ TEST(cli_config_registry_auto_dep_limit_uses_shared_default) { ASSERT_NOT_NULL(found); ASSERT_EQ(atoi(found->default_val), CBM_DEFAULT_AUTO_DEP_LIMIT); + char expected_range[CBM_SZ_32]; + snprintf(expected_range, sizeof(expected_range), "0-%d", CBM_MAX_AUTO_DEP_LIMIT); + ASSERT_STR_EQ(found->range, expected_range); PASS(); } diff --git a/tests/test_depindex.c b/tests/test_depindex.c index 8c44e1f9a..f3d91eb56 100644 --- a/tests/test_depindex.c +++ b/tests/test_depindex.c @@ -1090,6 +1090,18 @@ TEST(test_auto_index_deps_config_limit_policy) { ASSERT_EQ(cbm_dep_auto_index_effective_limit(cfg, CBM_DEFAULT_AUTO_DEP_LIMIT), 7); ASSERT_EQ(cbm_config_set(cfg, CBM_CONFIG_AUTO_DEP_LIMIT, "0"), 0); ASSERT_EQ(cbm_dep_auto_index_effective_limit(cfg, CBM_DEFAULT_AUTO_DEP_LIMIT), -1); + ASSERT_EQ( + cbm_dep_normalize_configured_limit(CBM_MAX_AUTO_DEP_LIMIT, CBM_DEFAULT_AUTO_DEP_LIMIT), + CBM_MAX_AUTO_DEP_LIMIT); + ASSERT_EQ(cbm_dep_normalize_configured_limit(-1, CBM_DEFAULT_AUTO_DEP_LIMIT), + CBM_DEFAULT_AUTO_DEP_LIMIT); + ASSERT_EQ( + cbm_dep_normalize_configured_limit(CBM_MAX_AUTO_DEP_LIMIT + 1, CBM_MAX_AUTO_DEP_LIMIT + 1), + CBM_DEFAULT_AUTO_DEP_LIMIT); + ASSERT_NEQ(cbm_config_set(cfg, CBM_CONFIG_AUTO_DEP_LIMIT, "-1"), 0); + char over_limit[CBM_SZ_32]; + snprintf(over_limit, sizeof(over_limit), "%d", CBM_MAX_AUTO_DEP_LIMIT + 1); + ASSERT_NEQ(cbm_config_set(cfg, CBM_CONFIG_AUTO_DEP_LIMIT, over_limit), 0); cbm_config_close(cfg); cleanup_fixture_dir(cache_tmp); diff --git a/tests/test_mcp.c b/tests/test_mcp.c index f2bdba876..421b4c0d8 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -610,6 +610,21 @@ TEST(mcp_canonical_input_schemas_cover_implemented_format_and_verbose_options) { PASS(); } +TEST(mcp_index_repository_auto_dep_limit_schema_uses_shared_bounds) { + const char *schema_json = cbm_mcp_tool_input_schema("index_repository"); + ASSERT_NOT_NULL(schema_json); + yyjson_doc *doc = yyjson_read(schema_json, strlen(schema_json), 0); + ASSERT_NOT_NULL(doc); + yyjson_val *properties = yyjson_obj_get(yyjson_doc_get_root(doc), "properties"); + ASSERT_TRUE(yyjson_is_obj(properties)); + yyjson_val *limit = yyjson_obj_get(properties, CBM_CONFIG_AUTO_DEP_LIMIT); + ASSERT_TRUE(yyjson_is_obj(limit)); + ASSERT_EQ(yyjson_get_int(yyjson_obj_get(limit, "minimum")), 0); + ASSERT_EQ(yyjson_get_int(yyjson_obj_get(limit, "maximum")), CBM_MAX_AUTO_DEP_LIMIT); + yyjson_doc_free(doc); + PASS(); +} + TEST(mcp_tools_have_behavior_annotations) { struct { const char *name; @@ -12726,6 +12741,7 @@ SUITE(mcp) { RUN_TEST(mcp_tools_list_latest_metadata); RUN_TEST(mcp_tool_input_schemas_are_closed_in_classic_and_streamlined_modes); RUN_TEST(mcp_canonical_input_schemas_cover_implemented_format_and_verbose_options); + RUN_TEST(mcp_index_repository_auto_dep_limit_schema_uses_shared_bounds); RUN_TEST(mcp_tools_have_behavior_annotations); RUN_TEST(mcp_index_repository_declares_name_override_issue571); RUN_TEST(mcp_tools_array_schemas_have_items);