fix(get,scan): retain patch-added new files; guard empty patch records - #158
Merged
Mikola Lysenko (mikolalysenko) merged 1 commit intoAug 12, 2026
Merged
Conversation
The scan/download/vendor record builder used `files_with_both_hashes`,
which kept only files carrying BOTH before+after hashes and silently
dropped every net-new file a patch ADDS (afterHash, no beforeHash). The
by-uuid apply path already tolerated new files via an empty-beforeHash
sentinel, so `get <uuid>` patched correctly while `scan`/`apply`/`vendor`
dropped added files.
Real-prod impact: the only free cargo patch (traitobject@0.1.1) is a
whole-crate export where ALL files lack beforeHash, so the manifest
recorded `files:{}` yet reported `applied:1` while writing nothing; and a
gem patch's genuinely-new `lib/rubygems_plugin.rb` CVE guard was dropped.
Fix (two parts):
1. New shared `files_for_manifest` helper keeps every file with an
afterHash using the empty-beforeHash sentinel; the record builders
(`record_from_patch_response`, `download_patch_records`,
`download_and_apply_patches`, `save_and_apply_patch`) now use it.
`files_with_both_hashes` is retained ONLY for installed-variant
matching, where new files are non-discriminating anyway.
2. Guardrail: a record whose resulting files map is EMPTY is counted as
failed (loud error / non-success exit), never reported as
applied/vendored — a defective all-files-dropped patch can no longer
be silently counted as `applied:1`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
enabled auto-merge (squash)
August 12, 2026 23:39
Wenxin Jiang (Wenxin-Jiang)
approved these changes
Aug 12, 2026
Mikola Lysenko (mikolalysenko)
deleted the
fix/get-tolerate-new-file-patch-entries
branch
August 12, 2026 23:43
Mikola Lysenko (mikolalysenko)
added a commit
that referenced
this pull request
Aug 13, 2026
…ardrail Same root cause as this PR's get_edge_cases_e2e fixes: #158's guardrail (empty applicable-files map -> "patch has no applicable files", exit 1) broke get_emits_patch_fetched_telemetry_on_uuid_lookup_success, whose mock patch-view used "files": {} while asserting a successful get. Give it a recordable new-file entry (afterHash = git-blob sha256 of the decoded blob) so the success + telemetry assertions hold; guardrail untouched. Verified with a full `cargo test --workspace --no-fail-fast` pass that these two binaries held the only 5 casualties, now all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
added a commit
that referenced
this pull request
Aug 13, 2026
…rd passes (#168) Merged PR #158 (5d7eb6f, "fix(get,scan): retain patch-added new files; guard empty patch records") added a correct guardrail: a fetched patch view whose `files_for_manifest` map is EMPTY is now an exit-1 failure ("patch has no applicable files") instead of a silent `applied:1`. But #158 only touched get.rs and left four tests in get_edge_cases_e2e.rs carrying stale `"files": {}` patch-view fixtures that still assert the `get` succeeds (exit 0). Those four now fail on main: - get_with_id_flag_selects_specific_patch - get_uuid_returns_paid_patch_with_token_succeeds - get_on_vendored_purl_warns_about_uuid_drift - get_uuid_replacing_existing_manifest_entry_reports_updated Their real concern is selection / paid-token / drift-warning / manifest-replacement logic, not "a patch with zero files" — the empty map was only a lazy stand-in. Give each view fixture one recordable net-new file (all-zero `beforeHash`, real git-blob `afterHash` via the shared `common::git_sha256` oracle, matching base64 `blobContent`), modeled on the passing `get_invariants::patch_response_json` fixture, so `files_for_manifest` is non-empty and the guard is satisfied. Every existing assertion (found==1, selected UUID, drift warning, `updated` reporting + `oldUuid`, manifest move) is unchanged. The guardrail in get.rs is untouched. get_edge_cases_e2e now 22/22; lib (350) and get_invariants / get_update_summary (15) unaffected. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
added a commit
that referenced
this pull request
Aug 13, 2026
…ile for #158's empty-record guard)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
fn files_with_both_hashes(patch)incrates/socket-patch-cli/src/commands/get.rskept only patch files carrying BOTHbeforeHash+afterHash, silently dropping every net-new file a patch ADDS (a file withafterHashbut nobeforeHash). It backed the scan/download/vendor record builders, while the by-uuid apply pathsave_and_apply_patchalready TOLERATED new files via an empty-beforeHashsentinel.So
get <uuid>patched correctly, butscan/apply/vendordropped added files:cf2e6f58forpkg:cargo/traitobject@0.1.1, is a whole-crate export where ALL 9 files lackbeforeHash. The manifest recordedfiles:{}, yetscan --mode vendored,apply, andvendorall printedapplied:1/successwhile writing NOTHING — telling the user to commit a.socket/vendor/directory that never got created.2535d43dforactivestorage@7.0.2.2ships a genuinely-newlib/rubygems_plugin.rb(a CVE-2022-21831 runtime guard) with nobeforeHash→ dropped by agent-mode and--vendor-source build, silently omitting a protection file the patch author intended.Repro (traitobject): fresh
cargo newwithtraitobject="=0.1.1", isolatedCARGO_HOME,SOCKET_NO_CONFIG=true,scan --json --yes --mode vendored→ observedapplied:1with nothing written; expected either the files recorded/vendored, or a loud failure.Fix
Two parts, both in
crates/socket-patch-cli/src/commands/get.rs:Retain new-file entries. New shared helper
files_for_manifest(get.rs:384) keeps every file with anafterHash, recording missingbeforeHashas the empty-string sentinel — the exact conventionsave_and_apply_patchalready relied on. All record builders now use it:record_from_patch_response(:405),download_patch_records(:938),download_and_apply_patches(:1226), andsave_and_apply_patch's inline loop (:1928, refactored to the shared helper).files_with_both_hashes(:353) is retained ONLY for installed-variant matching infilter_to_installed_releases(:800), where new files are non-discriminating anyway (select_installed_variantsfilters on a non-emptybeforeHash).Guardrail. If a built record's
filesmap ends up EMPTY, it is now surfaced as a failure instead of a success:download_and_apply_patchesanddownload_patch_recordspush afailedaction and increment the failed count (degradingstatus/exit viarun_outcome), andsave_and_apply_patchreports a loud error and returns exit 1. A defective all-files-dropped patch can never again be counted asapplied:1.Test
Three hermetic unit tests in get.rs's
#[cfg(test)]module:files_for_manifest_retains_new_file_without_before_hash— a patch addinglib/rubygems_plugin.rb(afterHash, no beforeHash) is retained with an empty-beforeHashsentinel; the strict helper still drops it.files_for_manifest_keeps_all_new_file_whole_crate_export— the P0 cargo case: 9 whole-crate-export files all retained (old rule producedfiles:{}).build_patch_record_from_new_files_is_not_empty—record_from_patch_responseretains new files (RED before the fix), and a no-afterHashpatch yields the empty map the guardrail rejects.Verified RED→GREEN: reverting
record_from_patch_responsetofiles_with_both_hashesmakesbuild_patch_record_from_new_files_is_not_emptyfail; with the fix all 56commands::get::testspass.cargo build -p socket-patch-cliis clean.Scope
Focused on the CLI record-builder + guardrail. Does not touch the core apply engine (already new-file-tolerant) or blob download. The other half of the gem-new-file finding (service-side export) is out of scope. Kills sweep findings: cargo-scan-apply-vendor-silent-noop, scan-vendored-false-applied-nothing-written, download-flow-drops-new-files, gem-new-file-dropped-build-weaker-than-service (CLI-drop half), and the K4 axios "works via get, breaks via scan" class.
🤖 Generated with Claude Code
Note
Medium Risk
Changes how scan/apply/vendor build manifest records and exit/status for defective patches—user-visible behavior improves but wrong patches now fail instead of silent no-ops; core apply engine unchanged.
Overview
Aligns
scan,apply, andvendorwithget <uuid>by introducing sharedfiles_for_manifest, which records every file with anafterHash—including net-new files that only have anafterHash—using an empty-stringbeforeHashsentinel. The previousfiles_with_both_hashesrule dropped those files, so whole-crate cargo exports and gem runtime-guard additions could end up withfiles:{}while JSON still reportedapplied:1and nothing was written.files_with_both_hashesis only used for installed-release variant matching infilter_to_installed_releases; all manifest record paths now callfiles_for_manifest(record_from_patch_response, download/apply, vendor download, andsave_and_apply_patch).Guardrail: if a patch yields no recordable files, flows treat it as failed (not skipped/applied)—vendor download,
download_and_apply_patches, andsave_and_apply_patcherror out or increment failure counts so empty patches cannot masquerade as successful protection.Unit tests cover new-file retention, whole-crate export (9 files), and non-empty records from
record_from_patch_response.Reviewed by Cursor Bugbot for commit 32bb21d. Configure here.