Fix ten hosted cargo redirect bugs found by a real-cargo matrix - #255
Merged
Mikola Lysenko (mikolalysenko) merged 21 commits intoSep 25, 2026
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issues.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 92ad228. Configure here.
Wenxin Jiang (Wenxin-Jiang)
approved these changes
Sep 25, 2026
A project that locks two versions of one crate (cfg-if 1.0.4 beside a renamed cfg-if-legacy at 0.1.10) had every same-named declaration pinned to the last patch's registry: the manifest planner matched the crate name only, so `^0.1.10` pointed at a registry that serves 1.0.4 and `cargo fetch --locked` failed. `get <uuid>` for one version broke it the same way. The planner now pins a declaration only when its version requirement selects the patched version. A requirement that also matches another locked version refuses the dep (nothing written); `workspace = true` inheritors of an entry naming another version are skipped. `remove` had the matching defect: manifest edits are keyed by crate name, so removing one version also reverted the other version's pin (leaving its lock entry hosted) and dropped its registry edit from the ledger, so the created `.cargo/config.toml` survived the second removal. Manifest edits whose pin names a sibling version's registry lineage are no longer claimed. Golden: cargo/cargo/multi-version (the depscan TS twin lags; list it in TS_LAGGING on the next submodule bump). Real-cargo regression: e2e_redirect_cargo_shapes multi-version (fresh `cargo fetch --locked` + offline build links both patched copies; removing both purls restores every byte). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`remove` of a hosted cargo patch deleted the `[registries.socket-patch-…]` block it had appended to an existing config but kept the blank separator line it inserted before it, so a legacy `.cargo/config` holding `[net]\nretry = 2\n` came back as `[net]\nretry = 2\n\n`. It also collapsed every run of three newlines anywhere in the file, rewriting the user's own spacing. The block now leaves through the replay path's fragment removal, which takes the block's own separator and nothing else. Real-cargo regression: e2e_redirect_cargo_shapes legacy-config. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Hosted mode pinned only the root Cargo.toml. In a workspace whose member declares the patched crate itself (`cfg-if = "1.0.4"` beside siblings that inherit `[workspace.dependencies]`), the member stayed on crates.io while Cargo.lock was repointed at the patch registry, so `cargo fetch --locked` failed — and the dep was still reported redirected and attested. `scan`/`get --mode hosted` now read every workspace member manifest (`[workspace] members` globs minus `exclude`, plus in-root path dependencies) and the rewriter plans them all in the dep's single transaction: each direct declaration is pinned, `workspace = true` inheritors resolve against the root's entry, and a member that cannot be pinned refuses the dep everywhere. `remove` already reverts edits by path; its registry-block reference probe now covers member manifests too. Golden: cargo/cargo/workspace-member (the depscan TS twin is handed no member manifests and lags; list it in TS_LAGGING on the next submodule bump). Real-cargo regression: e2e_redirect_cargo_shapes workspace-direct-member. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The real-cargo shape suite now also runs `socket-patch vex` over each fresh checkout (ledger kept, patch server admitted) and requires exactly the shape's patches to be attested — multi-version and workspace-member redirects included. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every hosted cargo planner matched LF text, so a Windows checkout with CRLF Cargo.toml / Cargo.lock was refused outright (redirect_cargo_toml_dep_unrewritable / redirect_cargo_lock_pkg_not_found). A manifest, lock or config whose every line break is CRLF is now planned as LF and written back as CRLF, and its recorded edit fragments are stored CRLF so `remove` and rollback find them in the file. Files with mixed endings are left alone and keep refusing where the grammar does not match. The shared fragment remover (used for the appended registry block) inverts CRLF files as LF so it no longer strands a `\r` line. Golden: cargo/cargo/crlf (CRLF manifest, lock and legacy config; the depscan TS twin lags — list it in TS_LAGGING on the next submodule bump). Real-cargo regression: e2e_redirect_cargo_shapes crlf (endings kept, fresh `cargo fetch --locked` + offline build, byte-identical remove). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A hosted cargo redirect pins a crate with `registry = "…"` on its manifest declaration, which reaches only that declaration: a crate the project gets purely through another dependency cannot be redirected in hosted mode. That stays a refusal — nothing is written, recorded or attested, and a requested VEX fails the run — but the warning said only "no [dependencies] entry for X in Cargo.toml", which reads like a discovery bug. When Cargo.lock resolves the crate at the patched version the warning now says it is a transitive-only dependency, that it was NOT redirected and stays unpatched, and that `scan --mode vendored` (whose `[patch.crates-io]` covers the whole graph) or a direct declaration can patch it. The code (`redirect_cargo_toml_dep_not_found`) is unchanged. Golden: cargo/cargo/transitive-refusal (pins the refusal and its warning code). In-process: the refusal writes nothing, is not attested, and the envelope carries the transitive-only warning. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The multi-version, workspace-member, legacy-config and CRLF shapes ran only against the toolchain's own v4 lock, so nothing in-repo checked them against a committed v1 lock (full-id dependency edges, checksums in `[metadata]`) or v2/v3. The shape suite now honours SOCKET_PATCH_CARGO_E2E_LOCK_VERSION like the other real-cargo suites and joins the cargo-vex-matrix CI job. The shared lock re-encoder kept only dependency names, which cannot tell two locked versions of one crate apart; it now keeps each edge's package id and writes the shortest form cargo writes for that format (`name`, `name version`, or the full id; always the full id in v1). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A hosted cargo redirect pins a crate with `registry = "…"` on each manifest declaration and repoints its one Cargo.lock entry. When a crates.io (or git) crate in the lock also depends on it — cfg-if, libc and serde usually are both direct and transitive — that edge keeps resolving from crates.io. The repointed lock then fails `cargo fetch --locked`, a plain build adds the crates.io copy back beside the patched one, and the dependent crate compiles the unpatched code, while scan reported the crate redirected and VEX attested it. The same happened for a path package whose manifest the rewriter never saw (outside the project root, or behind a symlink). Before committing a dep, every Cargo.lock package that depends on it (any edge spelling: `name`, `name version`, or the full v1 id) must be a source-less package whose manifest was planned and pinned. Otherwise the dep is skipped with the new additive warning `redirect_cargo_transitive_dependents`, which names the dependents, says the crate was NOT redirected and stays unpatched, and points to `scan --mode vendored`. Nothing is written, recorded or attested. Golden: cargo/cargo/shared-dependency (the depscan TS twin lags — list it in TS_LAGGING on the next submodule bump). Unit: registry, git, v1 full-id and unplanned-path dependents refuse; another version's edge and a planned member do not. Real cargo: e2e_redirect_cargo_shapes direct-and-transitive (cfg-if beside crc32fast) is refused and the untouched project still fetches `--locked`; before this change the same shape reported redirected: 1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Workspace member discovery checked the project root only lexically. A literal `members = ["linked"]` entry or a path dependency through a symlinked directory was returned as a rewrite target, and `scan --mode hosted` then wrote the registry pin into a Cargo.toml outside the project (the whole-run symlink guard checks only the file itself, not its parent directories). The same link matched by a `crates/*` glob was skipped instead, because the glob does not follow links. Discovery now returns only manifests reached without crossing a symbolic link: a symlinked member directory (literal or glob), a symlinked path dependency, a symlinked intermediate directory and a symlinked Cargo.toml are all left out. Cargo still reads those manifests, so a crate one of them depends on is refused by the Cargo.lock dependents check (redirect_cargo_transitive_dependents), exactly like a member or path dependency outside the root. Unit: symlinked literal/glob/nested members, a symlinked path dependency and manifest, and out-of-root members/path dependencies are not returned. In-process: an out-of-root path dependency and a glob or literal symlinked member each refuse the crate loudly and leave every file, the one outside the project included, untouched (the literal case reported redirected: 1 and wrote outside the project before). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A v1 Cargo.lock names every dependency by its full id, so repointing a crate also rewrites its dependents' references. Each dependent's whole `[[package]]` block was recorded as one edit. When one block referenced two patched packages (the root, for two cfg-if versions or any two patched crates it declares) the second edit's `original` was the first edit's `new`, and removing the first-applied purl alone found neither fragment: `remove`, scoped rollback and the hosted-to-vendored takeover refused as "drifted" unless purls went in exact reverse apply order. The dependents' references are now one `redirect_cargo_lock_reference` edit holding just the quoted full id `"name version (source)"`. It names that package exactly, so its inverse puts back every occurrence, independently of any other package's edits; the per-purl revert and the whole-ledger replay both handle it. The v1 whole-block edits were never released, so no existing ledger carries them. Unit: two cfg-if versions and two crates sharing a dependent block each remove in both orders byte-for-byte (both refused as drifted before); a full id named by two members replays clean. Real cargo: e2e_redirect_cargo_shapes now removes every multi-patch shape in apply order and in reverse, from the same post-scan state, and passes with SOCKET_PATCH_CARGO_E2E_LOCK_VERSION=1, 2 and 4. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The earlier bug-H fix removed the appended `[registries.…]` block with the shared fragment remover, which collapses the whole trailing newline run at EOF to one newline. That fixed a config ending in exactly one newline but lost a config's trailing blank line (restored byte-for-byte before that change) and still added a newline to a config without one. The block removal now inverts exactly what the rewriter appended: the recorded fragment plus the single blank separator before it. The newline a config without a final one needs rides in the recorded fragment, so that case is exact too; content the user appended after the block is kept; a config the rewrite created still ends empty and is deleted. `remove` and the whole-ledger replay share the helper (replay no longer leaves an emptied created config behind). All-CRLF configs are inverted as LF, and a fragment recorded with the other line endings still matches. Existing ledgers keep reverting as before. Unit: the bug-H revert test covers no final newline (LF and CRLF), trailing blank lines (LF and CRLF) and a whitespace-only config through both `remove` and the replay; helper pins for every append shape. Real cargo: e2e_redirect_cargo_shapes config-unterminated and config-trailing-blank restore the config byte-for-byte. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The CRLF support records a CRLF project's edit fragments with CRLF, and the ledger is JSON, so git line-ending normalization never touches them while it does rewrite the committed Cargo.toml / Cargo.lock / .cargo/config. A hosted redirect scanned on Windows (core.autocrlf) therefore could not be removed on a Linux checkout, nor an LF scan on a CRLF checkout: `remove`, rollback and the vendored takeover matched fragments byte-for-byte and refused as "drifted", and the suggested re-scan was a no-op that kept the same fragments. The cargo per-purl revert and the whole-ledger replay now match fragments regardless of CRLF/LF: an all-CRLF file is matched as LF and written back CRLF, any other file is tried with the recorded fragments and then with their LF forms. The file keeps its current line endings. The registry-block checks and removals use the same matching. Drift of the text itself still refuses exactly as before. Unit: a ledger recorded CRLF reverts LF files and a ledger recorded LF reverts CRLF files, each through `remove` and through the replay, with the ledger round-tripped through JSON (all four refused before). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Manifest edits are keyed by crate name, so the per-version revert told two redirected versions' edits apart by the registry each pin names. Ledgers an older CLI wrote with its name-only matcher break that rule: each version's run pinned BOTH declarations to its own registry, and a project it mis-pinned is later repaired by superseding that pin. On such a ledger `remove pkg:cargo/cfg-if@1.0.4` claimed half of a declaration's re-pin chain (or the superseded mis-pin) and refused as "drifted" — for exactly the users bug B had already hurt — and the other removal order dropped a still-pinned registry edit, leaving the created .cargo/config.toml behind. With another version of the crate still redirected, each manifest edit is now attributed to the version its declaration's requirement selects (the planner's own rule); a line without a readable requirement falls back to the registry it pins. A sibling version's registry is never claimed, and a still-referenced block keeps its ledger edit when another recorded wiring edit still pins to it, so the removal that retires that pin removes the block. A block kept for a hand pin still leaves the ledger, and the whole-ledger replay now keeps such a block too instead of deleting it from under the hand pin. Unit: a name-only two-version ledger and a repaired mis-pin ledger each remove in both orders back to the pristine project with no config and an empty ledger (removing 1.0.4 first refused before); the replay keeps a hand-pinned block. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The hosted flow appends a run's edits to the redirect ledger, skipping any edit already in it. The check also ran against the edits appended earlier in the same run, so identical edits from one rewrite collapsed into one. The cargo rewriter records one edit per occurrence, and a Cargo.toml that declares the crate with the same line in two sections (`[dependencies]` and `[dev-dependencies]`) yields two identical edits. With only one in the ledger, `remove` reverted one pin, kept the other and the registry block it references, and still reported success. Edits are now deduplicated only against the ledger as the run found it, so a re-run still records nothing twice. Real cargo: e2e_redirect_cargo_shapes two-sections (scan, fresh `--locked` build, VEX, byte-identical `remove`); it failed on the leftover pin before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CLI_CONTRACT.md still said the hosted rewriter reads only root candidate files, but cargo now also reads and pins workspace-member and in-root path-dependency manifests, which can show up in `rewrittenFiles`. The contract now covers that (members globs minus `exclude`, no symlinks, never `.socket/`), the new `redirect_cargo_transitive_dependents` refusal, the transitive-only and multi-version refusals, and CRLF handling across a checkout conversion. CHANGELOG [Unreleased] gains Fixed entries for this branch's user-visible hosted cargo changes, and docs/ecosystems.md notes that hosted cargo reaches direct dependencies only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A crate declared only with version requirements the patched version does not satisfy resolves those declarations to another version, so a pin cannot reach the locked crate. Hosted cargo now refuses it with redirect_cargo_toml_dep_unrewritable, the code the Socket backend uses for the same shape, instead of reporting it not found. Shared golden fixtures pin both refusals the backend and CLI must agree on: requirement-excludes-patched-version, path-dependency and transitive-dependents-git (a git crate also depending on the patched crate). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The hosted rewriter pins the patched crate in every manifest that declares it, workspace members included, but the guard that refuses to vendor on top of a live hosted redirect read the root Cargo.toml alone. A member-only pin was invisible, so with the redirect ledger gone vendor wired [patch.crates-io] beside a member still resolving the crate from the hosted registry — where [patch.crates-io] does not reach — and reported success. The guard now reads the root manifest and every member manifest the rewriter itself discovers, and the refusal names the file it found. Real cargo: mode_migration_cargo's takeover refusal gains the shape the guard exists for — the lock restored from version control while a table-form manifest pin survives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A v1 Cargo.lock written before cargo dropped [root] keeps the root package in that table, whose dependencies spell full package ids. The reference rewrite walked the [[package]] array only, so a redirected crate left [root] naming the crates.io id of a package the lock no longer held: cargo build --locked then fails, an unlocked build discards the lock and re-resolves, and the scan reported the crate redirected either way. The walk now covers that table too, recorded by the same every-occurrence reference edit, so remove still restores the lock byte for byte in any order. The vendored backend has read [root] since it learned v1 locks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One scan records one cargo edit per occurrence, so a manifest that declares the crate with the same line in two sections leaves two identical edits in the ledger, and the per-purl revert unwinds one occurrence per edit. The whole-ledger replay — the path a run whose patch-record fetch failed leaves behind, records empty — instead refused as soon as the fragment appeared more than once, and that refusal dropped every other cargo edit with it: the project stayed fully redirected, with a suggested remedy (re-run the scan) that cannot change a duplication the manifest itself carries. The replay now allows as many occurrences as there are identical edits left to spend on them, and still refuses a surplus no edit accounts for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Member discovery skipped cargo's build directory only while expanding a glob: a literal `members = ["target/gen"]` entry, or a path dependency pointing into target/, was returned and pinned — which the key predicate's own doc comment said could not happen. The next `cargo clean` deletes that manifest, and a recorded hosted edit whose file no longer exists refuses the rollback of every other cargo edit. A `target` segment is now excluded wherever a member path is accepted, in discovery and in the key predicate both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A `registry = …` pin reaches only the declarations it sits on, so a crate another package also pulls in is refused — but that check reads Cargo.lock, and a project without one was pinned unconditionally. Rust libraries routinely gitignore their lock, so the silent shape was the common one: the scan reported the crate redirected, vex attested it, and the next build resolved the patched copy for the root and an unpatched crates.io copy for whatever else depends on the crate. Without a lock the dependents question is now answered from the manifests instead: a crate declared beside any other dependency — or beside a workspace member this run could not read (a members glob, or a member outside the project or behind a symbolic link) — is refused `redirect_cargo_lockless_dependents`, whose detail names both remedies. A path dependency on a manifest the same run pins is not company, and a project whose only dependency is the patched crate has nothing that could pull it in and still redirects. Real cargo: e2e_redirect_cargo_shapes lockless-other-dependencies, the direct-and-transitive project with its lock deleted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
force-pushed
the
fix/cargo-hosted-matrix-bugs
branch
from
September 25, 2026 15:24
92ad228 to
2677e99
Compare
Mikola Lysenko (mikolalysenko)
merged commit Sep 25, 2026
44c629f
into
feat/cargo-vendor-manifest-patch
411 checks passed
Mikola Lysenko (mikolalysenko)
deleted the
fix/cargo-hosted-matrix-bugs
branch
September 25, 2026 16:22
Mikola Lysenko (mikolalysenko)
added a commit
that referenced
this pull request
Sep 25, 2026
* feat(vendor): wire cargo patches through Cargo.toml [patch]
Vendored cargo patches are now wired through a Socket-owned
[patch.crates-io] key in the workspace-root Cargo.toml instead of
.cargo/config, so the patch uuid is visible from the manifest alone.
Legacy config wiring is migrated on re-run, repair and uuid bumps.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(vendor): tag vendored cargo copies with +socket.<uuid>
A vendored cargo copy's own Cargo.toml version is now rewritten to
<version>+socket.<uuid> (existing build metadata is kept:
2.0.1+zstd.1.5.2.socket.<uuid>), and the detached Cargo.lock entry
records that tagged version, with every reference that spells the old
version rewritten, in lock formats v1-v4. This is the lock cargo itself
writes for the tagged copy, so the patch uuid of the copy that actually
builds can be read from Cargo.lock alone. The patched crate now sees
the tag in CARGO_PKG_VERSION.
A lock that cannot be kept consistent is refused before anything is
written (cargo_lock_untaggable). Revert restores the original lock byte
for byte. A uuid bump re-tags the copy and lock, and a re-run or repair
tags projects vendored before this change (cargo_version_tagged). VEX
discovery treats the tagged lock version as the primary identity signal.
The old-toolchain e2e tests prefer the rust:1.41-slim and rust:1.56-slim
docker images.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(vendor): harden tagged cargo copies after review
- Keep a recorded whole-tree inventory when repair tags a pre-tag copy
instead of re-baselining it over unverified bytes; the inventory check
accepts the copy's Cargo.toml only with exactly this uuid's tag
dropped, the cargo inventory carries forward for the same copy, and
repair refreshes it only from a member-verified rebuild.
- Select Cargo.lock entries by rank (this uuid's tagged copy, another
tagged copy, a registry entry, then an untagged sourceless one), so a
user's same-version path fork locked beside the copy is never
restored, retagged or counted as a multi-source conflict; the revert
spells the restored registry entry by its full id, exactly as cargo
writes it.
- GC keeps an entry whose lock tag is stale while the manifest still
wires its copy (cargo re-locks to it; the next re-run retags).
- Revert drops the tag the first build locked when no lock originals
were recorded (vendored before any Cargo.lock), so a hosted takeover
finds the crate again.
- Dry runs preview "would tag" and the cargo_lock_untaggable /
cargo_copy_untaggable refusals.
- lock_inventory lists a sourced Socket-tagged entry with no verifier.
- Docs: semver Version equality/ordering sees the tag; 1.56 is built in
CI docker; an untagged detached entry counts only beside an untagged
copy.
- Tests: fork-shape lock units (v1/v4, cargo's exact lock), vendor-flow
tests for revert/dry-run/untaggable copies, tag-aware inventory and
carry-forward units, VEX discovery copy-tag units, VEX e2e split and
ledger leg, and real-cargo cells for a transitive registry dependent,
the repair inventory refresh and a lockless vendor + hosted takeover.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(vendor): cover cargo tag rollbacks and 1.41 index
- A failed Cargo.lock retag write (read-only project root) untags the
copy again, through both the vendor re-run and repair's migration.
- repair's migration over a copy whose Cargo.toml cannot take the tag
reports cargo_version_untagged and writes nothing.
- The cargo 1.41 two-version leg now proves the documented remedy: a
populated crates.io index in $CARGO_HOME builds and runs both patched
copies with no network, instead of only accepting the failure.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(vendor): see every hosted pin shape before vendoring
The cross-mode guard that refuses to wire [patch.crates-io] on top of a
live hosted redirect read Cargo.toml with a single-line regex, so it saw
only `<crate> = { ... registry = "socket-patch-<uuid>" }`. The hosted
rewriter also writes a standalone `registry = ...` line under a
`[dependencies.<crate>]` header, pins renamed declarations by their
`package` key, and accepts a quoted key — all read as "not redirected".
With a Cargo.lock restored from version control (so the lock probe sees
crates.io again) and no redirect ledger, vendor then reported success on
a project pinned to the hosted registry and patched through
[patch.crates-io] at once: cargo resolves the crate from a non-crates.io
source, which [patch.crates-io] does not cover, so the build either
fails or silently links the unpatched copy.
The probe now reuses the rewriter's own manifest reader, so detection
and writing stay in step, and the refusal names the registry it found.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vendor): pin the cargo tag allowance to this patch
A cargo patch that edits the crate's own Cargo.toml verifies with the
Socket version tag dropped. The whole-tree inventory check drops only
THIS entry's tag; the per-file check dropped whatever Socket tag it
found, so a copy under <uuidA>'s path whose manifest is tagged for
<uuidB> — a hand edit, a merged vendored tree, a half-applied uuid bump
— verified clean and repair reported it healthy, while VEX discovery
refused the same bytes as dead wiring. Cargo builds it as the version
the tag names, which is not the version the detached lock pins.
Cargo entries record no file inventory, so this per-file path is the
live one for every vendored crate, not a legacy fallback. It now
applies the same uuid pin, with the four-arm test the inventory path
already had.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(vendor): assert the old-toolchain index constraint
The two-version old-toolchain test only printed whether a build without
--offline worked, and reached the documented 1.41 remedy (a populated
crates.io index in $CARGO_HOME) only when the plain --offline build
happened to fail. Both arms run with no network, so the unasserted run
could only fail, for a reason nothing checked — and a cargo that stopped
needing the index would have skipped the remedy and still passed.
Both directions are now asserted: the run without --offline must fail
on the unreachable crates.io index, and below 1.56 the empty-CARGO_HOME
--offline run must fail with the missing registry index before the
seeded index is required to build and run both patched copies.
The docs said 1.56 needs --offline; what 1.56 needs is --offline or a
reachable index, exactly like 1.41, which additionally needs the index
populated. Both the changelog and the ecosystem notes now say that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(purl): decode build-metadata versions before matching
The patches API serves canonical PURLs, so a semver build-metadata
version arrives percent-encoded — pkg:cargo/wasi@0.11.0%2Bwasi-snapshot-
preview1. The parsers compared that raw spelling against Cargo.lock's
0.11.0+wasi-snapshot-preview1, never matched, and refused the package
with vendor_fetched_missing and then locked_version_mismatch. It failed
closed, but no cargo crate with build metadata could be vendored at all,
and wasi is in most Rust dependency graphs.
Every ecosystem's parse now percent-decodes the namespace, name and
version exactly once, after the / and @ split and before the callers'
path-safety guards, so an escaped separator still cannot introduce a
path segment and an already-decoded %2B cannot become a second +. The
crawler and the ledger revert paths that decoded by hand, or fed an
already-canonical PURL back in, drop their extra pass.
Hosted mode already went through a decoding parse and is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(vendor): keep the Cargo.lock line endings
toml_edit renders LF only and drops the \r of every CRLF line, including
the ones no edit touched, so vendoring rewrote a lock committed with
Windows line endings as all-LF. vendor --revert then "restored" that
file, so the rollback differed from the original in every line ending
and was not byte-identical. The copy's Cargo.toml already mapped its
rendering back onto the original endings; the lock did not.
The lock edits — detach, retag and restore — now go through the same
reconciliation, in lock formats v1 through v4. A CRLF lock stays CRLF, a
missing trailing newline stays missing, and every line a mixed-ending
lock's edit leaves alone keeps its own ending. A line the edit rewrites,
removes or re-inserts still takes the file's dominant ending, because
its original one is not recoverable from the text, so a mixed lock is
promised an unchanged remainder rather than a byte-identical revert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(vendor): state the real cargo floor for two versions
Cargo before 1.56 resolves every source-less Cargo.lock entry for a crate
through ONE [patch.crates-io] path — the entry whose key sorts last — so
with two vendored versions one lock entry is pinned to the other
version's copy and cargo build --locked fails closed with "patch for
<crate> did not resolve to any crates". The docs claimed older cargo
needed only a populated crates.io index; the index does not help. The
old-toolchain e2e passed because its fixture uuids happened to sort so
that the lower version's key came last, the one order 1.41 can take.
Verified on the rust:1.41-slim and rust:1.56-slim images with two crates
(cfg-if 1.0.5/0.1.10, lazy_static 1.5.0/0.2.11) and with three versions
(bitflags 1.2.1/0.9.1/0.8.2): only the key sort order decides, the lock
entry order does not, and 1.56 builds either way.
The e2e fixture now uses uuids whose keys sort adversarially, asserts
that order so a future uuid cannot quietly make the test toothless, and
pins the refusal below 1.56 while requiring 1.56 to build both copies.
Vendoring a second version of a crate warns with
cargo_multi_version_old_cargo unless the project's rust-version or
rust-toolchain[.toml] promises cargo 1.56 or newer; socket-patch never
runs cargo, so those files are the only signal it has. A single vendored
version still builds on 1.41.
A key-naming scheme that puts the lowest version's key last does make
1.41 resolve every entry correctly, but it would replace the documented
<name>-socket-<uuid8> key with a version-rank encoding in every project's
committed manifest, and rest correctness on an undocumented cargo
iteration order. Left out deliberately; the failure is loud, not silent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(vendor): check the wasi oracle at compile time
The build-metadata e2e vendored `wasi` and then ran a consumer binary
calling the patched marker fns. `wasi` declares 46 `extern "C"` WASI
syscalls that only resolve on wasm targets; GNU and Apple linkers
dead-strip the never-called wrappers, but MSVC refuses the executable
(`LNK1120: 46 unresolved externals`), so the test failed on the
windows-latest leg. The fresh-checkout `cargo build` of the same binary
consumer would have failed identically.
Keep `wasi` as the fixture (it is the real-world build-metadata crate
the bug hit) and make the consumer a library whose oracle is
compile-time: the patch suffix exposes `SOCKET_PATCHED` and
`SOCKET_PKG_VERSION` constants and the consumer's `src/lib.rs` holds a
`const fn bytes_eq` plus two `const _: () = assert!(...)` items, so
`cargo build --locked --offline` of the library proves both that the
patched bytes are what cargo compiled and that `CARGO_PKG_VERSION`
carries the exact `<version>.socket.<uuid>` tag, without linking
anything on any OS. The byte-identical revert leg and the zero-downloads
assertion are unchanged.
Negative controls, run by hand against the vendored tree: removing the
suffix from the copy fails with E0425 (`SOCKET_PATCHED` not found in
`wasi`); stripping the tag from the copy's manifest and lock entry fails
with E0080 (the const assertion). Verified under cargo 1.93.1 and
1.83.0 and all four lock formats.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(vex): label cargo lock shapes in assert messages
CodeQL's default setup raised 14 new `rust/cleartext-logging` alerts
on this branch, all in `assert_attested`'s panic messages. The SARIF
code flows start at the `uuid` reads inside `cargo_tag::tag_version`'s
`format!` (CodeQL's name heuristics classify any `uid`/`uuid`-named
identifier as sensitive), pass through the test's `tagged_version(U)`
wrapper into the `copy`/`locked` strings of
`cargo_vendored_a_attests_in_every_lock_version`, and end in the `what`
label interpolated into every assertion message of the helper.
A Socket patch uuid is a public identifier, so the alerts are false
positives, but they gate the PR and no CodeQL config file exists to
exclude tests. Break the path where the value entered the messages: the
loop now labels its two cases `tagged` / `untagged` instead of
embedding the tagged version strings, and `assert_attested` documents
that `what` is a shape label so the next test does not reintroduce the
flow.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Fix ten hosted cargo redirect bugs found by a real-cargo matrix (#255)
* fix(redirect): pin and revert each cargo version separately
A project that locks two versions of one crate (cfg-if 1.0.4 beside a
renamed cfg-if-legacy at 0.1.10) had every same-named declaration pinned
to the last patch's registry: the manifest planner matched the crate name
only, so `^0.1.10` pointed at a registry that serves 1.0.4 and
`cargo fetch --locked` failed. `get <uuid>` for one version broke it the
same way.
The planner now pins a declaration only when its version requirement
selects the patched version. A requirement that also matches another
locked version refuses the dep (nothing written); `workspace = true`
inheritors of an entry naming another version are skipped.
`remove` had the matching defect: manifest edits are keyed by crate name,
so removing one version also reverted the other version's pin (leaving its
lock entry hosted) and dropped its registry edit from the ledger, so the
created `.cargo/config.toml` survived the second removal. Manifest edits
whose pin names a sibling version's registry lineage are no longer claimed.
Golden: cargo/cargo/multi-version (the depscan TS twin lags; list it in
TS_LAGGING on the next submodule bump). Real-cargo regression:
e2e_redirect_cargo_shapes multi-version (fresh `cargo fetch --locked` +
offline build links both patched copies; removing both purls restores
every byte).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(redirect): restore a cargo config's bytes on hosted remove
`remove` of a hosted cargo patch deleted the `[registries.socket-patch-…]`
block it had appended to an existing config but kept the blank separator
line it inserted before it, so a legacy `.cargo/config` holding
`[net]\nretry = 2\n` came back as `[net]\nretry = 2\n\n`. It also collapsed
every run of three newlines anywhere in the file, rewriting the user's own
spacing.
The block now leaves through the replay path's fragment removal, which
takes the block's own separator and nothing else.
Real-cargo regression: e2e_redirect_cargo_shapes legacy-config.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(redirect): pin cargo workspace members' own declarations
Hosted mode pinned only the root Cargo.toml. In a workspace whose member
declares the patched crate itself (`cfg-if = "1.0.4"` beside siblings
that inherit `[workspace.dependencies]`), the member stayed on crates.io
while Cargo.lock was repointed at the patch registry, so
`cargo fetch --locked` failed — and the dep was still reported
redirected and attested.
`scan`/`get --mode hosted` now read every workspace member manifest
(`[workspace] members` globs minus `exclude`, plus in-root path
dependencies) and the rewriter plans them all in the dep's single
transaction: each direct declaration is pinned, `workspace = true`
inheritors resolve against the root's entry, and a member that cannot be
pinned refuses the dep everywhere. `remove` already reverts edits by path;
its registry-block reference probe now covers member manifests too.
Golden: cargo/cargo/workspace-member (the depscan TS twin is handed no
member manifests and lags; list it in TS_LAGGING on the next submodule
bump). Real-cargo regression: e2e_redirect_cargo_shapes
workspace-direct-member.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(redirect): attest every cargo shape post-install
The real-cargo shape suite now also runs `socket-patch vex` over each
fresh checkout (ledger kept, patch server admitted) and requires exactly
the shape's patches to be attested — multi-version and workspace-member
redirects included.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(redirect): redirect CRLF cargo projects in hosted mode
Every hosted cargo planner matched LF text, so a Windows checkout with
CRLF Cargo.toml / Cargo.lock was refused outright
(redirect_cargo_toml_dep_unrewritable / redirect_cargo_lock_pkg_not_found).
A manifest, lock or config whose every line break is CRLF is now planned
as LF and written back as CRLF, and its recorded edit fragments are
stored CRLF so `remove` and rollback find them in the file. Files with
mixed endings are left alone and keep refusing where the grammar does not
match. The shared fragment remover (used for the appended registry block)
inverts CRLF files as LF so it no longer strands a `\r` line.
Golden: cargo/cargo/crlf (CRLF manifest, lock and legacy config; the
depscan TS twin lags — list it in TS_LAGGING on the next submodule bump).
Real-cargo regression: e2e_redirect_cargo_shapes crlf (endings kept,
fresh `cargo fetch --locked` + offline build, byte-identical remove).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(redirect): say why a transitive-only crate is not redirected
A hosted cargo redirect pins a crate with `registry = "…"` on its
manifest declaration, which reaches only that declaration: a crate the
project gets purely through another dependency cannot be redirected in
hosted mode. That stays a refusal — nothing is written, recorded or
attested, and a requested VEX fails the run — but the warning said only
"no [dependencies] entry for X in Cargo.toml", which reads like a
discovery bug.
When Cargo.lock resolves the crate at the patched version the warning
now says it is a transitive-only dependency, that it was NOT redirected
and stays unpatched, and that `scan --mode vendored` (whose
`[patch.crates-io]` covers the whole graph) or a direct declaration can
patch it. The code (`redirect_cargo_toml_dep_not_found`) is unchanged.
Golden: cargo/cargo/transitive-refusal (pins the refusal and its warning
code). In-process: the refusal writes nothing, is not attested, and the
envelope carries the transitive-only warning.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(redirect): run the cargo shape suite in every lock format
The multi-version, workspace-member, legacy-config and CRLF shapes ran
only against the toolchain's own v4 lock, so nothing in-repo checked
them against a committed v1 lock (full-id dependency edges, checksums
in `[metadata]`) or v2/v3.
The shape suite now honours SOCKET_PATCH_CARGO_E2E_LOCK_VERSION like
the other real-cargo suites and joins the cargo-vex-matrix CI job. The
shared lock re-encoder kept only dependency names, which cannot tell
two locked versions of one crate apart; it now keeps each edge's
package id and writes the shortest form cargo writes for that format
(`name`, `name version`, or the full id; always the full id in v1).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(redirect): refuse a cargo crate other crates depend on
A hosted cargo redirect pins a crate with `registry = "…"` on each
manifest declaration and repoints its one Cargo.lock entry. When a
crates.io (or git) crate in the lock also depends on it — cfg-if, libc
and serde usually are both direct and transitive — that edge keeps
resolving from crates.io. The repointed lock then fails
`cargo fetch --locked`, a plain build adds the crates.io copy back
beside the patched one, and the dependent crate compiles the unpatched
code, while scan reported the crate redirected and VEX attested it.
The same happened for a path package whose manifest the rewriter never
saw (outside the project root, or behind a symlink).
Before committing a dep, every Cargo.lock package that depends on it
(any edge spelling: `name`, `name version`, or the full v1 id) must be
a source-less package whose manifest was planned and pinned. Otherwise
the dep is skipped with the new additive warning
`redirect_cargo_transitive_dependents`, which names the dependents,
says the crate was NOT redirected and stays unpatched, and points to
`scan --mode vendored`. Nothing is written, recorded or attested.
Golden: cargo/cargo/shared-dependency (the depscan TS twin lags — list
it in TS_LAGGING on the next submodule bump). Unit: registry, git, v1
full-id and unplanned-path dependents refuse; another version's edge
and a planned member do not. Real cargo: e2e_redirect_cargo_shapes
direct-and-transitive (cfg-if beside crc32fast) is refused and the
untouched project still fetches `--locked`; before this change the
same shape reported redirected: 1.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(redirect): never follow a symlink to a cargo member
Workspace member discovery checked the project root only lexically. A
literal `members = ["linked"]` entry or a path dependency through a
symlinked directory was returned as a rewrite target, and `scan --mode
hosted` then wrote the registry pin into a Cargo.toml outside the
project (the whole-run symlink guard checks only the file itself, not
its parent directories). The same link matched by a `crates/*` glob
was skipped instead, because the glob does not follow links.
Discovery now returns only manifests reached without crossing a
symbolic link: a symlinked member directory (literal or glob), a
symlinked path dependency, a symlinked intermediate directory and a
symlinked Cargo.toml are all left out. Cargo still reads those
manifests, so a crate one of them depends on is refused by the
Cargo.lock dependents check (redirect_cargo_transitive_dependents),
exactly like a member or path dependency outside the root.
Unit: symlinked literal/glob/nested members, a symlinked path
dependency and manifest, and out-of-root members/path dependencies are
not returned. In-process: an out-of-root path dependency and a glob or
literal symlinked member each refuse the crate loudly and leave every
file, the one outside the project included, untouched (the literal
case reported redirected: 1 and wrote outside the project before).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(redirect): remove v1-lock cargo patches in any order
A v1 Cargo.lock names every dependency by its full id, so repointing a
crate also rewrites its dependents' references. Each dependent's whole
`[[package]]` block was recorded as one edit. When one block referenced
two patched packages (the root, for two cfg-if versions or any two
patched crates it declares) the second edit's `original` was the first
edit's `new`, and removing the first-applied purl alone found neither
fragment: `remove`, scoped rollback and the hosted-to-vendored takeover
refused as "drifted" unless purls went in exact reverse apply order.
The dependents' references are now one `redirect_cargo_lock_reference`
edit holding just the quoted full id `"name version (source)"`. It
names that package exactly, so its inverse puts back every occurrence,
independently of any other package's edits; the per-purl revert and the
whole-ledger replay both handle it. The v1 whole-block edits were never
released, so no existing ledger carries them.
Unit: two cfg-if versions and two crates sharing a dependent block each
remove in both orders byte-for-byte (both refused as drifted before); a
full id named by two members replays clean. Real cargo:
e2e_redirect_cargo_shapes now removes every multi-patch shape in apply
order and in reverse, from the same post-scan state, and passes with
SOCKET_PATCH_CARGO_E2E_LOCK_VERSION=1, 2 and 4.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(redirect): restore a cargo config's exact trailing bytes
The earlier bug-H fix removed the appended `[registries.…]` block with
the shared fragment remover, which collapses the whole trailing newline
run at EOF to one newline. That fixed a config ending in exactly one
newline but lost a config's trailing blank line (restored byte-for-byte
before that change) and still added a newline to a config without one.
The block removal now inverts exactly what the rewriter appended: the
recorded fragment plus the single blank separator before it. The
newline a config without a final one needs rides in the recorded
fragment, so that case is exact too; content the user appended after
the block is kept; a config the rewrite created still ends empty and is
deleted. `remove` and the whole-ledger replay share the helper (replay
no longer leaves an emptied created config behind). All-CRLF configs
are inverted as LF, and a fragment recorded with the other line endings
still matches. Existing ledgers keep reverting as before.
Unit: the bug-H revert test covers no final newline (LF and CRLF),
trailing blank lines (LF and CRLF) and a whitespace-only config through
both `remove` and the replay; helper pins for every append shape. Real
cargo: e2e_redirect_cargo_shapes config-unterminated and
config-trailing-blank restore the config byte-for-byte.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(redirect): remove cargo patches across line-ending changes
The CRLF support records a CRLF project's edit fragments with CRLF, and
the ledger is JSON, so git line-ending normalization never touches
them while it does rewrite the committed Cargo.toml / Cargo.lock /
.cargo/config. A hosted redirect scanned on Windows (core.autocrlf)
therefore could not be removed on a Linux checkout, nor an LF scan on
a CRLF checkout: `remove`, rollback and the vendored takeover matched
fragments byte-for-byte and refused as "drifted", and the suggested
re-scan was a no-op that kept the same fragments.
The cargo per-purl revert and the whole-ledger replay now match
fragments regardless of CRLF/LF: an all-CRLF file is matched as LF and
written back CRLF, any other file is tried with the recorded fragments
and then with their LF forms. The file keeps its current line endings.
The registry-block checks and removals use the same matching. Drift
of the text itself still refuses exactly as before.
Unit: a ledger recorded CRLF reverts LF files and a ledger recorded LF
reverts CRLF files, each through `remove` and through the replay, with
the ledger round-tripped through JSON (all four refused before).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(redirect): remove each cargo version from older ledgers
Manifest edits are keyed by crate name, so the per-version revert told
two redirected versions' edits apart by the registry each pin names.
Ledgers an older CLI wrote with its name-only matcher break that rule:
each version's run pinned BOTH declarations to its own registry, and a
project it mis-pinned is later repaired by superseding that pin. On
such a ledger `remove pkg:cargo/cfg-if@1.0.4` claimed half of a
declaration's re-pin chain (or the superseded mis-pin) and refused as
"drifted" — for exactly the users bug B had already hurt — and the
other removal order dropped a still-pinned registry edit, leaving the
created .cargo/config.toml behind.
With another version of the crate still redirected, each manifest edit
is now attributed to the version its declaration's requirement selects
(the planner's own rule); a line without a readable requirement falls
back to the registry it pins. A sibling version's registry is never
claimed, and a still-referenced block keeps its ledger edit when
another recorded wiring edit still pins to it, so the removal that
retires that pin removes the block. A block kept for a hand pin still
leaves the ledger, and the whole-ledger replay now keeps such a block
too instead of deleting it from under the hand pin.
Unit: a name-only two-version ledger and a repaired mis-pin ledger each
remove in both orders back to the pristine project with no config and
an empty ledger (removing 1.0.4 first refused before); the replay keeps
a hand-pinned block.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(redirect): keep identical hosted edits from one scan
The hosted flow appends a run's edits to the redirect ledger, skipping
any edit already in it. The check also ran against the edits appended
earlier in the same run, so identical edits from one rewrite collapsed
into one. The cargo rewriter records one edit per occurrence, and a
Cargo.toml that declares the crate with the same line in two sections
(`[dependencies]` and `[dev-dependencies]`) yields two identical edits.
With only one in the ledger, `remove` reverted one pin, kept the other
and the registry block it references, and still reported success.
Edits are now deduplicated only against the ledger as the run found it,
so a re-run still records nothing twice.
Real cargo: e2e_redirect_cargo_shapes two-sections (scan, fresh
`--locked` build, VEX, byte-identical `remove`); it failed on the
leftover pin before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* docs: describe the hosted cargo redirect fixes
CLI_CONTRACT.md still said the hosted rewriter reads only root candidate
files, but cargo now also reads and pins workspace-member and in-root
path-dependency manifests, which can show up in `rewrittenFiles`. The
contract now covers that (members globs minus `exclude`, no symlinks,
never `.socket/`), the new `redirect_cargo_transitive_dependents`
refusal, the transitive-only and multi-version refusals, and CRLF
handling across a checkout conversion.
CHANGELOG [Unreleased] gains Fixed entries for this branch's
user-visible hosted cargo changes, and docs/ecosystems.md notes that
hosted cargo reaches direct dependencies only.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(redirect): refuse cargo requirements the patch misses
A crate declared only with version requirements the patched version
does not satisfy resolves those declarations to another version, so a
pin cannot reach the locked crate. Hosted cargo now refuses it with
redirect_cargo_toml_dep_unrewritable, the code the Socket backend
uses for the same shape, instead of reporting it not found.
Shared golden fixtures pin both refusals the backend and CLI must
agree on: requirement-excludes-patched-version, path-dependency and
transitive-dependents-git (a git crate also depending on the patched
crate).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(vendor): see a hosted pin in a workspace member
The hosted rewriter pins the patched crate in every manifest that
declares it, workspace members included, but the guard that refuses to
vendor on top of a live hosted redirect read the root Cargo.toml alone.
A member-only pin was invisible, so with the redirect ledger gone
vendor wired [patch.crates-io] beside a member still resolving the
crate from the hosted registry — where [patch.crates-io] does not
reach — and reported success.
The guard now reads the root manifest and every member manifest the
rewriter itself discovers, and the refusal names the file it found.
Real cargo: mode_migration_cargo's takeover refusal gains the shape
the guard exists for — the lock restored from version control while a
table-form manifest pin survives.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(redirect): repoint a v1 lock's [root] table
A v1 Cargo.lock written before cargo dropped [root] keeps the root
package in that table, whose dependencies spell full package ids. The
reference rewrite walked the [[package]] array only, so a redirected
crate left [root] naming the crates.io id of a package the lock no
longer held: cargo build --locked then fails, an unlocked build
discards the lock and re-resolves, and the scan reported the crate
redirected either way.
The walk now covers that table too, recorded by the same
every-occurrence reference edit, so remove still restores the lock byte
for byte in any order. The vendored backend has read [root] since it
learned v1 locks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(redirect): spend one cargo edit per occurrence
One scan records one cargo edit per occurrence, so a manifest that
declares the crate with the same line in two sections leaves two
identical edits in the ledger, and the per-purl revert unwinds one
occurrence per edit. The whole-ledger replay — the path a run whose
patch-record fetch failed leaves behind, records empty — instead
refused as soon as the fragment appeared more than once, and that
refusal dropped every other cargo edit with it: the project stayed
fully redirected, with a suggested remedy (re-run the scan) that
cannot change a duplication the manifest itself carries.
The replay now allows as many occurrences as there are identical edits
left to spend on them, and still refuses a surplus no edit accounts
for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(redirect): never pin a manifest under target/
Member discovery skipped cargo's build directory only while expanding
a glob: a literal `members = ["target/gen"]` entry, or a path
dependency pointing into target/, was returned and pinned — which the
key predicate's own doc comment said could not happen. The next
`cargo clean` deletes that manifest, and a recorded hosted edit whose
file no longer exists refuses the rollback of every other cargo edit.
A `target` segment is now excluded wherever a member path is accepted,
in discovery and in the key predicate both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(redirect): refuse a lockless cargo pin
A `registry = …` pin reaches only the declarations it sits on, so a
crate another package also pulls in is refused — but that check reads
Cargo.lock, and a project without one was pinned unconditionally. Rust
libraries routinely gitignore their lock, so the silent shape was the
common one: the scan reported the crate redirected, vex attested it,
and the next build resolved the patched copy for the root and an
unpatched crates.io copy for whatever else depends on the crate.
Without a lock the dependents question is now answered from the
manifests instead: a crate declared beside any other dependency — or
beside a workspace member this run could not read (a members glob, or
a member outside the project or behind a symbolic link) — is refused
`redirect_cargo_lockless_dependents`, whose detail names both remedies.
A path dependency on a manifest the same run pins is not company, and
a project whose only dependency is the patched crate has nothing that
could pull it in and still redirects.
Real cargo: e2e_redirect_cargo_shapes lockless-other-dependencies, the
direct-and-transitive project with its lock deleted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Branch
fix/cargo-hosted-matrix-bugs, stacked on #254 (commits are keyed by subject below; shas move on each restack) (feat/cargo-vendor-manifest-patch). 21 signed commits, 73 files, +5665/−168 — 58 of those files are new shared golden fixtures undertests/fixtures/redirect/cargo/.Summary
Ten defects in hosted cargo redirects, every one found by running the real CLI against real cargo rather than by reading code. A 1,232-cell matrix (cargo 1.41 → stable, lock formats v1–v4, 22 project shapes, 6 modes) drove the discovery; each fix carries a real-cargo regression test and was reverted in turn to confirm the test fails without it.
pin and revert each cargo version separatelyplan_cargo_tomlmatched a crate by name without its version, so every same-named declaration was pinned to the last patch's registry. Each version is now pinned and reverted separately.repoint a v1 lock's [root] table[root]table kept full-id references on crates.io after the rewrite.never pin a manifest under target/target/, and followed symlinks out of the project — a symlinked member could be written to outside the repo.spend one cargo edit per occurrenceremovethen reported success and left a pin and a[registries]block behind.see a hosted pin in a workspace memberrestore a cargo config's exact trailing bytesremoveappended a blank line to an existing legacy.cargo/config, and left a config it had created behind. Reverts are now byte-exact, including files with no trailing newline.remove cargo patches across line-ending changesremove/rollback failed.remove each cargo version from older ledgersrefuse cargo requirements the patch missesnot_found; it is nowredirect_cargo_toml_dep_unrewritable, naming each requirement and its manifest.refuse a lockless cargo pinThe one behaviour change to weigh
registry = "socket-patch-<uuid>"only reaches the declarations it sits on. If another package in the graph also depends on the patched crate, that package keeps resolving it from crates.io — so the build gets a patched copy for the root and an unpatched copy for everyone else, while the scan reports the crate redirected andvexattests it. That shape is now refused withredirect_cargo_transitive_dependents, pointing at--mode vendored.Answering that question needs
Cargo.lock. A project without one was previously pinned unconditionally, so the silently-wrong case was the common one — and Rust libraries routinely gitignore their lock. With no lock,refuse a lockless cargo pinanswers it from the manifests instead: a crate declared beside any other dependency, or beside a workspace member this run could not read, is refusedredirect_cargo_lockless_dependents.This turns a previously-succeeding common shape into a loud skip. It is narrower than it sounds — a lockless project whose only dependency is the patched crate still redirects, and a path dependency on a manifest the same run pins does not count as company — but it is the change most likely to be noticed, and it is the one I would most like a second opinion on. The alternative is to keep silently half-patching those projects.
Cross-repo parity
redirect_cargo_transitive_dependentsandredirect_cargo_toml_dep_unrewritableare the shared names; depscan's TypeScript twin adopts both in SocketDev/depscan#SERVERPR. Four new shared golden cases (requirement-excludes-patched-version,transitive-dependents-git,path-dependency,lockless-dependents) plusshared-dependencypin the contract for both implementations.Testing
cargo test -p socket-patch-core4922 pass,-p socket-patch-cli4002 pass, clippy--workspace --all-features --all-targets -D warningsclean. Real-cargo suites across lock formats v1–v4 includinge2e_redirect_cargo_shapes8/8 each, plus the docker 1.41/1.56 legs 14/14.One trap worth recording:
cargo test -p socket-patch-cli --test <name>does not re-upliftdebug/socket-patch, so two worktrees sharing aCARGO_TARGET_DIRwill silently run each other's binary —e2e_redirect_cargo_shapesreported 0/8 four times before that was spotted. The suites now assert the binary's identity before running.Note
High Risk
Changes hosted cargo lockfile/manifest rewriting and introduces new skip/refusal paths (including lockless projects with multiple deps), which can break workflows that previously appeared to succeed while building mixed patched/unpatched graphs.
Overview
Hosted cargo redirects are reworked so pins stay consistent with what
cargo --lockedactually resolves: workspace-member and in-root pathCargo.tomlfiles are included in the same transaction as the root, semver-aware pinning applies per declaration when multiple versions of a crate are locked, and v1Cargo.lockhandling repoints[root]plus dependent full-ids via a dedicatedredirect_cargo_lock_referenceedit (soremoveworks in any order).Fail-closed refusals replace cases that previously reported success while leaving unpatched copies in the graph:
redirect_cargo_transitive_dependentswhen another lock package also depends on the crate,redirect_cargo_lockless_dependentswhen there is noCargo.lockbut other dependencies (or unread workspace members) might pull the crate in, and clearerredirect_cargo_toml_dep_unrewritablewhen requirements do not match the patched version. CRLF cargo files are rewritten with endings preserved; ledger recording no longer collapses duplicate manifest edits from two dependency sections, and appended.cargo/configfragments revert byte-for-byte.Adds
cargo_workspace::member_manifests, wires it intoscan --mode hosted, extends CI withe2e_redirect_cargo_shapes, and documents the contract in CHANGELOG / CLI_CONTRACT.Reviewed by Cursor Bugbot for commit 92ad228. Configure here.