Skip to content

Cargo VEX: read member manifests, match liveness by origin, withhold mixed and vendored bypasses - #2012

Merged
Mikola Lysenko (mikolalysenko) merged 32 commits into
mainfrom
fix/cargo-members-liveness-origin
Nov 9, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 32 commits into
mainfrom
fix/cargo-members-liveness-origin

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Cargo VEX and ledger liveness attested wiring the build doesn't use:

  • Only the root Cargo.toml was read, so a member reverted to crates.io still attested.
  • One manifest's pin attested a crate another declaration builds unpatched (crates.io, another registry, path, git, registry-index).
  • A vendored [patch] copy attested when another source, a same-version lock entry or a shadowed [patch] entry bypassed it.
  • Ledger liveness matched the uuid as a substring of the lock URL, on any host.

Fix

  • Members: read the hosted rewriter's member manifests via ProjectView, only when Socket wiring could need them.
  • Mixed pins: one route classifies each declaration. An unpinned one marks the ref vex_cargo_unpinned_declaration, with re-scan advice from the rewriter's own planner run dry. A pin to another patch becomes a wiring_conflict pair.
  • Vendored: vex_cargo_patch_bypassed when a declaration or lock entry escapes every honored [patch] table (Shadow models cargo's precedence; URLs compare as CanonicalUrl).
  • Liveness: lock URLs are judged by origin via crate::origin: Live / Undecided / Dead. Undecided is never attested; scan retains it.

Owner decisions

  • Fail closed: a pre-v5 ledger record wired through a hosted-shaped URL on an unconfigured origin is never attested, even under --no-verify. It's reported as redirect_unwired, with a note naming --patch-server-url.
  • Folded into one PR.

Where to look

  1. Scope extension, please confirm. Fail closed also covers URLs that appear only in recorded wiring files: yarn berry __archiveUrl, Cargo.lock, pom, Gemfile and go.mod (foreign_hosted_url_in_files, origin::hosted_shape_uuid). Look-alike userinfo hosts (patch.socket.dev@evil.example) count as foreign. The yarn, cargo and maven e2e cells expect redirect_unwired.
  2. cargo.rs (route, beside_the_pin, patch_bypassed, Shadow), discover/mod.rs (redirect_record_verdict), and plan_cargo_tomls (extracted from rewrite_cargo_with unchanged).

Testing

  • Core: vex:: origin cargo hosted:: patch::redirect:: vendor:: (4962 tests, golden included) and redirect_golden.
  • CLI: --lib plus the vex, scan, redirect and cargo-migration suites.
  • clippy and fmt are clean. CI green on f1eb764 (46 checks).

Folds #2019, #2024.

Review history

Fixed:

  • Lockless pin vs a member's other-version declaration (excluded_reqs).
  • Configured-origin records judged dead without the origin (three-way verdict).
  • One lockless pin and diagnostic per crate and patch.
  • Member walk goes through the view, async, and runs beside a stale lock or a [patch] pin.
  • Foreign URL needs a patch-server path; a Socket spelling elsewhere doesn't clear it; userinfo; Go paths.
  • [patch]-redirected and registry = "crates-io" declarations aren't unpinned.
  • Socket registry beside path/git/registry-index isn't a pin, and the message says so.
  • Stale-lock remedy names the redirecting table; vendor offered only for crates.io.
  • Re-scan advice follows the rewriter's whole-dependency refusal.
  • [patch] precedence: shadowed entries, named vs URL tables, an identical config pin.
  • registry-index and git tables, canonical URLs in one helper.
  • Misleading "only from other Socket patches" text; conflict gate matches the CLI.
  • Diagnostics for an unreflected other-patch pin and for ineffective Socket registries.

Declined:

  • Ambiguous unpinned declaration withheld: documented fail closed.
  • Socket registry B beside git/path makes no conflict pair: it routes nothing to B.
  • Golden workspace-member diagnostic: expected/ holds only changed files.
  • Sparse crates.io index not treated as crates.io: missed attestation only.
  • Open in Cargo VEX: an ignored [patch] Socket pin still attests a transitive crate; bypass remedies name ambiguous or incomplete commands #2045 (pre-existing on main or remedy text): an ignored [patch] Socket pin as a transitive crate's only wiring; ambiguous cargo update -p beside a tagged copy; no re-lock advice for a stale other-registry entry. Sparse URLs: cargo's CanonicalUrl also drops the trailing /, so no change.
  • Single-read liveness, one async member walker, cached lock Socket uuids, URLs from format models: left for lock-model unification.

🤖 Generated with Claude Code

Redirect-ledger liveness step 1 kept a record alive when any lock
inventory entry's resolved url merely CONTAINED the patch uuid, so a url
on any host spelling the uuid (or a Socket url for another patch whose
grant token is that uuid) kept a dead hosted record alive.

Step 1 now classifies the url with the one origin classifier,
`patch::redirect::hosted_patch_uuid`, over Socket's host and the run's
patch-server origins, and requires the classified uuid to equal the
record's. Discovery records the origins it was run with
(`Discovery::patch_server_origins`, set in `discover_with_ctx`), so
liveness classifies with exactly the origins discovery did; every
`LedgerLiveness` caller already holds a discovery built from the run's
`DiscoverOptions`. Socket-host and configured-origin urls for the
record's uuid stay live; anything else falls to step 2 (resolves
elsewhere: dead).

The uv script-lock regression test relied on the substring match for an
unconfigured internal host; it now shows that host dead without the
origin and live with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hosted mode pins a patched crate in the root Cargo.toml AND every
workspace-member manifest that declares it, and vendored mode's
live-redirect refusal reads member manifests too. Discovery read only
the root manifest, so a crate the root does not declare was "the lock is
the only word": a member whose declaration was reverted to crates.io
(no `registry = "socket-patch-<uuid>"`) left a ref attested although
cargo re-resolves that dependency from crates.io.

Discovery now enumerates the members the way the hosted rewriter does
(`cargo_workspace::member_manifests_in`, filtered by the rewriter's own
`is_cargo_member_manifest_key`, so nothing under `.socket/` or `target/`)
and applies its manifest-declaration check across root + members. Each
declaration carries its file: diagnostics, the conflict-pair Cargo.toml
ref and lockless `UnlockedPin`s name the member manifest that holds the
pin. `[patch]` stays root-only (cargo ignores a member's).

The member walk over a `DiskSnapshot` now notes every manifest it reads
and directory it probes or lists with the snapshot instead of asking for
its raw root, so a discovery run under a read-set recording (the re-scan
cache) keeps the recording usable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iveness

Cargo member walk (review 4, 6, 7):
- `cargo_workspace::member_manifests_in` is now async over the
  ProjectView: manifests are read with `read_text` (concurrently, bounded,
  ordered), globs list with `list_dir`, and directory probes use the view's
  own mediated `is_dir` / `is_symlink`. A snapshot's overlays and content
  cache apply and its recording notes the walk, so fresh manifests (a
  `.socket/` copy followed through `[patch]` included) no longer leave a
  racy read set. `DiskSnapshot::touch` / `touch_listing` are private again;
  `SnapshotTree` is gone. The in-memory engine keeps the sync walk
  (`member_manifests_in_memory`); disk-only callers keep `member_manifests`.
  A test pins the view walk to the disk walk over globs, `**`, excludes,
  path dependencies, `[patch]` copies, dot / `target` dirs and a
  symlinked member.

Discovery (review 5, 8):
- Members are walked only when they can matter: a lock with a
  Socket-sourced package, or no lock (then parsed only when some manifest
  carries a `socket-patch-` pin). The walk's texts are adopted into the
  ctx's read log and rule-11 recognition instead of being read again.
- The root `DocumentMut` is moved into the manifest list, not cloned.

Lockless pins (review 1, 3):
- `UnlockedPin` gains `excluded_reqs`: the requirements of the crate's
  unpinned declarations. The rewriter leaves a declaration alone only when
  its requirement excludes the patched version, so a member's
  `serde = "0.9"` beside a pinned `serde = "1"` no longer suppresses the
  pin; a ledger version must satisfy every pinned requirement and no
  excluded one. An unpinned declaration with no requirement still blocks.
- One pin and one diagnostic per (crate, patch): the union of the
  pinning declarations' requirements, every pinning manifest listed once.

Liveness (review 2):
- A lock entry whose url has the hosted shape for the record's uuid on an
  origin this run was not given is `RecordLiveness::Undecided`: never
  attested (`redirect_record_live` / `LedgerLiveness::redirect_record`
  stay attestation-grade) and never judged dead. `scan`'s takeover
  classifier and hosted-wiring-retained advisory use the new
  `LedgerLiveness::redirect_record_retained`, so a record written against
  a patch server a later run is not pointed at keeps its wiring. A url
  that merely contains the uuid (file name, query) is still dead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cargo resolves each source on its own. When one declaration of a crate
pins it to `socket-patch-<uuid>` but another (the root's or a workspace
member's) names no Socket registry (crates.io, another registry, a path
or git) with a requirement the locked patched version satisfies, that
declaration builds a separate, unpatched copy, possibly of the very
version the pin covers. Discovery still attested the Cargo.lock ref
because "some declaration pins the patch" was enough.

The hosted rewriter pins every declaration, so this mixed state is a
partial revert or a hand edit. The ref now stays (rollback, remove and
the ledgers' liveness still see the pin) but is marked unattested with
the new `UnattestedKind::CargoUnpinnedDeclaration`, naming the unpinned
declaration's manifest. When Cargo.lock already locks a same-version
copy from another source beside the Socket entry, the detail names that
lock entry as direct evidence. A declaration whose requirement excludes
the patched version, and a `[patch]` registry entry (which redirects
declarations rather than adding one), do not withhold the statement.

The CLI omits the ref as `vex_cargo_unpinned_declaration`; documented in
CLI_CONTRACT.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for the unpinned-declaration mark. Each declaration of a
Socket-locked crate is now classified once (`route`): routed to a Socket
patch (its own `socket-patch-<uuid>` registry, or a Socket `[patch.<source>]`
registry entry that redirects it), unpinned (cargo builds its own copy),
or saying nothing about the locked version.

- A crates.io (or registry) declaration that a Socket `[patch.<source>]`
  registry entry in the root manifest or project config redirects is no
  longer counted as unpinned.
- A declaration naming `registry = "socket-patch-<uuid>"` beside `path`,
  `git` or `registry-index` is not a pin (the hosted rewriter's own pinned
  check): cargo builds that other source, so it now marks the ref.
- Requirements go through `patch::redirect::cargo_req_selects` with the
  lock's other versions of the crate, and the detail says whether a
  re-scan can pin the declaration: only a crates.io declaration whose
  requirement matches no other locked version; otherwise the rewriter
  refuses (`redirect_cargo_toml_dep_unrewritable`), so the remedy is to
  change the declaration or roll back. The CLI note's remedy is now
  generic and the per-case remedy lives in the detail.
- `registry-index` declarations are reported as such, not as crates.io.
- A `[workspace.dependencies]` template counts only when some manifest
  inherits it (`workspace = true`).
- When the lock already holds a same-version copy from another source,
  no mark is added: the twin rule (#679) withholds the ref and names it.
- Root pinned to patch A with a member pinned to patch B (for the locked
  version, and B not resolved by the lock) now emits B's Cargo.toml ref
  beside A's, so the CLI's wiring-conflict gate runs.
- `Decl` carries a `DeclSource` enum and a `live` flag; the origin text is
  formatted only for messages, and `[patch]` pins are built with
  `Decl::patch` (no dead version parameter).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`vendor` wires a crate through the root's `[patch.crates-io]`, which
cargo applies only to declarations from crates.io. A workspace member
(or the root) that declares the crate from another registry, a
`registry-index` or git, at a requirement that may select the patched
version, resolves its own unpatched copy. The vendored ref stayed
attested beside it because the lock copy claim only reads sourceless
entries.

The ref now stays (rollback, remove and the ledgers still see the
wiring) but is marked unattested with the new
`UnattestedKind::CargoPatchBypassed`, naming the declaration. A
same-version Cargo.lock entry from any registry or git source beside the
copy is marked the same way, naming the lock entry. The check reads the
declarations through #2019's classification: live declarations only, a
registry dependency counts as covered when a `[patch.<registry>]` table
pointing at the same copy redirects it (`Decl::patched_by`), a path
declaration (the user's own fork) keeps the existing "ignored" rule, and
a `socket-patch-*` registry dependency is hosted wiring. The CLI omits
the ref as `vex_cargo_patch_bypassed`; documented in CLI_CONTRACT.md.

The workspace member walk ran only when the lock held a Socket-sourced
package (or, with no lock, a manifest named a `socket-patch-` registry).
A vendored copy's lock entry has no source and it has no such pin, so
the walk now also runs whenever the root manifest or the project config
wires a Socket copy through `[patch]`, whatever the lock's state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three Cargo follow-ups to the declaration classification:

Lockless pins read declarations like locked ones. A
`socket-patch-<uuid>` registry beside `path`, `git` or `registry-index`
is no pin (cargo builds that source, and the rewriter's own pinned check
agrees), so it no longer adds its requirement to an `UnlockedPin` or
makes one on its own. It counts as an unpinned declaration instead,
excluding the versions it accepts (every version, without a
requirement). A `[workspace.dependencies]` template no manifest
inherits is no declaration: a Socket-pinned one makes no lockless pin,
and an unpinned one excludes nothing from the root's pin. A non-Socket
`[patch]` registry entry no longer counts as an unpinned declaration
either (it redirects declarations, it adds none).

`DeclSource::Git` keeps its URL, and `Decl::patched_by` matches a
`[patch."<repository URL>"]` table against it (a trailing `/` or `.git`
ignored). So a git declaration whose repository table points at the
same vendored copy no longer withholds that copy, while one from
another repository still does. Hosted routing gains the same match for
a Socket registry pin in such a table.

When no declaration pins the lock's patch, a pin to another patch now
makes the conflict pair only when it is live and its requirement may
select the locked version (`cargo_req_selects`). One that excludes it is
about another version; the stale lock entry is then diagnosed and
dropped like any unrouted one.

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

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@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.

Stale Bugbot comment from a previous run.

foreign_hosted_uuid parsed the raw lock url, while hosted_patch_uuid
first unescapes \/, percent-decodes and strips a Cargo sparse+ /
registry+ prefix. A hosted url on an unconfigured origin that only
parses after that normalization was never Undecided and could be
marked Dead, dropping live wiring. Share the normalization. Addresses
one of the two Bugbot findings on this PR.

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

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@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.

Stale Bugbot comment from a previous run.

@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review November 6, 2026 12:12
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: this needs an owner decision. Head 94af3e4f. CI is green apart from one PR-caused failure (coverage (2)) and the main-wide pdm/hishel e2e leg (with its ci-ok aggregate).

The failure. e2e_vex_lockfile yarn::uuid_on_a_foreign_host_is_not_a_hosted_reference (yarn.rs:1048–1085), for Classic with origin https://evil.example + a redirect ledger, not installed:

  • the test expects package_not_found
  • the PR returns redirect_unwired

Cause. The PR intends this change. Since 43fd5aa ("Match ledger liveness uuids by origin") and 650143e (RecordLiveness::Undecided), a lock entry whose hosted-shaped url is on an origin this run wasn't given is never attested. That contradicts what is documented today:

  • CLI_CONTRACT.md § "Patch hosts (manifest-less VEX)": "A pre-v5 redirect ledger still on disk can keep such a record in play under the pre-v5 rule: judged by the ledger's own recorded wiring, attesting only an installed tree that hashes to the record unless --no-verify…"
  • The e2e test's "DOCUMENTED FALLBACK" comment says the same. Its later cells expect hash_mismatch for a tampered tree and an attestation for a verified tree.

Question for the owner. Should a pre-v5 redirect-ledger record whose lock url sits on an unconfigured host (a staging server used without --patch-server-url, or a look-alike) still attest an installed tree that hashes to the record?

  • No (the PR as written). Next step: rewrite this e2e cell to expect redirect_unwired throughout, and update the CLI_CONTRACT paragraph.
  • Yes (the documented contract). Next step: vex must treat Undecided as "judged by the ledger, verify-only" instead of omitting it.

I didn't change either side, because either way the documented user-facing contract changes, and I won't rewrite a test's expectations just to get it green.

Minor, for whoever picks this up: in 94af3e4, hosted_patch_url_uuids's doc comment now sits above normalized_hosted_url (crates/socket-patch-core/src/patch/redirect/mod.rs ~7646). It needs splitting back.

Bugbot reviewed 94af3e4f.


Generated by Claude Code

False attestation: the vendored bypass check counted a declaration as
redirected by any root or config `[patch]` entry pointing at the copy,
including a manifest entry cargo ignores (the project config redefines
its crates.io key, or a crates.io URL table replaces `[patch.crates-io]`).
Cargo then builds whatever replaced it for those declarations, while the
copy's other, honored entry was attested. Only honored entries count now:
the root's `[patch]` entries are collected once with their shadow reason
(a `Shadow` built from the root manifest and the config), and the bypass
check sees only the unshadowed ones. `vendored` (the member gate) is
derived from the same list instead of re-reading every `[patch]` table.

One classifier: the bypass check runs each declaration through `route`
with the copy's honored `[patch]` tables as the pins, as hosted pins do,
passing the lock's other versions of the crate. A path declaration (the
user's own fork) is still left alone.

`patched_by` now covers a `registry-index` declaration with a
`[patch."<index URL>"]` table for the same index (a `registry+` prefix,
host case and a trailing `/` ignored; `sparse+` is another source), and
treats a `registry-index` naming the crates.io git index as crates.io.
The sparse crates.io index stays a separate source. Git URLs compare as
cargo's `CanonicalUrl` spells them (one trailing `/` and a `.git` suffix
dropped, a GitHub URL lowercased whole, any host lowercased).

The bypass detail ends with the remedy for its evidence: a declaration
(re-running `vendor` does not help), a crates.io lock entry beside a
`[patch.crates-io]` copy (stale: re-lock), or another lock entry
(`cargo tree -i` names its declarer). The CLI note no longer claims
re-vendoring never helps.

Diagnostics: a Socket pin to another patch that the lock does not
reflect (its requirement excludes every locked version) used to vanish
once any Socket entry of the crate was locked; it is now diagnosed
`patched_ref_unattributable`. A well-formed `socket-patch-<uuid>`
registry that pins nothing (beside `path` / `git` / `registry-index`, or
on a `[workspace.dependencies]` entry no manifest inherits) is diagnosed
`patched_ref_invalid`. The committed `workspace-member` redirect golden
gains that diagnostic: its `expected/` tree omits the unchanged
inheriting member, so the pinned template reads as uninherited there.

`Decl::describe` and `same_version_foreign_entry` are shared by the
hosted and vendored checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Second review round on Cargo member liveness and origin matching.

- `read_members` returned nothing for a parsed lock with no
  Socket-sourced package or an unparseable one, so a member's
  `socket-patch-` pin beside a stale lock lost its "stale lock, or a
  server outside the allowlist" diagnostic and its uuid was no longer
  recognized (rule 11). Members are now parsed whenever the lock is
  Socket-sourced or a `socket-patch-` name appears in a member's text,
  the root's or the config's, whatever the lock says; a root or config
  `[patch]` pin now counts too (it governs members' declarations).
- `foreign_hosted_uuid` read any url on any host ending in the record's
  uuid as a foreign patch server's, so an artifact store's
  `/blobs/<uuid>/` url kept a dead hosted record retained (and raised a
  takeover note). It now needs a patch server's path shape (a `patch`,
  `patch-registry` or `gopatch` segment).
- Two inserted functions split their neighbours' doc comments
  (`hosted_patch_url_uuids`, `log_read`); restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Second review round. The unpinned-declaration remedy sent every
declaration with an explicit `registry` key to "it names another
registry ... roll the patch back", including `registry = "crates-io"`,
which the hosted rewriter re-pins (#386); it now says a re-scan pins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o fix/cargo-vendored-twins-and-split-patches

# Conflicts:
#	crates/socket-patch-core/src/vex/discover/cargo.rs
FALSE ATTESTATION (found reviewing #2019; fixed here, where `Shadow`
lives): a root `[patch.crates-io]` Socket registry pin cargo ignores
(a `[patch."<crates.io URL>"]` table replaces it, or the config
redefines the key) still counted as pinning every crates.io
declaration of the crate, so a member's plain declaration was taken as
routed to the patch and the hosted ref attested while cargo builds
crates.io. Such an entry stays a declaration (Socket's wiring is
present, the ref stays) but is not live, and `route` only weighs live
`[patch]` pins, as the vendored bypass check already weighs only
honored entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cargo replaces a manifest [patch] item with a project-config item of the
same source and key, whatever the source table and whether either wires a
path. Shadow only looked at crates.io tables and treated two path-less
items (a registry pin and a git fork) as the same wiring, so a config fork
left the manifest's Socket pin live and its stale lock row could be
attested. Only an identical path is now the same wiring, and named and
URL tables are compared too (URLs as an index key).

The ineffective-registry diagnostic also called a shadowed [patch] pin an
uninherited [workspace.dependencies] template; it now says cargo ignores
the [patch] entry.

Addresses Bugbot 4501098254, 4501098256 and 4501098258.

Co-Authored-By: Claude <noreply@anthropic.com>
Shadow compared a config's [patch."<url>"] table with the manifest's
through index_key, which keeps a trailing .git and a GitHub path's case.
Cargo canonicalizes both, so a config item under a canonical-equivalent
git URL replaced the manifest's Socket copy while discovery still counted
it. Non-sparse URL tables now compare through git_key; sparse+ ones stay
index keys (another source).

Addresses Bugbot 4501313607.

Co-Authored-By: Claude <noreply@anthropic.com>
Main's #2002 moved the hosted-origin helpers out of patch::redirect into
crate::origin, and DiscoverCtx now holds an OriginPolicy instead of the
raw origin list. Carry this PR's normalized_hosted_url split into
origin.rs, point its liveness and foreign-origin checks at
crate::origin::hosted_patch_uuid, and read the run's origins from
ctx.origin.origins().

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

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: merged main (568e376). #2002 moved the hosted-origin helpers from patch::redirect into crate::origin, and DiscoverCtx now holds an OriginPolicy. Changes:

  • This PR's normalized_hosted_url split now lives in origin.rs.
  • The liveness and foreign-origin checks call crate::origin::hosted_patch_uuid.
  • Discovery::patch_server_origins is read from ctx.origin.origins().

New head ca08f29; vex::, origin and cargo core tests (1325) pass locally. The agent:needs-human question is still open.


Generated by Claude Code

@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 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared a fix for 1 of the 2 issues found in the latest run.

  • ✅ Fixed: Member pin attests unreverted lock
    • Added check in hosted_from_lock to detect when unpinned declarations from different files (root vs member) match the locked version and correctly skip attestation.
  • ⚠️ Could not fix: Foreign host skips VEX verification
    • Implemented logic to handle Undecided verdict and proceed to hash verification, but test still fails because redirect_record_verdict returns Dead instead of Undecided for foreign hosts.

Create PR

Or push these changes by commenting:

@cursor push e8b393275a
Preview (e8b393275a)
diff --git a/crates/socket-patch-cli/src/commands/vex_sources.rs b/crates/socket-patch-cli/src/commands/vex_sources.rs
--- a/crates/socket-patch-cli/src/commands/vex_sources.rs
+++ b/crates/socket-patch-cli/src/commands/vex_sources.rs
@@ -82,7 +82,7 @@
 use socket_patch_core::utils::purl_key::{canonical_base_purl, PurlKey};
 use socket_patch_core::vendor::state::{VendorArtifact, VendorEntry, VendorState};
 use socket_patch_core::vex::discover::{
-    vendor_ref, Discovery, LedgerLiveness, PatchedRef, WiringMode,
+    vendor_ref, Discovery, LedgerLiveness, PatchedRef, RecordLiveness, WiringMode,
 };
 use socket_patch_core::vex::{FailedPatch, Unattested, UnattestedKind};
 
@@ -657,15 +657,19 @@
                 continue;
             }
         } else if cand.redirected {
-            let live = assumed.contains(strip_purl_qualifiers(&cand.key))
-                || liveness.redirect_record(&cand.key, &cand.uuid).await;
-            if live {
+            let verdict = if assumed.contains(strip_purl_qualifiers(&cand.key)) {
+                RecordLiveness::Live
+            } else {
+                liveness.redirect_record_verdict(&cand.key, &cand.uuid).await
+            };
+            if verdict == RecordLiveness::Live {
                 Basis::Hosted {
                     lockfile_basis: false,
                 }
-            } else if cand.manifest_owned {
-                // The hosted claim is dead but the manifest still owns the
-                // purl: verify it as the agent-mode patch it now is.
+            } else if verdict == RecordLiveness::Undecided || cand.manifest_owned {
+                // Undecided: foreign host, verify by hash. Manifest-owned:
+                // the hosted claim is dead but the manifest still owns the
+                // purl, verify it as the agent-mode patch it now is.
                 Basis::Installed
             } else {
                 notes.extend(dead_claim_notes(
@@ -784,7 +788,13 @@
             continue;
         }
         match basis {
-            Basis::Installed => {}
+            Basis::Installed => {
+                // Foreign-host candidates from a redirect ledger are verified
+                // by hash but still marked as "(redirected)" for provenance.
+                if cand.redirected && !cand.manifest_owned {
+                    plan.redirected.push(cand.key.clone());
+                }
+            }
             Basis::Vendored(entry) => {
                 plan.vendor_entries.insert(cand.key.clone(), *entry);
             }

diff --git a/crates/socket-patch-core/src/vex/discover/cargo.rs b/crates/socket-patch-core/src/vex/discover/cargo.rs
--- a/crates/socket-patch-core/src/vex/discover/cargo.rs
+++ b/crates/socket-patch-core/src/vex/discover/cargo.rs
@@ -629,10 +629,70 @@
             out.push(lock_ref);
             continue;
         };
-        if regs
+        let pinned: Vec<&Decl> = regs
             .iter()
-            .any(|d| d.registry.as_deref() == Some(reg.as_str()))
-        {
+            .filter(|d| d.registry.as_deref() == Some(reg.as_str()))
+            .collect();
+        if !pinned.is_empty() {
+            // Check if there are unpinned declarations from different files
+            // that would resolve independently. A [patch] entry applies
+            // globally, but a member dependency with a registry does not
+            // affect the root's declaration (or vice versa).
+            let unpinned: Vec<&Decl> = regs
+                .iter()
+                .filter(|d| d.registry.as_deref() != Some(reg.as_str()) && !d.socket_shaped())
+                .collect();
+            // Only check if unpinned declarations are from different files than
+            // the pinned ones (e.g., root vs member), indicating independent
+            // resolution. If all declarations are from the same file (e.g., a
+            // [patch] in Cargo.toml plus a regular dep in Cargo.toml), the
+            // [patch] applies globally.
+            let different_files = !unpinned.is_empty()
+                && unpinned.iter().any(|u| {
+                    pinned.iter().all(|p| {
+                        // Consider declarations from different files, but treat
+                        // Cargo.toml and .cargo/config.toml as the "root" scope
+                        // since [patch] entries from either apply globally.
+                        let p_is_root = p.file == CARGO_TOML
+                            || p.file == ".cargo/config"
+                            || p.file == ".cargo/config.toml";
+                        let u_is_root = u.file == CARGO_TOML
+                            || u.file == ".cargo/config"
+                            || u.file == ".cargo/config.toml";
+                        // Different if one is root-scope and the other isn't,
+                        // or if both are non-root and from different files.
+                        (p_is_root != u_is_root) || (!p_is_root && p.file != u.file)
+                    })
+                });
+            if different_files {
+                use crate::patch::redirect::{cargo_req_selects, CargoReqMatch};
+                let matches_locked = unpinned.iter().any(|d| {
+                    let req = d.version.as_deref();
+                    matches!(
+                        cargo_req_selects(req, &pkg.version, &[]),
+                        CargoReqMatch::Ours | CargoReqMatch::Ambiguous
+                    )
+                });
+                if matches_locked {
+                    let files = decl_files(unpinned.iter().copied());
+                    let verb = if files.contains(", ") {
+                        "declare"
+                    } else {
+                        "declares"
+                    };
+                    out.diag(
+                        DIAG_REF_INVALID,
+                        CARGO_LOCK,
+                        format!(
+                            "{CARGO_LOCK}: {purl} resolves from Socket patch {uuid}, but {files} \
+                             {verb} {} without registry = \"{reg}\" — cargo re-resolves it from the \
+                             declared source; not counted",
+                            pkg.name
+                        ),
+                    );
+                    continue;
+                }
+            }
             out.push(lock_ref);
             continue;
         }

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 ca08f29. Configure here.

Claude (claude) and others added 7 commits November 9, 2026 02:35
- vex/discover/mod.rs: keep both sides' UnattestedKind variants;
  CargoUnpinnedDeclaration stays after MavenClassifierUnpatched, followed
  by the base's pnpm/npm/sbt/NuGet/Go kinds and its file_scoped() impl
  (Cargo is not file-scoped).
- vex_sources.rs: keep both sides' note constants and unattested_note()
  arms; keep the base's new tests (npm-family tags, Go workspace marks)
  and re-add the PR's Cargo gating test after them.
- CLI_CONTRACT.md: take the base's "Unattested references" paragraph,
  bump its count to nine ("one of them" for the sbt case listed
  elsewhere) and append the PR's Unpinned Cargo declarations case; keep
  both sides' rows in the vex reasons table.

Co-Authored-By: Claude <noreply@anthropic.com>
…plit-patches

The base picked up #2012's main merge, which added the npm-family,
sbt, NuGet and Go UnattestedKind variants and their vex notes. This
branch adds CargoPatchBypassed. The two sides only add entries, so
the merge keeps both:
- vex/discover/mod.rs: CargoPatchBypassed comes after
  CargoUnpinnedDeclaration, then the base's variants and file_scoped().
  Cargo stays non-file-scoped.
- vex_sources.rs: keeps both sides' note constants and
  unattested_note() arms.
- CLI_CONTRACT.md: keeps the base's paragraph, appends this branch's
  vendored-Cargo case, and changes the case count from "Nine" to
  "Ten". The reasons table keeps both sides' rows.

Co-Authored-By: Claude <noreply@anthropic.com>
Cargo reads an explicit `registry = "crates-io"` the same as no
registry, so a Socket pin under `[patch."https://github.com/rust-lang/
crates.io-index"]` redirects such a declaration. `patched_by` only
compared the key to the table name, so it missed that case and marked
the lock ref CargoUnpinnedDeclaration. Route the explicit spelling
through `is_crates_io_source` like a bare declaration (Bugbot on #2019).

Co-Authored-By: Claude <noreply@anthropic.com>
…plit-patches

Resolve the patched_by conflict: keep this branch's per-source match
(registry, registry-index, git) and apply the base's fix that reads an
explicit registry = "crates-io" as crates.io in the Registry arm.

Co-Authored-By: Claude <noreply@anthropic.com>
patch_bypassed only withheld the vendored ref for declarations route()
finds Unpinned. A member declaring the crate from another socket-patch
registry routes to Pin(other) and was skipped. When the lock resolves the
crate from that patch, its hosted ref sits beside the vendored one and the
CLI's wiring_conflict gate refuses both. When the lock is stale and holds
no entry from that patch, only a patched_ref_unattributable diagnostic was
left, and patch A was attested even though the member re-resolves to
patch B. Mark the vendored copy CargoPatchBypassed in that case instead.

Co-Authored-By: Claude <noreply@anthropic.com>
The exemption for a member pinned to another Socket patch trusted any
lock line from that patch. hosted_from_lock drops a line whose project
registry definition disagrees, and a line at another version is no ref
for this package, so wiring_conflicts never fired and the vendored copy
stayed attested. Exempt only when the hosted pass emitted a ref for that
patch at this very purl that is not itself withheld.

Co-Authored-By: Claude <noreply@anthropic.com>
Owner decision on #2012: a pre-v5 redirect-ledger record whose wiring
resolves the package from a hosted-shaped url on an origin this run was
not given (a staging server used without --patch-server-url, or a
look-alike host) is never attested. vex reports it as redirect_unwired,
whatever is installed and even under --no-verify.

Step 1 of redirect_record_verdict already returned Undecided for a lock
inventory entry carrying such a url. The formats whose inventory has no
url (yarn berry __archiveUrl bindings, Cargo.lock registry sources, pom
repositories, Gemfile source blocks, go.mod replace module paths) fell
through to the recorded-wiring step 3 (or the gem step 0) and attested.
Those steps now return Undecided when any recorded file spells a
hosted-shaped url for the uuid on an unconfigured origin. A Socket-host
spelling elsewhere in the files does not clear it.

foreign_hosted_uuid drops userinfo before reading the origin, so a
https://patch.socket.dev@evil.example/... look-alike is Undecided in
every step rather than Dead in step 1 and Live in step 3.

vex adds a vex_claim_unwired note for such a record naming the cause and
--patch-server-url. CLI_CONTRACT.md "Patch hosts (manifest-less VEX)"
and the vex_sources module docs state the new rule. The yarn, cargo and
maven e2e cells that asserted the old staging fallback now expect
redirect_unwired.

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

Copy link
Copy Markdown
Collaborator Author

The owner settled the agent:needs-human question: fail closed, keep the PR's behavior. A pre-v5 redirect-ledger record whose wiring resolves from a hosted-shaped URL on an unconfigured origin is never attested and is reported as redirect_unwired, even when the installed tree verifies and even under --no-verify.

Changes in cb98b9b:

  • The e2e cells for yarn (the CI failure), cargo and maven now expect redirect_unwired.
  • The same rule now also applies when the URL lives only in recorded wiring files (berry, Cargo.lock, pom, Gemfile, go.mod). Before, those cases still attested.
  • Userinfo look-alikes are treated as foreign.
  • vex adds a vex_claim_unwired note that points to --patch-server-url.
  • CLI_CONTRACT.md § "Patch hosts (manifest-less VEX)" is updated.

/code-review high findings on the change: 6 fixed, 3 declined (details in the PR body).

Review threads:

agent:needs-human is removed.

Review on #2019 (/code-review high):

- The vex_cargo_unpinned_declaration remedy said "re-run socket-patch
  scan" whenever every unpinned declaration was a crates.io one the
  rewriter would pin. The rewriter skips the whole dependency on its
  first refusal, including one over a declaration discovery doesn't count
  as unpinned (the pin's own requirement matching another locked version,
  a path template nobody inherits). Discovery now runs the rewriter's own
  Cargo.toml planner dry over the same manifests (plan_cargo_tomls,
  extracted from rewrite_cargo_with) before it promises a re-scan.
- A declaration naming registry = "socket-patch-<uuid>" beside a path,
  git or registry-index read as if the registry were missing. The detail
  now says the source overrides it, and says "those declarations" when
  there are several.
- manifest_declarations looks up inherited template keys in a set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…plit-patches

Brings #2012's fail-closed liveness and #2019's round-3 review fixes.
Conflict in beside_the_pin's declaration text: kept this branch's
Decl::describe and moved #2019's 'which overrides its registry' clause
into it, so every caller says it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review on #2024 (/code-review high), round 4:

- The "resolves it only from other Socket patches" diagnostic fired
  beside a same-version crates.io twin; it now needs a lock with no
  non-Socket entry of the crate.
- Shadow resolves a [patch.<registry>] table through the config's
  [registries] index, so a config [patch."<index>"] item shadows it.
- An identical path-less Socket registry [patch] pin in the manifest and
  the config is the same wiring, not a shadow.
- patched_by (registry-index) and same_patch_source share
  patch_source_key, with CanonicalUrl rules on both.
- patch_bypassed's conflict-gate check matches the CLI's bypassed_ref
  (file-scoped marks only); it routes each declaration once; its detail
  also names other-patch declarations; a git lock entry a git [patch]
  table redirects gets the stale/re-lock remedy.
- Reuse patch::redirect::is_crates_io_source for lock sources.
- Move the manifests comment back above its vector.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[agent] Review findings (Claude Opus + codex, independent passes):

  1. Root-reverted pin is attested (crates/socket-patch-core/src/vex/discover/cargo.rs:634, both reviewers). hosted_from_lock now pools root and member declarations and attests if any one carries registry = "socket-patch-<uuid>". Input: root serde = "=1.0.1" (pin reverted), crates/a still serde = { version = "=1.0.1", registry = "socket-patch-<uuid>" }, stale lock holding only the Socket-sourced serde → ref attested, but the root builds crates.io serde unpatched. On main the root alone decided this (DIAG_REF_INVALID). #2019 fixes it, so this PR should not land on main without #2019. The PR body's "main behaves the same" for mixed states is true only in the root-pinned/member-unpinned direction.
  2. Lowercase percent escapes bypass the foreign-origin guard (vex/discover/mod.rs:2710, codex). A Berry __archiveUrl=https%3a%2f%2fevil.example%2fpatch%2f<U>%2fx-1.0.0.tgz is not normalized; since Berry inventory omits the resolved URL, the recorded-file fallback returns Live and a legacy record for U attests under vex --no-verify.

Minor: cargo_workspace.rs:136-147 silently drops a symlinked/unreadable member manifest (not a regression).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[agent] Approving per maintainer decision; see the findings in the review above. Land together with #2019, which fixes finding 1.

Behavior-preserving cleanup of #2012 after #2019 and #2024 were folded in:

- One `distinct<T>` replaces four hand-written order-preserving dedups;
  `describe_all` and one `other_declared` replace repeated describe/join
  chains.
- `DeclSource::patched_by` is the one redirect rule; the bypass check's
  stale-entry test maps the lock source to a `DeclSource` instead of
  re-deriving crates.io and git matching. `index_key` is folded into
  `patch_source_key`.
- `beside_the_pin` collects the lock's Socket uuids once; the manifests
  are serialized for the rewriter's dry planner once per discovery
  (`ManifestTexts`), not per crate. `vendored_from_patches` takes the lock
  from `Bypass`; the two `decls` passes in `extract` are one.
- `foreign_hosted_uuid` moves to `origin::hosted_shape_uuid`, so
  `normalized_hosted_url` is private again; the cheap accepted-origin
  check runs first in `foreign_hosted_url_in_files`.
- Comments and docs no longer narrate review history; the cargo module
  doc points at `beside_the_pin` / `patch_bypassed` instead of restating
  them; CLI_CONTRACT.md states the registry-index rule once and the
  `apply` sentence parses.
- Tests share `workspace_root`, `member`, `one_mark` and `shadow` instead
  of respelling manifests and config parsing; the e2e fail-closed comments
  point at CLI_CONTRACT.md.

Review of the simplify diff (/code-review high): the stale-lock remedy
named `[patch.crates-io]` and `socket-patch vendor` for a git entry too;
it now names the table that redirects the entry and offers `vendor` only
for crates.io. CLI_CONTRACT.md keeps the member-walk and uninherited-
template rules the trim had dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mikolalysenko Mikola Lysenko (mikolalysenko) changed the title Read Cargo members in discovery; match liveness uuids by origin Cargo VEX: read member manifests, match liveness by origin, withhold mixed and vendored bypasses Nov 9, 2026
@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Nov 9, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Nov 9, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final reviewer: the merge queue removed this PR at 22:11Z because #2018 merged and f1eb764 now conflicts with main. It is still approved, and approvals stay valid, so once main is merged in and ci-ok/clippy are green, it goes back in the queue (by me or the next run). I didn't push because the branch had a push in the last 45 minutes.


Generated by Claude Code

Brings #2018 (never patch a user-patched pnpm/yarn/vlt dependency).
Conflict in CLI_CONTRACT.md "Unattested references": kept main's
patchedDependencies wording and its new pnpm/Yarn/patch-package
paragraph, and this branch's two Cargo cases (count Eight -> Ten).

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

Copy link
Copy Markdown
Collaborator Author

I merged origin/main (#2018) to clear the conflict, in 678f42e. It's a merge only, with no other code changes.

There was one conflict, in CLI_CONTRACT.md, the "Unattested references" bullet:

vex_sources.rs, vex/discover/mod.rs and hosted/engine.rs merged automatically.

Ran locally: core vex:: origin cargo hosted:: patch::redirect:: (2303 tests), CLI --lib, e2e_vex_lockfile and e2e_vex_redirect. All pass, and clippy and fmt are clean.

@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit 3ef8f20 Nov 9, 2026
49 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the fix/cargo-members-liveness-origin branch November 9, 2026 22:33
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.

4 participants