Harden vendored-mode trust in patch-service artifacts - #249
Merged
Mikola Lysenko (mikolalysenko) merged 7 commits intoSep 24, 2026
Merged
Conversation
…get artifact is rebuilt When the hot path found the committed artifact missing or stale, it rebuilt it but returned no entry, so the ledger kept the previous fingerprint: the gem file inventory, the maven/nuget sha256, and the nuget lock pin. A rebuild from the other source (service vs local) then produced bytes the ledger did not describe. VEX and verify reported tamper, repair of a service-vendored maven/nuget entry could fail, --revert left packages.lock.json pinned to the patched contentHash, and a service-sourced rebuild was labelled already_vendored. The rebuild branches now return a refreshed entry built from the new bytes or tree, with no wiring of their own. For nuget the entry carries only the re-pinned lock record, with original: None. carry_forward_wiring (same uuid) re-attaches the first run's records and fills in the true pre-vendor hash. The CLI does not record such an entry when the ledger has no previous one, because it would carry no wiring and --revert would delete the artifact while the project still points at it. repair carries the repaired entry's wiring forward before persisting, and emits vendor_inventory_refreshed when the backend's refreshed inventory differs from the recorded one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…fore accepting it service_archive_copy (the Tier-A maven .jar / nuget .nupkg path) accepted any archive that matched its SRI. The bytes were written verbatim and every patched file was reported AlreadyPatched, so a served archive without the patch was committed as patched. The hot path then saw a stale artifact on every later run and rebuilt it. The helper now takes the PatchRecord and requires every patched member to hash to its afterHash (zip_bytes_match_after_hashes, the check the hot path already uses) before returning Used. A mismatch is a service miss: auto falls back to the local build with vendor_prebuilt_layout_mismatch, and service refuses with vendor_prebuilt_required. The vendor_prebuilt_downloaded advisory is only pushed for accepted bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…to too ServiceArtifact documents IntegrityMismatch as always a hard error, but golang, composer, pypi, npm and gem (the .gem and the stub gemspec) mapped it through their miss policy. Under auto they warned and built the package locally, so a sign of tampering became a quiet fallback. Under service they refused with the generic vendor_prebuilt_required code. These arms now refuse in every mode, as cargo already did: golang, composer, pypi and gem return vendor_prebuilt_integrity_mismatch, and npm fails the package with the integrity detail. The npm test that pinned the old fallback is replaced by one that expects the refusal. The service-mode tests for these ecosystems now expect the specific code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…figured Every backend's service helper treats !service_enabled() as "build locally", and service_enabled() is false without a client. The service-only policy was enforced by common::service_offline_conflict, which checked only --offline. So a VendorServiceConfig with source: Service and client: None built the artifact locally in every backend, contradicting service's fail-closed promise. The CLI always passes a client, so only library callers could reach this. The gate every backend already calls at its entry point now also refuses that combination (vendor_prebuilt_required), before any service consultation or write. Regression tests cover all eight backends. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The in-sync hot path rebuilt a missing/stale crate copy with copy_and_patch directly, so `--vendor-source=service` with --offline or no API client rebuilt locally and reported success, and online service mode never consulted the patch service. Refuse via service_offline_conflict first and prefer cargo_service_copy, falling back to the local build only where the policy allows, as composer and gem already do. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The service SRI proves only that the download is intact. A wheel or tarball whose patched members still held the original bytes was written as-is, reported as already patched, and pinned in the lockfile. Check each patched member against its afterHash before using the artifact: auto builds locally with vendor_prebuilt_layout_mismatch, service refuses (pypi: vendor_prebuilt_required; npm fails the package). Adds read_archive_bytes_to_map for the in-memory tarball check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A wiring-less refreshed entry relies on carry_forward_wiring, which re-attaches wiring only from a same-uuid ledger entry. Over an entry from another patch uuid (the run that wired this uuid never saved), the guard let it through, saving an entry --revert could not unwire and sweeping the other uuid's dir. Leave the ledger as it is instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
8 tasks
Mikola Lysenko (mikolalysenko)
enabled auto-merge (squash)
September 24, 2026 13:02
Wenxin Jiang (Wenxin-Jiang)
approved these changes
Sep 24, 2026
Mikola Lysenko (mikolalysenko)
deleted the
fix/vendor-service-integrity-hardening
branch
September 24, 2026 13:14
Mikola Lysenko (mikolalysenko)
added a commit
that referenced
this pull request
Sep 24, 2026
…outage-idempotence Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
Summary
These are pre-existing gaps in how vendored mode trusts and records artifacts from the patch service (
patch.socket.dev). They were found while root-causing #248's CI flake, and each was reproduced with a failing test before it was fixed. The companion PR for outage idempotence (vendored re-runs that churn locks after a 503) is separate.artifact.sha256, and the nuget lock record. As a result:vex/verify reported tamper;repaircould fail;vendor --revertleftpackages.lock.jsonpinned to the patched contentHash;already_vendored.autofalls back and warnsvendor_prebuilt_layout_mismatch;servicerefuses.auto. This affected golang, composer, pypi, npm and gem (the.gemand the stub gemspec). It broke the documented rule that tampered bytes are always a hard failure. They now fail closed withvendor_prebuilt_integrity_mismatch, as cargo already did.--vendor-source servicefailed open. With no API client it quietly built locally. It now refuses withvendor_prebuilt_required. The cargo wired-copy rebuild also follows--vendor-sourcenow: offline or no client refuses, and online uses the prebuilt crate.CLI_CONTRACT.md and CHANGELOG.md (
[Unreleased], Fixed) are updated.Test plan
--revertrestores the lock (nuget), and the artifact isHealthyafterwards;service, falls back underauto;IntegrityMismatchunderautohard-fails, per backend;servicewith no client, andservicewith--offline, both refuse;cargo test -p socket-patch-cli -p socket-patch-core --no-fail-fast: 7760 passed, 0 failed.cargo clippy -p socket-patch-cli -p socket-patch-core --all-targets -- -D warnings: clean.Not in this PR
npm_common.rsalso changes in the outage-idempotence PR. The hunk here is small and separate, so expect at most a trivial merge.🤖 Generated with Claude Code
Note
High Risk
Changes fail-closed behavior for tampered or unpatched service artifacts and mutates vendor ledger persistence on rebuilds across many ecosystems, affecting verify, repair, and revert flows.
Overview
Hardens vendored-mode trust in patch-service artifacts and fixes ledger drift when missing committed copies are rebuilt.
Artifact rebuilds now refresh ledger fingerprints (gem inventory, maven/nuget
sha256, nuget lock pin) while preserving wiring viacarry_forward_wiring; the CLI only persists a rebuilt entry when a same-uuid predecessor exists, so revert cannot be left with an orphan artifact.Prebuilt downloads must carry patched files: members are checked against
afterHashbefore use (vendor_prebuilt_layout_mismatchunderauto, refuse underservice). Integrity mismatches no longer fall back to local builds underautofor npm, pypi, composer, gem, and golang (vendor_prebuilt_integrity_mismatch).--vendor-source=servicewithout an API client now refuses instead of building locally; cargo wired-copy rebuilds follow the same service/offline rules as a fresh vendor.CHANGELOG and
CLI_CONTRACT.mddocument the new refusal codes and fallback table; repair/vendor paths and broad regression tests cover revert, uuid-mismatch, and service fixtures with real archives.Reviewed by Cursor Bugbot for commit 032ab2b. Configure here.