[AI-1731] A branch can no longer refuse every borrowed review by exceeding the review-context cap - #448
Conversation
…launch The review-context extractor admitted reserved-path blobs under a hard 256 KiB aggregate cap and threw borrowed_snapshot_review_context_capacity_exceeded past it. That throw was not scoped to the review context: it failed the whole borrowed snapshot build, so one tracked vendor MCP config padded past 256 KiB made every borrowed review of the repository impossible, and a config that crossed the cap between rounds terminated a live reviewer through the fail-closed refresh path. The content is branch-authored, so this handed a hostile branch a launch-refusal primitive over exactly the reviews that would have inspected it. Containment and review-context capacity are separate properties. The config never enters the executable tree regardless of size; the manifest is now bounded and honest instead of fail-closed. A blob that does not fit the remaining capacity is declared in a new omittedForCapacity list (path, index mode, blob object id, byte count, sha256) rather than shipped, and admission continues, so an omitted blob consumes no capacity needed by later configs. The hash is computed streaming from cat-file, never buffered, so an arbitrarily large branch-authored blob cannot cost the daemon its size in memory. Silently dropping the entry would recreate the false-clean review failure this surface exists to prevent, so the MCP server's instructions and tool description tell the reviewer to report omitted configs as unverifiable, and an empty entries array is affirmative only when omittedForCapacity is empty too. Read-back validation requires each manifest path to appear at most once across the entries and omission lists, membership in the classifier-matched set for both, and shape checks (regular index mode, positive size, valid object id, lowercase sha256 hex) on every declaration. Omission declarations cannot re-create the refusal one level up: their count and path bytes are bounded by the exclusion plan's own caps, well inside the serialized-manifest ceiling's headroom. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR Summary by QodoDeclare over-capacity review-context configs instead of failing borrowed reviews
AI Description
High-Level Assessment
Files changed (4)
|
Code Review by Qodo
1.
|
…drains on every exit Round-1 review findings: a manifest that lost a record between write and read-back validated as complete (and an empty one reads as an affirmative all-clear), so validation now requires the represented path set to EQUAL the matched set, not merely embed in it. HashBlobSha256Async adopts the bounded capture helpers' finally-based TerminateAndDrainAsync discipline so a timeout, cancellation, or mid-read IOException cannot leave the git child unreaped or the stderr pump unobserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Linear: AI-1731. Pre-existing in the merged #443 review-context server, not a regression from #445 — filed separately for exactly that reason.
The defect
ExtractReviewContextEntriesAsyncadmitted reserved-path blobs into the review-context manifest under a hard 256 KiB aggregate cap and threwborrowed_snapshot_review_context_capacity_exceededpast it. The throw was not scoped to the review context: it propagated out ofCreateReviewContextGenerationAsync→BuildIndependentSnapshotOnceAsync→BuildIndependentSnapshotAsync→CreateBorrowedSnapshotAsync, and the whole borrowed snapshot build failed (the retry loop catches onlySourceChangedException).The content is branch-authored and the cap is small. One tracked vendor MCP config padded past 256 KiB made every borrowed review of that repository impossible — the adversary this surface defends against (someone adding a hostile vendor config to a branch) could contain the payload and deny the review that would have found it. Worse mid-flow:
TryRefreshBorrowedSnapshotAsynctreats refresh failure as fatal by design, so a config crossing the cap between rounds terminated a reviewer already in progress.The fix
Containment and review-context capacity are separate properties, and only containment needs to hold for a launch to be safe. The config never enters the executable tree regardless of size (unchanged); the manifest is now bounded and honest rather than fail-closed:
omittedForCapacitylist carries path, index mode, blob object id, byte count, and sha256. Admission continues — an omitted blob consumes no capacity needed by later configs.cat-file(HashBlobSha256Async), never buffered — an arbitrarily large branch-authored blob cannot cost the daemon its own size in memory the way reusingRunGitCaptureByteswould.entriesarray is affirmative only whenomittedForCapacityis empty too. Silently dropping the entry would recreate the false-cleanfailure ([AI-1680] Quarantine branch-authored MCP config so a reviewer can still read it #437 class) this surface exists to prevent.MaxCwdDepth,MaxVendorPathAggregateBytes), well insideMaxReviewContextManifestBytesheadroom — noted in its doc comment.Mid-flow refresh needs no orchestrator change: with capacity no longer throwing,
SyncBorrowedSnapshotFromSourceAsyncpublishes a generation that declares the omission, and the fail-closed catch stays for genuine failures.Tests
TDD (each watched failing on the old behaviour first):
Aggregate_capacity_accepts_exact_limit_and_declares_one_extra_byte_as_omitted— updated, not deleted: exact 256 KiB still fully admitted with an affirmative empty omission list; one extra byte now launches, keeps.mcp.jsonout of the executable tree, and declares path/mode/oid/size/sha256 with nobase64/textshipped.Omitted_oversized_config_does_not_consume_capacity_needed_by_later_configs— the oversized blob sorts first and a later small config is still admitted in full.Refresh_after_config_grows_past_capacity_succeeds_and_declares_omission— the between-rounds path that used to kill a live reviewer.Manifest_validation_rejects_malformed_or_out_of_set_omissions— out-of-set path, zero size, symlink mode, all-zero oid, uppercase/truncated sha, and a path appearing in both lists.Verification:
BorrowedReviewContextTests19/19; neighbours (BorrowedSnapshotExclusionScopeTests31,WorktreeManagerTests20+1 platform skip,WorkspaceMcpNeutralizationTests25,McpReviewContextServerTests6, integrationMcpReviewContextServerIntegrationTests1) all green;KCAP_WORKSPACE_MCP_CERT=1live certification passed 2/2 with realkiro-cli(positive control reproduced the exploit in a raw worktree; production path stayed contained) — nothing about relaxing the cap puts the config back in the executable tree. NativeAOT publish: daemon clean; CLI shows 4 pre-existing IL3050/IL2026 in untouchedMcpWorkItemsServer.cs(flagged separately). Full suites delegated to CI.🤖 Generated with Claude Code