GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Restore withdrawn hosted pins upstream during scan by mikolalysenko · Pull Request #1592 · SocketDev/socket-patch

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 15, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Collaborator

Problem

When a hosted patch is unpublished, depscan answers 410 for its tarball, so every fresh npm ci (or other install) of a lockfile pinned to it fails. scan --mode hosted never repaired this. A withdrawn patch drops out of the search results, nothing selects it, and its lockfile pin stayed in place until someone ran socket-patch rollback by hand.

Change

Before the hosted rewrite, scan --mode hosted checks every patch uuid the lockfiles pin, for a package the run covered, against the reference endpoint (POST …/patches/package):

  • withdrawn / not_found: the pin is restored to its upstream registry entry through the existing rollback restore (hosted_unwind::run_hosted_leg → restore_upstream), under the apply lock. --dry-run previews the restore without writing.
  • A replacement patch is on offer: the rewrite that follows pins the replacement (the existing UPGRADE path), so the net effect is an update.
  • Anything else is kept, including a pin merely missing from search (a failed batch, the free-only proxy fallback and a lapsed paid entitlement all look like that), and forbidden, pending_build or build_failed. depscan sets withdrawn from unpublished_at before any auth or tier check (classifyPatchPackageRow), so the signal holds on the public proxy too.

Outage safety. A failed reference lookup keeps every pin and warns with redirect_withdrawn_check_failed. A run whose every search batch fails exits before the check. Neither case unpins anything.

Output. Each restored pin is a rolledBack event (verified on a dry run) with errorCode: "patch_withdrawn" and details.mode: "hosted". A restore failure warns with redirect_withdrawn_restore_failed. Status and exit code are unchanged. All three are documented in CLI_CONTRACT.md.

Scope. Only scan --mode hosted changes. get, agent mode and vendored mode are untouched.

Tests

tests/in_process_scan_withdrawn.rs (9 tests) wires a real npm package-lock through hosted scan, then re-scans against a mock that reports the pinned patch as:

  • withdrawn → the lock is byte-identical to the pristine one
  • not_found → same
  • withdrawn with a replacement offered → the replacement is pinned
  • granted but absent from search → kept
  • forbidden → kept
  • reference endpoint returning 500 → kept
  • every search batch failing → exit 1, no reference call
  • withdrawn under --dry-run → nothing written
  • --json envelope → one rolledBack event with errorCode: "patch_withdrawn"

As a mutation check, I disabled the gone-status match and the three restore tests failed.

Also run locally: the socket-patch-cli lib tests (924), in_process_redirect (146), in_process_rollback_hosted (41), hosted_memory_engine (45) and cli_scan_silent (30), all passing; CI's clippy command (--workspace --all-features -D warnings) is clean.

Follow-ups (not in this PR)

  • The in-memory hosted engine (the napi engine the GitHub app uses) has no equivalent sweep yet.
  • Agent and vendored modes keep withdrawn patches. Their bytes are local, so installs don't break, but they should probably warn.

🤖 Generated with Claude Code


Note

Medium Risk
Changes hosted scan lockfile mutation and registry restore behavior when patches are withdrawn; failures are designed to keep existing pins, but incorrect status handling could unpin or leave broken pins.

Overview
Hosted scan now unpins withdrawn patches so lockfiles that still point at unpublished patches (410 on install) get fixed without a manual rollback.

Before the hosted lockfile rewrite, scan --mode hosted calls the patch reference API for every pinned uuid on packages the run discovered. Only explicit withdrawn or not_found statuses trigger an upstream restore via the existing hosted_unwind path; missing search results, forbidden, build-pending states, and reference API failures leave pins unchanged (with redirect_withdrawn_check_failed / redirect_withdrawn_restore_failed warnings when appropriate). If a replacement patch is offered, the normal rewrite can re-pin it afterward.

Contract/output: CLI_CONTRACT.md documents the behavior and new tags (patch_withdrawn, etc.); restored pins emit rolledBack / verified events with details.mode: "hosted". Scope is hosted project scans only—not get, agent, or vendored modes.

