Repository navigation
Keep the hosted pin when a vendored takeover is refused (#853, #944) - #963
Open
Mikola Lysenko (mikolalysenko) wants to merge 7 commits into
Open
Mikola Lysenko (mikolalysenko) wants to merge 7 commits into
Mikola Lysenko (mikolalysenko) wants to merge 7 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
This was referenced Oct 6, 2026
Switching a hosted-patched package to vendored mode (scan or get --mode vendored, or vendor) first restores the package's upstream registry entry, then runs the vendored backend. When that backend refused the package (a pnpm catalog dependency, a CRLF pnpm lock, a workspace exact-pin override, a uv inline sources table, ...), the restore had already been made, so the package ended up neither hosted nor vendored and the next install pulled the unpatched release. The takeover now takes a savepoint in the run's group commit before the restore. If the backend does not vendor the package, the restore is rolled back in memory before anything reaches disk: the hosted pin, lock and side config stay byte-for-byte, the purl fails with the backend's own code, and no "restored its upstream entry" advisory is printed. This covers every ecosystem's post-restore refusal, not just the pnpm and uv triggers that were reported. A restore that writes a file the group commit does not capture keeps the old behaviour. vendor --dry-run also previews the backend's refusal over the restored project, staged in a throwaway overlay that never reaches disk. Refs #853, #944 Assisted-by: Claude Code:claude-opus-5-5
Adds hermetic regression tests for #944. In both cases a hosted six pin is switched to vendored mode and refused after the takeover's upstream restore: - a uv project whose pyproject.toml names [tool.uv] sources as an inline table - a `uv pip compile --universal` requirements.txt that splits six across two exact pins by marker (#928's shape) Either way, six must stay patched: vendored, or still hosted with pyproject.toml, uv.lock and requirements.txt byte-for-byte as hosted mode wrote them. Both tests fail on main, where the restore was already made when the refusal came, and pass with the rollback. Refs #944 Assisted-by: Claude Code:claude-opus-5-5
Adds a regression test: `vendor --dry-run` over a hosted pnpm catalog pin now previews the backend's refusal (vendor_lock_entry_unsupported) instead of promising the takeover, and the wet `vendor` keeps the hosted pin byte-for-byte. CLI_CONTRACT.md's "Takeover reconciliation" now states that a takeover the backend refuses is rolled back with the hosted wiring untouched, and that the scan / get --dry-run preview does not model the takeover yet. Refs #853, #944 Assisted-by: Claude Code:claude-opus-5-5
Mikola Lysenko (mikolalysenko)
marked this pull request as ready for review
October 6, 2026 21:23
Collaborator
Author
|
BugBot review Generated by Claude Code |
docs/testing/vlt-coverage.json still listed the renamed vlt takeover test under the reinstall-advisory variant. That test now asserts the advisory is not emitted (the hosted pin stays), so drop it from that list. lint-ecosystems failed on the stale name. The two new takeover test files spawned the binary with a bare Command::new, which the spawn_env_hygiene ratchet rejects. They now use hermetic::binary_command(). Refs #853, #944 Assisted-by: Claude Code:claude-opus-5-5
Collaborator
Author
|
BugBot review Generated by Claude Code |
Bugbot found two paths that stopped vendoring a purl after the upstream restore was already staged, but skipped the rollback: a restore whose flush failed partway, and an artifact redownload that failed. The group commit would still write the staged restore, so the package was left un-hosted and unvendored, which is the #853 / #944 bug by another route. Both paths now roll back to the savepoint, the same way a backend refusal does. Refs #853, #944 Assisted-by: Claude Code:claude-opus-5-5
Collaborator
Author
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 284d687. Configure here.
Collaborator
Author
|
[burn-down agent] Ready for review at
Generated by Claude Code |
Tanmay Singla (Tanmay182003)
approved these changes
Oct 7, 2026
This branch has not been deployed
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.
LLM Description written by Claude Code:claude-opus-5-5
Refs #853
Refs #944
Both issues stay open for the remaining slice:
scan/get --dry-runparity (see "Not in this PR").What users saw
Switching a hosted-patched package to vendored mode (
scan --mode vendored,get --mode vendored, orvendor) restored the package's upstream registry entry first, and only then ran the vendored backend. If that backend refused the package, the package was left patched in neither mode, and the next install pulled the unpatched release. The run did exit 1, but the hosted pin was already gone. Refusals that trigger this include:catalog:dependency, a CRLFpnpm-lock.yaml, or a workspace exact-pin overridescan/get --mode vendored) restores the package to PyPI before the uv vendored refusals run, so an inline[tool.uv] sources = {…}table (or a #928 marker split) leaves it unpatched in both modes, while --dry-run previews would_vendor #944 (uv): an inline[tool.uv] sources = {…}table, or a marker-split requirements.txtRoot cause
The takeover in
crates/socket-patch-cli/src/commands/vendor.rscommittedrestore_upstreambefore the target backend's refusals ran. Earlier fixes added per-ecosystem preflights in front of the restore (Bun, vlt, yarn berry, npm v1, gem), but pnpm and PyPI had none. Patching one more ecosystem at a time would leave every refusal nobody has found yet with the same data loss.Fix
The fix sits at the shared boundary, so it covers every ecosystem and every refusal.
The vendored run already holds every lockfile, manifest and config write in memory until one group commit at the end (
utils::group_commit). This PR adds two things to that:GroupCommit::savepoint()/rollback_to(), which roll back the captured project files and leave the ledgers alone.group_commit::captures(rel), which says whether a write to a path is captured.The takeover takes a savepoint before the restore. If the backend then does not vendor the purl (any refusal, an unsupported result, or a failed apply that recorded nothing), the takeover rolls back to the savepoint before anything reaches disk:
vendor_takeover_reverted_redirect, the restore's advisories and the vlt store heal are only emitted once the restore standsA restore that writes an uncaptured file (
.socket/gradle/hosted-index.tsv) keeps today's behaviour.vendor --dry-runnow previews the samefailed <code>. It runs the backend's dry run over the restored text, staged in a throwaway overlay that is never committed. A restore with a binary lock or an uncaptured file is not previewed.CLI_CONTRACT.md"Takeover reconciliation" documents all of this.Not in this PR (follow-up slice)
The
scan/get --mode vendored --dry-runpreview (preview_vendor_json) is an offline, ledger-only classification. It doesn't model the takeover at all: it doesn't reportvendor_would_revert_redirecteither. So it still lists these purls aswould_vendor. Fixing that means running the restore in the preview and evaluating the npm-familylock_text_refusalsover the restored text, plus a new uv inline-sources gate. That's a separate change with its own contract decision, so the contract now says it explicitly.Other changes
8a796fbfixes two CI guards this branch tripped. It drops the renamed vlt test fromdocs/testing/vlt-coverage.json's reinstall-advisory variant (the test now asserts that advisory is not emitted), and it moves the new test files ontohermetic::binary_command()so thespawn_env_hygieneratchet accepts them.3015804ports Route Gradle digests through utils::digest #878 ("Route Gradle digests through utils::digest"). Theproduction_digests_go_through_the_helpersguard test is red onmain, and this commit becomes a no-op once Route Gradle digests through utils::digest #878 lands.vlt_failed_vendor_after_the_takeover_revert_still_heals_the_storepinned the old behaviour (un-host, then heal the store). It is renamed to…_keeps_the_hosted_pinand now asserts the hosted lock,package.jsonand store copy stay put.Test evidence (red on
main→ green here)mainscan_vendored_over_hosted_pnpm_catalog_dep_keeps_the_hosted_pinget_vendored_over_hosted_pnpm_catalog_dep_keeps_the_hosted_pinscan_vendored_over_hosted_pnpm_crlf_lock_keeps_the_hosted_pinscan_vendored_over_hosted_pnpm_workspace_override_keeps_the_hosted_pinvendor --dry-runparityvendor_dry_run_over_hosted_pnpm_catalog_dep_previews_the_refusalscan_vendored_over_hosted_pnpm_plain_dep_still_takes_overscan_vendored_over_hosted_uv_inline_sources_keeps_the_hosted_pinscan_vendored_over_hosted_marker_split_requirements_never_unpatchesrollback_to_savepoint_forgets_later_project_writes_only,captures_only_root_relative_commit_pointsThe new files are
tests/in_process_vendor_pnpm_takeover.rsandtests/in_process_vendor_pypi_takeover.rs. In the hermetic uv fixture, the run can stop atvendor_prebuilt_requiredbefore the uv backend's own inline-table refusal. That is still a post-restore refusal, so it exercises the rollback, and the test asserts the invariant (hosted pin byte-for-byte) rather than the specific code.Other suites run locally, all passing:
in_process_vendor(116),in_process_vendor_bun_takeover(30),in_process_vendor_npm_v1_takeover,vendor_eject,spawn_env_hygiene,contract_gradle_codes,help_text_hygienescripts/tests(254)cargo clippy --workspace --all-features -- -D warnings, cleanCore lib is 5247 passed, 5 failed. All 5 fail identically on
main: 4 depend on permission errors and can't fail when the sandbox runs as root, and the 5th is the digest guard, fixed here by the #878 port.cargo test --workspacedidn't fit in this sandbox's disk, andcargo fmt --checkis already red onmainwith the pinned 1.93.1 toolchain (CI doesn't run it). Only the new code here was formatted. CI is the full run.🤖 Generated with Claude Code
https://claude.ai/code/session_01VFYnhVEY8Sd5MjToPfcrd3
Note
Medium Risk
Changes atomic multi-file vendor takeover behavior and group-commit rollback semantics; incorrect rollback could leave inconsistent lock state, though the fix targets a known data-loss bug with broad test coverage.
Overview
Hosted → vendored takeover no longer leaves packages unpatched when vendoring fails (#853, #944). Previously,
vendor/scan/get --mode vendoredcommitted the upstream registry restore before the vendored backend ran; a later refusal (pnpmcatalog:/ CRLF lock / overrides, uv inlinesources, etc.) dropped the hosted pin with nothing vendored in its place.The shared path in
vendor.rsnow takes aGroupCommitsavepoint beforerestore_upstream, defersvendor_takeover_reverted_redirectand related advisories until vendoring actually succeeds, androllback_tothe savepoint when the backend refuses, apply fails without a ledger entry, or redownload fails—so lockfiles and hosted wiring stay byte-for-byte. Restores that touch uncaptured paths (e.g..socket/gradle/hosted-index.tsv) are unchanged.vendor --dry-runpreviews the samefailed <code>by dry-running the backend over the restored project in a throwaway overlay (takeover_dry_refusal).group_commitaddssavepoint/rollback_to,captures(rel), and clone support for overlay state. CLI_CONTRACT documents rollback semantics and notesscan/getvendored dry-run still does not model takeover.Tests: new hermetic pnpm and PyPI takeover suites; vlt test renamed to assert the hosted pin is kept. Minor digest calls route through
utils::digest(aligned with #878).Reviewed by Cursor Bugbot for commit 284d687. Configure here.
Generated by Claude Code