Skip to content

fix(scan): warn when a mode takeover leaves the other ledger stale - #164

Open
Mikola Lysenko (mikolalysenko) wants to merge 1 commit into
mainfrom
fix/mode-takeover-ledger-warn
Open

fix(scan): warn when a mode takeover leaves the other ledger stale#164
Mikola Lysenko (mikolalysenko) wants to merge 1 commit into
mainfrom
fix/mode-takeover-ledger-warn

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Problem

Switching a project's patch mode leaves the DISPLACED mode's ledger
stale, with no warning or reconciliation.

  • Hosted mode writes .socket/vendor/redirect-state.json.
  • Vendored mode writes .socket/vendor/state.json (+ committed tarballs
    under .socket/vendor/<eco>/).

Running scan --mode vendored on a project already redirected by hosted (and
vice-versa) rewires the lockfile to the new mode, but the OLD mode's ledger
stays on disk asserting wiring that is no longer live — and, vendored→hosted,
the orphaned vendored tarball is left behind. Both scans report
warnings: []. Anything auditing a ledger as "what is live" (including vex)
is misled.

Confirmed both directions against real prod (minimist@1.2.2 npm): after
hosted→vendored, package-lock.json resolves minimist to
file:.socket/vendor/npm/.../minimist-1.2.2.tgz while redirect-state.json
still says mode: hosted, edits[0].action: rewritten, new.resolved: https://patch.socket.dev/...; mirror for vendored→hosted. No warning either
way. (Kills sweep finding: stale-ledger-on-mode-takeover, P2; reduces the
downstream vex-prefers-stale-vendor-ledger issue.)

Fix

Detect the cross-mode overlap and warn in each flow — no silent mutation of
the other mode's ledger.

  • crates/socket-patch-cli/src/commands/scan/mod.rs:421 — new
    overlapping_ledger_purls(cwd) loads BOTH ledgers and returns the PURLs
    claimed by each (canonicalized, mirroring vendored_ledger_supplement).
    A non-empty result means one mode took the lockfile over from the other for
    those package(s) and exactly one ledger is stale per PURL. Empty when either
    ledger is missing/empty or the two describe disjoint packages (no false
    positives). Plus mode_takeover_detail(...) and the two warning codes
    REDIRECT_SUPERSEDES_VENDORED / VENDOR_SUPERSEDES_REDIRECT.
  • crates/socket-patch-cli/src/commands/scan/hosted.rs:469run_redirect
    detects a still-live vendored ledger after writing its own ledger and adds a
    redirect_supersedes_vendored warning to the JSON warnings[] (line 524)
    and stderr (line 587).
  • crates/socket-patch-cli/src/commands/scan/vendor_flow.rs:56 — new
    note_vendor_supersedes_redirect(...) (mirrors note_classic_migration_risk)
    pushes a vendor_supersedes_redirect RunWarning onto the envelope +
    stderr; called at both finalize points of run_scan_vendor_step
    (lines 139 and 178).

The warning names the displaced package(s) and the stale ledger / orphaned
artifacts to clean up.

Test

crates/socket-patch-cli/src/commands/scan/mod.rs (tests module), hermetic,
no network:

  • overlapping_ledgers_flag_the_taken_over_package — both ledgers claim
    minimist ⇒ detection returns exactly that PURL (the takeover warning fires).
  • single_ledger_present_flags_nothing — a first-time redirect (only one
    ledger, or none) displaces nothing ⇒ empty (guards against warning on the
    FIRST scan of a fresh project).
  • disjoint_ledgers_are_not_a_takeover — one package redirected, a different
    one vendored ⇒ empty (no false positive on a legitimate split).
  • takeover_detail_names_direction_package_and_remediation — each direction's
    detail names the PURL, the correct stale ledger file, and distinct codes.

cargo test -p socket-patch-cli --lib 'commands::scan::tests' → 7 passed.
cargo build -p socket-patch-cli and cargo clippy -p socket-patch-cli clean.

