From 49b5aef8fd04d9a037de83923dbc83e657ca879a Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 13:59:54 +0900 Subject: [PATCH 1/3] Bound JSON string-list decoding for #3045 --- changelog.d/unreleased/3045.security.md | 16 ++++ src/CodeIndex/Database/JsonStringListCodec.cs | 79 +++++++++++++++++-- .../JsonStringListCodecTests.cs | 75 ++++++++++++++++++ 3 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 changelog.d/unreleased/3045.security.md create mode 100644 tests/CodeIndex.Tests/JsonStringListCodecTests.cs diff --git a/changelog.d/unreleased/3045.security.md b/changelog.d/unreleased/3045.security.md new file mode 100644 index 0000000000..bbddf52ba2 --- /dev/null +++ b/changelog.d/unreleased/3045.security.md @@ -0,0 +1,16 @@ +--- +category: security +issues: + - 3045 +affected: + - src/CodeIndex/Database/JsonStringListCodec.cs + - tests/CodeIndex.Tests/JsonStringListCodecTests.cs +--- + +## English + +- **Persisted JSON string-list metadata now enforces bounded parsing (#3045)** — string-list metadata now rejects over-deep JSON, oversized arrays, oversized raw payloads, and excessive decoded string content instead of allocating unbounded parsed values from corrupted database rows. + +## 日本語 + +- **永続化 JSON string-list metadata の parse に上限を適用しました (#3045)** — string-list metadata は、壊れた database row から無制限に parse 結果を確保せず、過深な JSON、過大な配列、過大な raw payload、過大な decoded string 内容を拒否するようになりました。 diff --git a/src/CodeIndex/Database/JsonStringListCodec.cs b/src/CodeIndex/Database/JsonStringListCodec.cs index 25b8c93178..397d77a7bb 100644 --- a/src/CodeIndex/Database/JsonStringListCodec.cs +++ b/src/CodeIndex/Database/JsonStringListCodec.cs @@ -5,6 +5,18 @@ namespace CodeIndex.Database; internal static class JsonStringListCodec { + internal const int MaxRawJsonCharacters = 512 * 1024; + internal const int MaxJsonDepth = 4; + internal const int MaxArrayItems = 1024; + internal const int MaxDecodedStringCharacters = 64 * 1024; + + private const int MaxJsonStringBytesPerDecodedChar = 6; + + private static readonly JsonReaderOptions ReaderOptions = new() + { + MaxDepth = MaxJsonDepth, + }; + public static string Serialize(IReadOnlyList values) { using var buffer = new MemoryStream(); @@ -20,32 +32,85 @@ public static string Serialize(IReadOnlyList values) } public static List? Deserialize(string? raw) + => Deserialize(raw, out _); + + internal static List? Deserialize(string? raw, out string? diagnostic) { + diagnostic = null; if (string.IsNullOrWhiteSpace(raw)) return null; + if (raw.Length > MaxRawJsonCharacters) + return Reject("json_string_list_raw_too_large", out diagnostic); + try { - using var document = JsonDocument.Parse(raw); - if (document.RootElement.ValueKind != JsonValueKind.Array) - return null; + var utf8 = Encoding.UTF8.GetBytes(raw); + var reader = new Utf8JsonReader(utf8, ReaderOptions); + if (!reader.Read() || reader.TokenType != JsonTokenType.StartArray) + return Reject("json_string_list_not_array", out diagnostic); var values = new List(); - foreach (var element in document.RootElement.EnumerateArray()) + var itemCount = 0; + var decodedCharacters = 0; + while (reader.Read()) { - if (element.ValueKind != JsonValueKind.String) + if (reader.TokenType == JsonTokenType.EndArray) + { + if (reader.Read()) + return Reject("json_string_list_trailing_json", out diagnostic); + return values; + } + + itemCount++; + if (itemCount > MaxArrayItems) + return Reject("json_string_list_too_many_items", out diagnostic); + + if (reader.TokenType != JsonTokenType.String) + { + if (reader.TokenType is JsonTokenType.StartArray or JsonTokenType.StartObject) + reader.Skip(); + continue; + } + + var remainingCharacters = MaxDecodedStringCharacters - decodedCharacters; + if (ExceedsStringByteBudget(ref reader, remainingCharacters)) + return Reject("json_string_list_too_many_characters", out diagnostic); + + var value = reader.GetString(); + if (value == null) continue; + if (value.Length > remainingCharacters) + return Reject("json_string_list_too_many_characters", out diagnostic); - var value = element.GetString(); + decodedCharacters += value.Length; if (!string.IsNullOrWhiteSpace(value)) values.Add(value); } - return values; + return Reject("json_string_list_incomplete", out diagnostic); } catch (JsonException) { + diagnostic = "json_string_list_malformed"; return null; } } + + private static bool ExceedsStringByteBudget(ref Utf8JsonReader reader, int remainingCharacters) + { + if (remainingCharacters < 0) + return true; + + var byteBudget = (long)remainingCharacters * MaxJsonStringBytesPerDecodedChar; + return reader.HasValueSequence + ? reader.ValueSequence.Length > byteBudget + : reader.ValueSpan.Length > byteBudget; + } + + private static List? Reject(string reason, out string? diagnostic) + { + diagnostic = reason; + return null; + } } diff --git a/tests/CodeIndex.Tests/JsonStringListCodecTests.cs b/tests/CodeIndex.Tests/JsonStringListCodecTests.cs new file mode 100644 index 0000000000..dd6b2f3d98 --- /dev/null +++ b/tests/CodeIndex.Tests/JsonStringListCodecTests.cs @@ -0,0 +1,75 @@ +using CodeIndex.Database; + +namespace CodeIndex.Tests; + +public class JsonStringListCodecTests +{ + [Fact] + public void Deserialize_ValidListReturnsNonBlankStrings() + { + var raw = JsonStringListCodec.Serialize(["alpha", " ", "beta"]); + + var values = JsonStringListCodec.Deserialize(raw, out var diagnostic); + + Assert.Null(diagnostic); + Assert.Equal(["alpha", "beta"], values); + } + + [Fact] + public void Deserialize_NonStringElementsAreIgnoredWithinBounds() + { + var raw = """["alpha",null,42," ","beta",{"name":"ignored"},["nested"]]"""; + + var values = JsonStringListCodec.Deserialize(raw, out var diagnostic); + + Assert.Null(diagnostic); + Assert.Equal(["alpha", "beta"], values); + } + + [Fact] + public void Deserialize_RejectsOverDepthJson() + { + var depth = JsonStringListCodec.MaxJsonDepth + 8; + var raw = new string('[', depth) + "\"value\"" + new string(']', depth); + + var values = JsonStringListCodec.Deserialize(raw, out var diagnostic); + + Assert.Null(values); + Assert.Equal("json_string_list_malformed", diagnostic); + } + + [Fact] + public void Deserialize_RejectsTooManyArrayItems() + { + var raw = "[" + + string.Join(",", Enumerable.Repeat("\"value\"", JsonStringListCodec.MaxArrayItems + 1)) + + "]"; + + var values = JsonStringListCodec.Deserialize(raw, out var diagnostic); + + Assert.Null(values); + Assert.Equal("json_string_list_too_many_items", diagnostic); + } + + [Fact] + public void Deserialize_RejectsTooManyDecodedCharacters() + { + var raw = "[\"" + new string('a', JsonStringListCodec.MaxDecodedStringCharacters + 1) + "\"]"; + + var values = JsonStringListCodec.Deserialize(raw, out var diagnostic); + + Assert.Null(values); + Assert.Equal("json_string_list_too_many_characters", diagnostic); + } + + [Fact] + public void Deserialize_RejectsOversizedRawJson() + { + var raw = "[" + new string(' ', JsonStringListCodec.MaxRawJsonCharacters) + "]"; + + var values = JsonStringListCodec.Deserialize(raw, out var diagnostic); + + Assert.Null(values); + Assert.Equal("json_string_list_raw_too_large", diagnostic); + } +} From 473755879417587a45d9170b6f6b66714f71c239 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 14:20:26 +0900 Subject: [PATCH 2/3] Align JSON string-list writer bounds for #3045 --- changelog.d/unreleased/3045.security.md | 1 + src/CodeIndex/Database/DbWriter.cs | 8 ++--- src/CodeIndex/Database/JsonStringListCodec.cs | 24 +++++++++++++ .../JsonStringListCodecTests.cs | 35 +++++++++++++++++++ 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/changelog.d/unreleased/3045.security.md b/changelog.d/unreleased/3045.security.md index bbddf52ba2..9010715be4 100644 --- a/changelog.d/unreleased/3045.security.md +++ b/changelog.d/unreleased/3045.security.md @@ -4,6 +4,7 @@ issues: - 3045 affected: - src/CodeIndex/Database/JsonStringListCodec.cs + - src/CodeIndex/Database/DbWriter.cs - tests/CodeIndex.Tests/JsonStringListCodecTests.cs --- diff --git a/src/CodeIndex/Database/DbWriter.cs b/src/CodeIndex/Database/DbWriter.cs index 957aad4007..0d7b21b151 100644 --- a/src/CodeIndex/Database/DbWriter.cs +++ b/src/CodeIndex/Database/DbWriter.cs @@ -2135,9 +2135,9 @@ public void WriteUnknownExtensionFileMetadata(IReadOnlyList paths) { ArgumentNullException.ThrowIfNull(paths); - var sample = paths - .Take(DbContext.UnknownExtensionFilePathSampleLimit) - .ToArray(); + var sample = JsonStringListCodec.TakeSerializableSample( + paths, + DbContext.UnknownExtensionFilePathSampleLimit); SetMeta( DbContext.UnknownExtensionFileCountMetaKey, paths.Count.ToString(System.Globalization.CultureInfo.InvariantCulture)); @@ -2146,7 +2146,7 @@ public void WriteUnknownExtensionFileMetadata(IReadOnlyList paths) JsonStringListCodec.Serialize(sample)); SetMeta( DbContext.UnknownExtensionFilesTruncatedMetaKey, - (paths.Count > sample.Length).ToString(System.Globalization.CultureInfo.InvariantCulture)); + (paths.Count > sample.Count).ToString(System.Globalization.CultureInfo.InvariantCulture)); SetMeta( DbContext.UnknownExtensionFilePathLimitMetaKey, DbContext.UnknownExtensionFilePathSampleLimit.ToString(System.Globalization.CultureInfo.InvariantCulture)); diff --git a/src/CodeIndex/Database/JsonStringListCodec.cs b/src/CodeIndex/Database/JsonStringListCodec.cs index 397d77a7bb..51ccb50c0d 100644 --- a/src/CodeIndex/Database/JsonStringListCodec.cs +++ b/src/CodeIndex/Database/JsonStringListCodec.cs @@ -31,6 +31,30 @@ public static string Serialize(IReadOnlyList values) return Encoding.UTF8.GetString(buffer.ToArray()); } + internal static List TakeSerializableSample(IReadOnlyList values, int maxItems) + { + ArgumentNullException.ThrowIfNull(values); + if (maxItems <= 0) + return []; + + var itemLimit = Math.Min(maxItems, MaxArrayItems); + var sample = new List(Math.Min(values.Count, itemLimit)); + var decodedCharacters = 0; + foreach (var value in values) + { + if (sample.Count >= itemLimit) + break; + + if (value.Length > MaxDecodedStringCharacters - decodedCharacters) + break; + + decodedCharacters += value.Length; + sample.Add(value); + } + + return sample; + } + public static List? Deserialize(string? raw) => Deserialize(raw, out _); diff --git a/tests/CodeIndex.Tests/JsonStringListCodecTests.cs b/tests/CodeIndex.Tests/JsonStringListCodecTests.cs index dd6b2f3d98..388380c143 100644 --- a/tests/CodeIndex.Tests/JsonStringListCodecTests.cs +++ b/tests/CodeIndex.Tests/JsonStringListCodecTests.cs @@ -1,7 +1,9 @@ using CodeIndex.Database; +using Microsoft.Data.Sqlite; namespace CodeIndex.Tests; +[Collection("SQLite pool sensitive")] public class JsonStringListCodecTests { [Fact] @@ -72,4 +74,37 @@ public void Deserialize_RejectsOversizedRawJson() Assert.Null(values); Assert.Equal("json_string_list_raw_too_large", diagnostic); } + + [Fact] + public void WriteUnknownExtensionFileMetadata_CapsSampleWithinReaderBudget() + { + var dbPath = Path.Combine(Path.GetTempPath(), $"codeindex_json_string_list_{Guid.NewGuid():N}.db"); + try + { + using var db = new DbContext(dbPath); + db.InitializeSchema(); + var writer = new DbWriter(db.Connection); + var longPathPrefix = new string('a', 2048); + var paths = Enumerable.Range(0, DbContext.UnknownExtensionFilePathSampleLimit) + .Select(i => $"{longPathPrefix}{i:D2}.mystery") + .ToArray(); + Assert.True(paths.Sum(path => path.Length) > JsonStringListCodec.MaxDecodedStringCharacters); + + writer.WriteUnknownExtensionFileMetadata(paths); + + var status = new DbReader(db.Connection).GetStatus(); + var sample = Assert.IsType>(status.UnknownExtensionFiles); + Assert.NotEmpty(sample); + Assert.True(sample.Sum(path => path.Length) <= JsonStringListCodec.MaxDecodedStringCharacters); + Assert.True(status.UnknownExtensionFilesTruncated); + Assert.Equal((long)DbContext.UnknownExtensionFilePathSampleLimit, status.UnknownExtensionFilePathLimit); + Assert.Equal((long)paths.Length, status.UnknownExtensionFileCount); + } + finally + { + SqliteConnection.ClearAllPools(); + if (File.Exists(dbPath)) + File.Delete(dbPath); + } + } } From dc53778a5c014dc25634e537e88fded3aa1a91b8 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 5 Jun 2026 15:01:44 +0900 Subject: [PATCH 3/3] Document bounded unknown-extension samples for #3045 --- AGENT_GUIDE.md | 2 +- DEVELOPER_GUIDE.md | 2 +- README.md | 2 +- changelog.d/unreleased/3045.security.md | 3 +++ 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/AGENT_GUIDE.md b/AGENT_GUIDE.md index f63e472d9b..5437a37838 100644 --- a/AGENT_GUIDE.md +++ b/AGENT_GUIDE.md @@ -137,7 +137,7 @@ CI watching must be bounded. Do not loop indefinitely. - `issues_table_available` reports physical `file_issues` table presence only. `file_issues_data_current` reports whether the table is also stamped current for the active index generation. - `index_writer_version` records the `cdidx` version that last wrote to the DB (stamped into `codeindex_meta` as `cdidx_writer_version` on every full scan, update, and MCP index). `index_newer_than_reader` flips to `true` whenever any persisted numeric contract stamp in `codeindex_meta` (or unknown `PRAGMA user_version` readiness bits) exceeds the current binary's compiled maximum, so an older CLI re-opening a DB written by a newer CLI degrades loudly with an audit trail instead of silently dropping back to text-search fallbacks. `index_newer_than_reader_reason` enumerates the specific newer-than-reader stamps. - `status` also surfaces indexed-HEAD freshness via `indexed_head_sha`, `indexed_head_branch`, `indexed_head_timestamp`, and `commits_ahead_of_indexed_head`. They are stamped by `cdidx index` on every successful run (full scan AND partial update, distinct from `indexed_head_commit` which is full-scan only) on a best-effort basis (never blocks an otherwise-successful index) and omitted on non-git workspaces, detached HEAD (branch only), or legacy DBs created before this contract. -- `status` also surfaces unknown-extension scan coverage via `unknown_extension_file_count`, stamped by successful full-repository index runs (`cdidx index ` and MCP `index_project`) as the number of non-indexed files with non-empty extensions that do not map to a known language. Current scans also stamp `unknown_extension_files` as a bounded path sample, `unknown_extension_files_truncated` when the count exceeds the sample, and `unknown_extension_file_path_limit` as the cap. These fields are omitted on legacy DBs or before a current full scan has stamped them. +- `status` also surfaces unknown-extension scan coverage via `unknown_extension_file_count`, stamped by successful full-repository index runs (`cdidx index ` and MCP `index_project`) as the number of non-indexed files with non-empty extensions that do not map to a known language. Current scans also stamp `unknown_extension_files` as a path sample bounded by `unknown_extension_file_path_limit` items and the string-list decoded-character budget, `unknown_extension_files_truncated` when more paths existed than were emitted for either bound, and `unknown_extension_file_path_limit` as the item cap rather than a guarantee that that many paths are returned. These fields are omitted on legacy DBs or before a current full scan has stamped them. - `status` also surfaces extractor plugin and pattern-config runtime diagnostics via `extractors`, including loaded counts, skipped file counts, and a bounded diagnostics list for incompatible or malformed plugin/pattern files. Diagnostic paths and messages are sanitized before output. - `status` also surfaces metadata-only post-extraction hook candidates and callback budgets through `hooks[]` / `hooks[].callback_budget_ms` without loading hook assemblies. Index runs still enforce `CDIDX_HOOK_CALLBACK_BUDGET_MS` (default: 5000 ms) on scratch copies, discard timed-out mutations, and disable timed-out hooks for the remainder of the current run. - `status` also surfaces `.cdidx` data-directory permissions via `data_dir_mode` on POSIX filesystems. New `.cdidx` data directories are forced to `0700`; the field is omitted on Windows, URI DBs, or when the directory mode cannot be inspected. diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 8de4fbd025..b00e05594b 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -3150,7 +3150,7 @@ USER_GUIDEの[終了コード](USER_GUIDE.md#終了コード)セクションを - **AI向けの軽量検索スニペット** — `search --json` と MCP の `search` は、チャンク全文ではなく snippet range、match line、highlight、context count、`truncated_line_count`、`truncation_context` を持つ一致中心スニペットを返す。`truncation_context.char_counts` と `truncation_context.total_chars` はクランプされた各スニペット行の省略文字数を公開し、truncated な highlight も `truncated_char_counts` を持つ。`--snippet-lines` でペイロード量と文脈量のバランスを取れ、`--max-line-width`(CLI)/ `maxLineWidth`(MCP)は `find` / `references` / `excerpt` / `inspect` と同じ共有 `LineWidthFormatter.ClampLine` 契約で各スニペット行を最初のマッチトークン周辺にクランプするため、minified / transpiled / 生成された 1 行ファイル内の 1 ヒットで数百 KB を返さなくなる。クランプされた行はスニペットに `...(+N)...` マーカーが入り、`highlights[].truncated` と `highlights[].original_line_length` で AI クライアントがクランプを検出できる。 - **初動向けの repo map** — `map` は、インデックス済みデータから言語、モジュール、主要ファイル、ホットスポット、推定エントリポイントを集約し、AIクライアントが精密検索前に見るべき場所を決めやすくする。シンボル抽出が `Main` 系シンボルを出さない場合でも、既知のトップレベル実行ファイルへフォールバックして入口候補を補う。 - **信用判断のための鮮度メタデータ** — `status` はワークスペース全体の鮮度と git 状態を返す。`map` は `indexed_at` / `latest_modified` を絞り込み結果の鮮度として維持しつつ、`workspace_indexed_at` / `workspace_latest_modified` でワークスペース全体の鮮度も返す。`inspect` も同じワークスペース鮮度と git フィールドを返すため、シンボル中心の AI フローで `status` を別途呼ばずに済む。さらに `status` は `sql_graph_contract_ready` / `sql_graph_contract_degraded_reason`、`hotspot_family_ready` / `hotspot_family_degraded_reason` に加えて、forward-compatibility 監査 (`index_writer_version`、`index_newer_than_reader`、`index_newer_than_reader_reason`、詳細は「リーダー側の forward-compatibility 監査」を参照)、および fold-only remediation 用の `fold_ready_reason`、`degraded_reason`、`recommended_action`、`alternative_action` も返すため、AI クライアントは SQL graph/dependency/impact、duplicate-name hotspot family、Unicode `--exact` のどれが authoritative か、また DB が現在の binary より新しい `cdidx` で書かれていないかを最初に判断できる。現行の全体 scan 後は `unknown_extension_file_count` も返すため、未知拡張子で index 対象外になった件数を `status` から確認できる。これらの fold-only remediation field は、明示的な read-only `file:///...?...` DB URI から導出された場合でも、失敗する read-only URI をそのままコマンドへ埋め込まず、writable な filesystem path に正規化して返す。さらに `impact` / MCP `impact_analysis` に加えて、`inspect` / MCP `analyze_symbol`、`references` / `callers` / `callees`、`deps` / `unused` / `hotspots` 系も、SQL ベースの graph/dependency read が実際に結果へ関与したときだけ `sql_graph_contract_ready` / `sql_graph_contract_degraded_reason` を反映するため、stale な SQL 行が authoritative なヒットや 0 件応答に見えてしまうのを防ぎつつ、mixed-language index 内の純粋な非SQL結果を誤って degraded 扱いしない。`files` はファイルごとの checksum・modified・indexed timestamp を返す。古いDBに対する file 列の移行は可能なら自動で行い、その場移行できない場合でも読み取り経路がクラッシュしないようにする。CLI と MCP の 0 件 JSON レスポンスは `indexed_file_count`、`indexed_at`、`freshness_available` を含む。`freshness_available=true` で `indexed_at:null` なら空インデックス、`freshness_available=false` なら legacy/read-only DB で鮮度 timestamp を取得できず、理由は `freshness_degraded_reason` に入る。**HEAD 起点の stale 検知**: `cdidx index` の full scan が成功するたびに、現時点の `git HEAD` を `codeindex_meta` に stamp し、後続実行で workspace HEAD と比較できるようにする。`--rebuild` 指定なしに両者が異なる場合、CLI は `cdidx index --rebuild` を勧める `head_changed` 警告を表示し、`index --json` に `head_changed` / `prior_indexed_head_commit` / `current_head_commit` / `head_change_notice` を出力する。`status --check` も同じ比較を `workspace_check.head_changed` として公開し、差分時には `indexed_head_commit` / `workspace_head_commit` も併記するため、鮮度 gate ですでに `status --check` を通している AI クライアントは `git switch ` 後の既定の incremental scan を別クエリなしで拒否できる。`--commits` / `--files` の部分更新は意図的に記録 HEAD を維持し、次の full scan が worktree を再インデックスするまで stale 通知が継続する。非 Git workspace と HEAD を記録していない legacy DB は比較自体をスキップし、false-positive な警告を出さない。 -`unknown_extension_files` は `unknown_extension_file_path_limit` 件までの未知拡張子 path sample で、`unknown_extension_files_truncated` は `unknown_extension_file_count` が sample 上限を超えたことを示します。 +`unknown_extension_files` は `unknown_extension_file_path_limit` 件と decoded-character budget の両方で上限付けされた未知拡張子 path sample で、`unknown_extension_files_truncated` は件数上限または decoded-character budget により未出力の path が残ったことを示します。`unknown_extension_file_path_limit` は item 上限であり、常にその件数まで返す保証ではありません。 `extractors` は extractor plugin と pattern config の runtime health で、読み込み済み plugin assembly / pattern 件数、symbol/reference extractor 件数、skip されたファイル数、上限付き diagnostics list を返します。 diff --git a/README.md b/README.md index ef392c6d34..99c98d3c59 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ The documented `status --json` trust contract covers these fields: When any readiness field is degraded, `degraded_root_cause` identifies the primary stable code and `readiness_degradations[]` lists every degraded field with `root_cause`, human `degraded_reason`, `recommended_action`, and `alternative_action`. `issues_table_available` reports physical table presence; use `file_issues_data_current` to decide whether `file_issues` rows are current for the index generation. -After a current full-repository scan, `unknown_extension_file_count` reports how many skipped files had unmapped non-empty extensions, while `unknown_extension_files` lists up to `unknown_extension_file_path_limit` paths and `unknown_extension_files_truncated` marks when more paths exist. +After a current full-repository scan, `unknown_extension_file_count` reports how many skipped files had unmapped non-empty extensions, while `unknown_extension_files` lists a path sample capped by `unknown_extension_file_path_limit` items and the decoded-character budget. `unknown_extension_files_truncated` marks when more paths existed than were emitted for either cap; `unknown_extension_file_path_limit` is the item cap, not a guarantee that the sample always reaches that count. `extractors` reports runtime extractor plugin and pattern-config diagnostics, including loaded counts, skipped file counts, and a bounded diagnostics list for load failures. Diagnostic paths and messages are sanitized before they are surfaced. diff --git a/changelog.d/unreleased/3045.security.md b/changelog.d/unreleased/3045.security.md index 9010715be4..5e5fcdb8ac 100644 --- a/changelog.d/unreleased/3045.security.md +++ b/changelog.d/unreleased/3045.security.md @@ -6,6 +6,9 @@ affected: - src/CodeIndex/Database/JsonStringListCodec.cs - src/CodeIndex/Database/DbWriter.cs - tests/CodeIndex.Tests/JsonStringListCodecTests.cs + - README.md + - DEVELOPER_GUIDE.md + - AGENT_GUIDE.md --- ## English