Tests: New in_process_scan_withdrawn.rs covers restore, repin, keep-on-granted/forbidden/outage, dry-run, and JSON envelope; scan_render expects an extra reference call from the sweep.

Reviewed by Cursor Bugbot for commit 3d987b5. Configure here.


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@mikolalysenko @claude
Restore withdrawn hosted pins upstream during scan
268e0ed
A hosted pin points the lockfile at the patch server's copy of one patch
uuid. Once that patch is unpublished the server answers 410 for it, so
every fresh install of the pinned lockfile fails. scan never noticed: a
withdrawn patch drops out of the search results, nothing selects it, and
its pin was left in place until someone ran `rollback` by hand.

`scan --mode hosted` now checks every uuid the lockfiles pin, for a
package this run covered, against the reference endpoint before the
rewrite. A pin reported `withdrawn` or `not_found` is restored to its
upstream registry entry with the same restore `rollback` runs, under the
apply lock (`--dry-run` previews it). When a replacement patch is on
offer, the rewrite that follows pins it, so the net effect is an update.

Only those two explicit statuses unpin. A pin merely missing from search
(a failed batch, the free-only proxy fallback, a lapsed entitlement) or
reported forbidden / pending_build / build_failed is kept. A failed
reference lookup keeps every pin with a `redirect_withdrawn_check_failed`
warning, and a run whose every search batch failed exits before the check,
so an outage on the patch API never unpins anything.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko
Mikola Lysenko (mikolalysenko) enabled auto-merge October 15, 2026 20:57
@mikolalysenko Mikola Lysenko (mikolalysenko) mentioned this pull request Oct 15, 2026
Swarm health manager log #1382
Open
cursor[bot]
cursor Bot reviewed Oct 15, 2026
View reviewed changes

@cursor cursor Bot left a comment •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Restore runs before hosted safety refusals
    • Added early safety refusal checks before the withdrawn sweep to prevent lockfile mutation on symlink or governing-root layouts that hosted mode would refuse.

Create PR

Or push these changes by commenting:

@cursor push a58cd7f933
Preview (a58cd7f933)
diff --git a/crates/socket-patch-cli/src/commands/scan/mod.rs b/crates/socket-patch-cli/src/commands/scan/mod.rs
--- a/crates/socket-patch-cli/src/commands/scan/mod.rs
+++ b/crates/socket-patch-cli/src/commands/scan/mod.rs
@@ -2576,8 +2576,85 @@
     // longer serves, before the rewrite below re-pins any replacement.
     // Runs ahead of every "nothing found" exit: a package whose only patch
     // was withdrawn has no search results at all.
+    //
+    // Safety refusals run BEFORE the restore: a symlink or governing-root
+    // layout that hosted mode refuses must not be mutated.
     let withdrawn_sweep = if hosted && !args.common.is_global() && policy.writes_allowed() {
         let pins = withdrawn::pins_in_scope(&hosted_pin_list, &all_purls);
+
+        // Check layout refusals for the ecosystems involved in the sweep,
+        // before any restore (the sweep's `restore_upstream` would mutate
+        // lockfiles, including an ancestor lock or symlink the refusals
+        // prevent).
+        if !pins.is_empty() {
+            use socket_patch_core::vendor::lock_inventory::ProjectView;
+            let view = ProjectView::Disk(&args.common.cwd);
+
+            // Extract ecosystems from pins.
+            let mut has_npm = false;
+            let mut has_cargo = false;
+            let mut has_pypi = false;
+            for pin in &pins {
+                match Ecosystem::from_purl(&pin.purl) {
+                    Some(Ecosystem::Npm) => has_npm = true,
+                    Some(Ecosystem::Cargo) => has_cargo = true,
+                    Some(Ecosystem::Pypi) => has_pypi = true,
+                    _ => {}
+                }
+            }
+
+            // Check bun.lockb symlink refusal (npm ecosystem).
+            if has_npm {
+                use socket_patch_core::hosted::engine;
+                use socket_patch_core::vendor::lock_inventory::bun_text_lock_drives;
+                let bun_text_drives = bun_text_lock_drives(&view);
+                if !bun_text_drives && view.is_symlink("bun.lockb") {
+                    let refusal = engine::bun_lockb_symlink_refusal();
+                    if args.common.json {
+                        let mut env = scan_envelope(&args.common);
+                        env.mark_error(EnvelopeError::new(&refusal.code, &refusal.message));
+                        emit_scan(&env);
+                    } else {
+                        eprintln!("Error: {}", ui::sentence_case(&refusal.message));
+                    }
+                    return 1;
+                }
+            }
+
+            // Check governing-root refusals (cargo/pypi/npm ecosystems).
+            if has_cargo || has_pypi || has_npm {
+                use socket_patch_core::hosted::governing_root;
+                // Read trust_lockfile_config the way the main rewrite does.
+                let trust_env_off =
+                    socket_patch_core::utils::pnpm_workspace::trust_lockfile_env_for(
+                        &view,
+                        |var| std::env::var(var).ok(),
+                    )
+                    .await
+                    .is_some_and(|found| found.as_bool() != Some(true));
+                let trust_lockfile_config = !args.common.no_trust_lockfile_config && !trust_env_off;
+
+                if let Some(refusal) = governing_root::refusal_for_ecosystems(
+                    &view,
+                    has_cargo,
+                    has_pypi,
+                    has_npm,
+                    trust_lockfile_config,
+                )
+                .await
+                {
+                    if args.common.json {
+                        let mut env = scan_envelope(&args.common);
+                        env.mark_error(EnvelopeError::new(&refusal.code, &refusal.message));
+                        emit_scan(&env);
+                    } else {
+                        eprintln!("Error: {}", ui::sentence_case(&refusal.message));
+                    }
+                    return 1;
+                }
+            }
+        }
+
         Some(withdrawn::sweep(&args.common, &api_client, &pins).await)
     } else {
         None

diff --git a/crates/socket-patch-core/src/hosted/governing_root.rs b/crates/socket-patch-core/src/hosted/governing_root.rs
--- a/crates/socket-patch-core/src/hosted/governing_root.rs
+++ b/crates/socket-patch-core/src/hosted/governing_root.rs
@@ -92,12 +92,43 @@
         ProjectView::Snapshot(snap) => snap.root(),
         ProjectView::Memory(_) => return None,
     };
-    if candidates.iter().any(|c| c.dep.ecosystem == "cargo") {
+    let has_cargo = candidates.iter().any(|c| c.dep.ecosystem == "cargo");
+    let has_pypi = candidates.iter().any(|c| c.dep.ecosystem == "pypi");
+    let has_npm = candidates.iter().any(|c| c.dep.ecosystem == "npm");
+    refusal_for_ecosystems_impl(root, has_cargo, has_pypi, has_npm, trust_lockfile_config).await
+}
+
+/// Like [`refusal`], but takes ecosystem flags instead of candidates. Used by
+/// the withdrawn-pin sweep to check layout safety before any restore.
+pub async fn refusal_for_ecosystems(
+    view: &ProjectView<'_>,
+    has_cargo: bool,
+    has_pypi: bool,
+    has_npm: bool,
+    trust_lockfile_config: bool,
+) -> Option<Refusal> {
+    let root: &Path = match view {
+        ProjectView::Disk(root) => root,
+        ProjectView::Snapshot(snap) => snap.root(),
+        ProjectView::Memory(_) => return None,
+    };
+    refusal_for_ecosystems_impl(root, has_cargo, has_pypi, has_npm, trust_lockfile_config).await
+}
+
+/// Shared implementation for [`refusal`] and [`refusal_for_ecosystems`].
+async fn refusal_for_ecosystems_impl(
+    root: &Path,
+    has_cargo: bool,
+    has_pypi: bool,
+    has_npm: bool,
+    trust_lockfile_config: bool,
+) -> Option<Refusal> {
+    if has_cargo {
         if let Some(refusal) = cargo_member_refusal(root).await {
             return Some(refusal);
         }
     }
-    if candidates.iter().any(|c| c.dep.ecosystem == "pypi") {
+    if has_pypi {
         if let Some(workspace) = crate::utils::uv_workspace::governing_uv_workspace(root).await {
             return Some(Refusal {
                 code: WORKSPACE_LOCKFILE_ELSEWHERE.to_string(),
@@ -105,7 +136,7 @@
             });
         }
     }
-    if candidates.iter().any(|c| c.dep.ecosystem == "npm") {
+    if has_npm {
         let workspace = if has_own_npm_family_lock(root) {
             member_stray_lock_refusal(root).await
         } else {

You can send follow-ups to the cloud agent here.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread crates/socket-patch-cli/src/commands/scan/mod.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread crates/socket-patch-cli/src/commands/scan/withdrawn.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

@mikolalysenko
Mikola Lysenko (mikolalysenko) disabled auto-merge October 15, 2026 22:09
@claude
Expect the withdrawn sweep's reference call in the hosted render test
3d987b5
scan --mode hosted now checks every pinned uuid against the reference
endpoint before the rewrite, so the hosted render test, which seeds a
pin to OLD_UUID, sees two reference posts: the sweep's (OLD_UUID,
forbidden, kept) and the engine's (UUID). Assert both by body instead
of a bare count of one, which failed the coverage job.

Co-Authored-By: Claude <[email protected]>
@mikolalysenko

Mikola Lysenko (mikolalysenko) commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 3d987b5: fixes the red coverage / ci-ok checks. scan_render::scan_hosted_human_prints_table_updates_and_redirects seeds a pin to OLD_UUID, so the new withdrawn sweep makes its own reference POST before the engine's, and the test's count of 1 was out of date. It now expects both POSTs and checks each one's uuid. cargo test -p socket-patch-cli --test scan passes locally (241/241).

I disabled auto-merge before this push (burn-down rule). The final reviewer re-arms it after re-reviewing the new head.

bugbot run


Generated by Claude Code

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mikolalysenko

Mikola Lysenko (mikolalysenko) commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

cursor[bot] reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

cursor[bot]
cursor Bot reviewed Oct 15, 2026
View reviewed changes

@cursor cursor Bot left a comment

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 3d987b5. Configure here.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 15, 2026
@mikolalysenko

Mikola Lysenko (mikolalysenko) commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

Ready for review — head 3d987b5.

  • CI: 35/35 checks green (19 skipped), ci-ok green. No main-wide failures.
  • Fixed this run: coverage failed in scan_render::scan_hosted_human_prints_table_updates_and_redirects, which expected one reference POST. The new withdrawn sweep adds one for the pinned uuid, so the test now expects both and checks each body. The fix is test-only.
  • Bugbot: no new issues on 3d987b5. The two earlier findings (restore before the hosted safety refusals; a flush failure reported as a restore) were already resolved on their threads.
  • Auto-merge was disabled before the push. The final reviewer re-arms it.
  • Reviewers should look at commands/scan/withdrawn.rs. It is the only path that unpins hosted lockfile entries on a reference status of withdrawn or not_found.

Generated by Claude Code

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mikolalysenko
Merge branch 'main' into fix/scan-unpin-withdrawn-patches
d2cdfbf
@mikolalysenko
Mikola Lysenko (mikolalysenko) enabled auto-merge October 19, 2026 13:23
Wenxin-Jiang
Wenxin Jiang (Wenxin-Jiang) approved these changes Oct 19, 2026
View reviewed changes
@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit 14eee88 Oct 19, 2026
37 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the fix/scan-unpin-withdrawn-patches branch October 19, 2026 13:35
This was referenced Oct 20, 2026
Fix nightly Pipenv pylock legs broken by uv 0.13.2 #1639
Closed
CI performance dashboard #1182
Open
Speed up bun and yarn-berry rescans (#1606, #1607) #1693
Merged
Perf regression: bun-isolated/rescan wall +21% (cc938aa6..17793173; #1279 candidate) #1606
Closed
Perf regression: yarn-berry/rescan wall +21% (a43cf327..17793173; #1321/#1323/#1397 candidates) #1607
Open
Fix duplicate references request on hosted rescans (#1606, #1607) #1695
Merged
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@cursor cursor[bot] cursor[bot] left review comments
@Wenxin-Jiang Wenxin Jiang (Wenxin-Jiang) Wenxin-Jiang approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

3 participants

@mikolalysenko @Wenxin-Jiang @claude
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.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.