Scope

  • Detection + warning only. Full auto-reconciliation (marking the displaced
    ledger superseded, or deleting the now-orphaned vendored tarball) is
    deliberately deferred — the fix instruction called for warning as sufficient,
    and neither mode should silently mutate/delete the other's ledger. The
    warning text points the user at vendor --revert / removing
    redirect-state.json / deleting orphaned .socket/vendor/<eco>/.
  • Detection is disk-state-based (both ledgers on disk with overlapping PURLs),
    so it fires on the SECOND scan (the takeover) and stays silent on the first,
    matching the real prod repro in both directions.
  • Hosted --dry-run does not write redirect-state.json, so a would-be
    takeover during a pristine dry run is not flagged; the real (non-dry-run)
    takeover is. Not addressed here.
  • No cross-dependency on other in-flight PRs.

🤖 Generated with Claude Code


Note

Medium Risk
Changes audit-facing warnings and CLI JSON contracts for scan modes; behavior is advisory only (no ledger mutation), but misleading VEX/audit reads were the reported production issue.

Overview
When hosted and vendored patch modes both have ledgers claiming the same package, the lockfile only reflects the current mode — the other ledger (and vendored tarballs after hosted takeover) can stay on disk with no signal. This PR detects overlapping PURLs between redirect-state.json and state.json (canonicalized like existing vendor supplement logic) and warns without deleting or rewriting the displaced ledger.

Hosted run_redirect adds redirect_supersedes_vendored to JSON warnings[] and stderr after its ledger write. Vendored scan flow adds note_vendor_supersedes_redirect (envelope warnings[] + stderr), aligned with note_classic_migration_risk, at both finalize paths in run_scan_vendor_step. Warning text names packages, which file is stale, and manual cleanup (vendor --revert, remove redirect ledger, etc.).

Hermetic tests cover overlap detection, single/disjoint ledgers (no false positives), and direction-specific detail strings.

Reviewed by Cursor Bugbot for commit 44df961. Configure here.

Switching a project's patch mode rewired the lockfile to the new mode but
left the displaced mode's ledger on disk asserting wiring that is no longer
live (hosted: .socket/vendor/redirect-state.json; vendored:
.socket/vendor/state.json + orphaned tarballs). Both scans reported
`warnings: []`, so anything auditing a ledger as "what is live" (including
`vex`) was misled.

Detect the overlap (PURLs claimed by BOTH ledgers) and emit a takeover
warning in each flow — `redirect_supersedes_vendored` from the hosted flow,
`vendor_supersedes_redirect` from the vendored flow — surfaced in both the
JSON `warnings[]` and stderr, naming the displaced package(s) and the stale
ledger/orphaned artifacts to clean up. Neither mode silently mutates or
deletes the other's ledger; reconciliation is deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 1 potential issue.

Fix All in Cursor

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

  • ✅ Fixed: Wrong takeover warning direction
    • Fixed by checking that packages were actually confirmed/vendored in the current run before emitting takeover warnings, preventing false warnings on dry-runs and no-op scans where the lockfile still points to the other mode.

Create PR

Or push these changes by commenting:

@cursor push b56c9b00d2
Preview (b56c9b00d2)
diff --git a/crates/socket-patch-cli/src/commands/scan/hosted.rs b/crates/socket-patch-cli/src/commands/scan/hosted.rs
--- a/crates/socket-patch-cli/src/commands/scan/hosted.rs
+++ b/crates/socket-patch-cli/src/commands/scan/hosted.rs
@@ -472,13 +472,28 @@
     // stale. Detect + warn (JSON `warnings[]` and stderr) WITHOUT deleting the
     // other mode's ledger; full reconciliation is deferred (see PR Scope).
     // Read after the ledger write above so a non-dry-run reflects this run.
+    // Only warn about PURLs that THIS RUN confirmed redirected (landed in the
+    // lockfile) — a dry-run or no-op scan that redirected zero packages must
+    // not warn, even when both ledgers exist (the lockfile still points at the
+    // vendored artifacts, not the hosted server).
     let mut takeover_warnings: Vec<serde_json::Value> = Vec::new();
-    let superseded = super::overlapping_ledger_purls(&args.common.cwd).await;
-    if !superseded.is_empty() {
-        takeover_warnings.push(serde_json::json!({
-            "code": super::REDIRECT_SUPERSEDES_VENDORED,
-            "detail": super::mode_takeover_detail(&superseded, /*current_is_hosted=*/ true),
-        }));
+    if !confirmed.is_empty() {
+        use socket_patch_core::utils::purl::{normalize_purl, strip_purl_qualifiers};
+        let confirmed_purls: std::collections::HashSet<String> = confirmed
+            .iter()
+            .map(|(purl, _)| normalize_purl(strip_purl_qualifiers(purl)).into_owned())
+            .collect();
+        let all_overlapping = super::overlapping_ledger_purls(&args.common.cwd).await;
+        let superseded: Vec<String> = all_overlapping
+            .into_iter()
+            .filter(|purl| confirmed_purls.contains(purl))
+            .collect();
+        if !superseded.is_empty() {
+            takeover_warnings.push(serde_json::json!({
+                "code": super::REDIRECT_SUPERSEDES_VENDORED,
+                "detail": super::mode_takeover_detail(&superseded, /*current_is_hosted=*/ true),
+            }));
+        }
     }
 
     // Emit an OpenVEX attestation when `--vex` was requested. The redirected

diff --git a/crates/socket-patch-cli/src/commands/scan/vendor_flow.rs b/crates/socket-patch-cli/src/commands/scan/vendor_flow.rs
--- a/crates/socket-patch-cli/src/commands/scan/vendor_flow.rs
+++ b/crates/socket-patch-cli/src/commands/scan/vendor_flow.rs
@@ -61,8 +61,32 @@
 /// at the envelope level (JSON `warnings[]` and stderr), mirroring
 /// [`note_classic_migration_risk`]; the stale ledger is NOT deleted here
 /// (reconciliation is deferred — see the redirect twin in `hosted.rs`).
+/// Only warns about PURLs that THIS RUN actually vendored (have Applied or
+/// Rebuilt events) — a no-op vendor run or dry-run that vendored zero packages
+/// must not warn, even when both ledgers exist.
 async fn note_vendor_supersedes_redirect(env: &mut Envelope, cwd: &Path, common: &GlobalArgs) {
-    let superseded = super::overlapping_ledger_purls(cwd).await;
+    use crate::json_envelope::PatchAction;
+    use socket_patch_core::utils::purl::{normalize_purl, strip_purl_qualifiers};
+
+    // Extract PURLs that were actually vendored in this run (Applied or Rebuilt).
+    let vendored_purls: std::collections::HashSet<String> = env
+        .events
+        .iter()
+        .filter(|e| matches!(e.action, PatchAction::Applied | PatchAction::Rebuilt))
+        .filter_map(|e| e.purl.as_ref())
+        .map(|purl| normalize_purl(strip_purl_qualifiers(purl)).into_owned())
+        .collect();
+
+    if vendored_purls.is_empty() {
+        return;
+    }
+
+    let all_overlapping = super::overlapping_ledger_purls(cwd).await;
+    let superseded: Vec<String> = all_overlapping
+        .into_iter()
+        .filter(|purl| vendored_purls.contains(purl))
+        .collect();
+
     if superseded.is_empty() {
         return;
     }

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

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 44df961. Configure here.

"code": super::REDIRECT_SUPERSEDES_VENDORED,
"detail": super::mode_takeover_detail(&superseded, /*current_is_hosted=*/ true),
}));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Wrong takeover warning direction

High Severity

Overlap detection only proves both ledgers claim the same PURLs, but each flow still assumes the active command displaced the other and that the lockfile matches that mode. After a real takeover leaves both ledgers on disk, a later hosted dry-run or no-op scan (or a vendored run that does not rewire the overlapping packages) emits the opposite *_supersedes_* warning and remediation, which can point cleanup at the live ledger.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 44df961. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant