Skip to content

v5 WS1+WS2: ledger-free hosted mode, upstream-restore rollback, vendor ejects hosted projects - #280

Merged
Mikola Lysenko (mikolalysenko) merged 66 commits into
release/v5-prereleasefrom
v5/ledger-free-hosted
Sep 28, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 66 commits into
release/v5-prereleasefrom
v5/ledger-free-hosted

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Implements WS1 and WS2 of docs/design/v5-plan.md. Draft.

WS1: ledger-free hosted mode

  • scan/get --mode hosted no longer write .socket/vendor/redirect-state.json. Hosted runs write only lockfile edits.
  • New core module patch::redirect::upstream: for each hosted pin found in the lockfiles (vex::discover refs: purl, uuid, file), it restores the default upstream registry entry by re-resolving the registry artifact. When it can't (fields it can't derive, offline runs, non-PyPI Pipenv indexes, and so on), it refuses and tells the user to run git checkout -- <lockfile>. It never replays recorded fragments.
  • rollback, remove and the vendor takeover route their hosted step through this module.
  • A legacy ledger is read only for migration and is never written.

WS2: vendor ejects hosted projects

  • When there is no manifest, vendor takes the patch set from the lockfile's hosted pins, restores upstream first, vendors those patches into .socket/vendor/, and rewires the lockfiles from hosted to vendored. vendor --revert goes back to upstream.
  • Binary bun.lockb hosted pins are rebuilt natively for the takeover. Format-1 locks are demoted back to format 1 exactly, and workspace-normalized locks are refused. rollback/remove keep refusing hosted bun.lockb pins with the git checkout -- bun.lockb remedy.

Status

  • scan stops writing the ledger
  • upstream restore for npm/yarn/pnpm/bun (text and bun.lockb for vendor), vlt, cargo, golang, composer, gem, maven, nuget and PyPI (requirements, uv, pylock, Poetry 1.0-2.x, PDM, Pipenv), or a documented refusal
  • hosted round-trip tests rewritten to the new contract, not deleted
  • WS2 vendor eject
  • CLI_CONTRACT.md / README.md / CHANGELOG.md [Unreleased]
  • owner sign-off: a hosted Pipfile.lock now keeps Pipenv's own index (comment 5862392841)

CI

Everything is green except two checks that also fail on release/v5-prerelease: Windows covgap_commands_scan_mod, and vlt install-proof (vlt_pinned_matrix_agent_get_and_remove). Details in comment 5864860663.

🤖 Generated with Claude Code

https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ


Note

High Risk
Major semver release that rewrites hosted rollback/remove/vendor behavior and multi-ecosystem lockfiles with registry-dependent upstream restore, so incorrect restore logic could leave projects in a bad install state.

Overview
v5 breaking change: hosted scan/get no longer write .socket/vendor/redirect-state.json — only lockfile and registry-config edits persist, and pre-v5 ledgers are ignored for planning and retired on full rollback.

Rollback, remove, and vendored takeover now discover hosted pins from lockfiles and restore each to a re-resolved upstream registry entry via core patch::redirect::upstream (replacing ledger replay and hosted_revert_unsupported). Refusals are fail-closed with git checkout -- <files> guidance; binary hosted bun.lockb stays refused on rollback/remove while vendor eject can rebuild it.

list, vex, and scan discovery treat hosted state as lockfile wiring (details.lockfiles, slimmer redirectState); legacy redirect ledgers are read-only for extra record detail or warnings, not for unwind.

vendor without a manifest ejects hosted projects: fetch records, upstream-restore, then vendor — all-or-nothing with rollback on failure. get/vendored flows treat hosted pins via lockfile discovery (patch_server_url); vendor_supersedes_redirect is removed.

Docs (README, CHANGELOG, CLI_CONTRACT), Windows CI timeout (50m), and -text gitattributes for Poetry/Pipenv fixtures align with byte-exact lock tests.

Reviewed by Cursor Bugbot for commit fc9a52a. Configure here.

Hosted scan keeps its edits and records in memory only; the lockfiles
are the record of a redirect. Replays the parked WIP (which was
snapshotted on an older tree) as just its hosted.rs delta.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…laying the ledger

Imports the stopped local WS1 agent's work-in-progress (backup/local-v5-
ledger-free-hosted): core patch::redirect::upstream re-resolves npm-family,
cargo and golang registry entries for every hosted pin vex::discover finds
in the lockfiles, and rollback/remove/vendor route their hosted legs
through it instead of the redirect ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
v5 keeps no hosted ledger, so every reader that consulted
.socket/vendor/redirect-state.json now reads the hosted pins lockfile
discovery finds:

- list shows each hosted pin with the lockfiles wiring it (details.lockfiles);
  a pre-v5 ledger only supplies the details of pins it still describes.
- scan's updates[] fold, redirectState block and the agent-flow
  hosted_wiring_retained probe read the pins.
- The hosted-over-vendored takeover classifier reads the pins; the
  vendored-over-hosted ledger reconcile (vendor_supersedes_redirect) is
  gone: once the lock routes a package to .socket/vendor/ no hosted state
  is left to go stale.
- vex treats a malformed pre-v5 redirect ledger as an advisory.

scan/mod.rs unit tests still need rewriting (WIP).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
… scan fold

- get's vendored lock-text gates read the lockfiles' hosted pins instead of
  the redirect ledger (DownloadParams carries --patch-server-url for it).
- repair's hosted-only no-op fires for hosted lockfile pins (or a pre-v5
  ledger).
- The in-memory hosted engine neither reads nor emits
  .socket/vendor/redirect-state.json.
- Disk hosted scan no longer folds edits into a throwaway ledger; its
  records feed only the stale-install probes and in-run VEX.
- The cargo vendor backend's hosted_redirect_live refusal names rollback /
  git checkout instead of a ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
…s upstream first

WS2: standalone `vendor` with no manifest now takes its patch set from the
lockfiles' hosted pins (purl + the uuid in each hosted URL), fetches each
record from the API, vendors it into .socket/vendor/ through the same step
`scan --mode vendored` and `get --mode vendored` use, and rewires the lock
from hosted to vendored. Without hosted pins it keeps the no-manifest
no-op.

The vendor takeover now restores the upstream registry entry before
vendoring for every ecosystem, not only cargo/npm/golang, so the vendor
ledger always records the upstream entry as its original and
`vendor --revert` returns to upstream rather than to hosted. A pin whose
upstream entry cannot be restored is refused with the checkout remedy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
…tate

v5 hosted mode keeps no ledger, so the scan/mod.rs unit tests now make a
purl "hosted" by writing a lockfile that pins the hosted URL instead of
planting .socket/vendor/redirect-state.json:

- overlap / classify_overlap_takeover tests use hosted package-lock,
  yarn (classic + berry), bun and cargo sparse-index pins; the
  non-default-host test configures --patch-server-url and pins that an
  unconfigured host is no pin.
- New behavior pinned: a pre-v5 ledger on disk is never hosted state;
  a lock routed to vendored (npm or cargo) yields no pin and no overlap;
  an edits-only state names no package; a half-migrated project whose
  locks name both sides stays silent; the redirectState block has no
  ledger/ledgerKey fields.
- hosted_wiring_retained_purls / redirect_state_json tests read the
  lockfile-derived state, keeping the probe's own liveness gate covered.
- Removed tests of retired behavior: note_vendor_supersedes_redirect
  reconcile (wet/npmrc/dry-run/no-op/persist-failure), the edits-only
  fallback (degraded ledger, vlt tilde keys) and following the vendored
  remediation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
Add the RubyGems and Composer restorers to the v5 ledger-free hosted
unwind (`redirect::upstream`).

Gem (Gemfile.lock / gems.locked + Gemfile / gems.rb): a converged Socket
GEM section is removed and its spec moved back, in name order, into the
upstream section (the single remaining one, else the manifest's global
source or rubygems.org, else refused as ambiguous); a bundler <= 2.1
merged section loses only the Socket remote; the DEPENDENCIES `!` pin is
dropped; CHECKSUMS is re-pinned from the rubygems.org compact index. The
Gemfile source block becomes `gem "n", "v"[, opts]` again (the original
constraint is not derivable), or is removed with its DEPENDENCIES entry
only when provably a transitive append. The pre-2.6 mixed state is undone
when a pin is supplied, keeping the untouched lock's own constraint.

Composer: dist {type,url,reference,shasum} and the dropped source block
are rebuilt from packagist p2 metadata (composer/2.0 minified, expanded),
cross-checked against the lock's dist.reference, in the lock's indent,
slash style and line endings. Non-packagist entries are refused.

UpstreamClient gains cached rubygems and packagist lookups
(SOCKET_RUBYGEMS_URL, SOCKET_PACKAGIST_URL).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
Add the Python family to the v5 hosted -> upstream restore: Pipfile.lock,
requirements.txt, Hatch direct references (pyproject.toml / hatch.toml),
poetry.lock, pdm.lock, and uv.lock / PEP 723 script locks / PEP 751 pylock
files with their paired pyproject / script metadata.

Each restorer rewrites only entries whose reference is a hosted URL for an
in-scope patch uuid and re-derives what the hosted rewrite overwrote from
PyPI's JSON API (UpstreamClient::pypi_files, base overridable with
SOCKET_PYPI_JSON_API, cached like the other lookups). Where a field is not
derivable the pin is refused instead of guessed: requirements hash-checking
mode that no other line settles, a Pipfile.lock index that is not PyPI,
PDM locks without cross_platform (or uv locks) when the release ships
platform- or interpreter-specific wheels, uv locks with no sibling
registry package to show the artifact shape, several or non-PyPI
registries, exclude-newer / no-binary / no-build filtering, multi-clause
uv specifiers with no spelling evidence, uv 0.2 [[distribution]] locks,
offline runs and registry failures.

The golden harness round-trips the native Poetry (1.0-2.4), PDM (every
supported lock_version) and Pipenv fixtures plus synthetic
requirements/Hatch/uv/pylock projects through the real hosted rewriter;
the shared requirements golden restores modulo the grant's name casing and
the uv golden (no registry sibling) is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
vlt-lock.json: slot [2] from npm dist.integrity, slot [3] per the lock's
own convention (same-era default-registry siblings, else DepID era and
options.registries), every hosted instance of name@version together.
Maven (no network): base versions back, added dependencyManagement
entries, socket-patch repositories and emptied wrappers removed,
trusted-checksum lines dropped and .mvn files deleted only when nothing
but hosted content is left; module poms and stray suffix uses refuse.
NuGet: socket-patch source and mapping removed, a mapping that only fans
* out to every source dropped; packages.lock.json contentHash re-derived
from nuget.org's catalog packageHash (SOCKET_NUGET_URL), refusing when
the restored config does not resolve the id from nuget.org alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
…e the cargo guard test

rollback in a project whose only state is a stale pre-v5
redirect-state.json (no manifest, no vendor ledger, no hosted pin in the
lockfiles) removes that file and exits 0 instead of failing on the missing
manifest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
The in-run `scan --mode hosted --vex` attestation read its hosted records
from the ledger the run had just written. With no ledger, the run's
fetched records reach the VEX builder in memory (VexBuildParams
hosted_records), merged over any pre-v5 ledger's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
A package vendored over hosted wiring before v5 recorded the hosted
fragment as its pre-vendor original, so reverting it wired the lock back to
the patch server. After a wet revert, any hosted pin on a reverted purl is
restored to its upstream registry entry (warning
vendor_revert_restored_upstream; a refused restore is a failed event,
hosted_restore_failed, exit 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

CI is expected to be red for now. This draft PR removes the hosted ledger (.socket/vendor/redirect-state.json), so every test that asserted the ledger gets written or replayed now fails until it is rewritten to the new contract.

  • Fixed in pushed commits:
    • clippy on 03643e0, fixed in 37e96e2 and c0d0797.
    • The hosted in-run --vex returning manifest_not_found (e2e_redirect_npm_build) on 5eadb00, fixed in 2b331c1. The in-run VEX now gets this run's records in memory.
  • In progress: the remaining failures are ledger-contract tests: the integration suites plus scripts/backtest-*.py, which the compatibility workflows run. They are being rewritten and will land in follow-up pushes on this branch.

Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

v5 review: this is the critical hosted-pivot PR, and it needs fixes before landing. Reviewed/tested 5eadb006 and checked the subsequent 2b331c1 delta; the findings below are unchanged.

I reproduced three management-path problems (inline), even though all 36 upstream_restore_golden tests pass. The missing coverage is the complete CLI lifecycle, especially fresh checkouts and offline behavior.

For simplification, this branch currently adds another family of per-format inverse parsers while retaining old ledger replay machinery. Its reviewed diff was +12,100/-2,827 lines, including a checked-in 586-line upstream/client.rs.orig. Delete that backup, route restore through #281's shared models, and audit removal of unused ledger writers/replay paths while preserving a small legacy reader. Verify external library consumers before deleting exported APIs.

Make hosted→vendored a single planned transition: acquire/verify the replacement, plan edits against a staged project view, then commit. Avoid restoring the live project to upstream before knowing that vendoring can succeed. The current failure path still commits accumulated restore edits, so add failure-injection coverage proving a failed eject preserves hosted protection.

Use raw lockfile wiring as state and VEX eligibility as a separate judgment of that state. Reusing an attestation-filtered list as the entire management inventory loses information.

Update README/CLI_CONTRACT/CHANGELOG with the actual ledger-free, rollback, offline and eject guarantees in the same PR. These still describe hosted ledger persistence at the reviewed head.

Validation: built the CLI; localhost HTTP probes for offline and fresh-Cargo eject; conflicting-lock list/vendor/rollback probes; 36 core upstream restoration goldens passed. Full cross-platform matrix not run locally.

Comment thread crates/socket-patch-cli/src/commands/vendor.rs
Comment thread crates/socket-patch-cli/src/commands/vendor.rs Outdated
Comment thread crates/socket-patch-cli/src/commands/mod.rs
…y request

Review follow-ups:

- core HostedInventory keeps raw hosted wiring apart from attributable
  pins: a hosted identity discovery recognizes but cannot attribute (locks
  that disagree, a malformed reference, a lockless registry pin) is
  contested wiring. rollback (unscoped), remove, list and vendor refuse
  around it with hosted_wiring_contested, naming the files and the
  git checkout remedy, instead of reporting a bare project.
- vendor's eject refuses offline (flag or env, wet or dry) with
  offline_eject_unavailable before it builds any request.
- Drop the stray upstream/client.rs.orig merge backup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
Review follow-ups on the hosted -> vendored eject:

- Every patch record is fetched first; one the API cannot serve refuses
  the whole eject (eject_refused) before anything is touched.
- The upstream restore is planned first (a dry resolve of every pin); a pin
  that cannot be restored refuses the whole eject with its remedy.
- A dry run stops at the verified plan (eject_planned events) and writes
  nothing, not even .socket/.
- The wet run takes the apply lock once, snapshots every file the eject
  can touch (root files, pin and restore files, cargo/maven config, the
  vendor ledger, vendored uuid dirs), restores the pins upstream BEFORE the
  vendor engine inventories sources (so a fresh checkout with nothing
  installed resolves the pristine registry package), vendors, and on any
  failure puts the snapshot back (eject_rolled_back): a failed eject
  leaves the project hosted, byte for byte.

Adds failure-injection and dry-run coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
…restore

Hosted state is the lockfile pin: rollback/remove restore the default
upstream registry entry (mock npm registry via SOCKET_NPM_REGISTRY), a
refused pin (offline, registry 404, missing integrity) fails closed with
the git-checkout remedy, and a pre-v5 ledger is never replayed but
retired once no pin remains. Ledger-persist failure tests become
restored-lockfile write failure tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
…ontract

- in_process_rollback_hosted (+ vlt): hosted pins come from the lockfiles
  and are restored from a mock npm registry; per-pin refusal, scoped
  restores, legacy-ledger retirement (never replayed), preserve-state,
  and the vlt heal following restored pins. A vlt project re-locked onto
  the registry has no hosted state left to roll back.
- coverage_fix_rollback_ecosystem_scoped_replay -> ..._scoped_hosted:
  --ecosystems never restores another ecosystem's pin nor retires the
  pre-v5 ledger while a pin remains.
- e2e_golang_hosted_state: rollback and vendor takeover restore go.sum
  from a mock checksum database; offline refuses.
- redirect_npm_allow_remote: no ledger records the .npmrc edit; restore
  deletes only a pristine scaffold .npmrc and reports a kept
  allow-remote=all line (npm_allow_remote_left).
- hosted_symlinked_files, repair_invariants: no ledger is written; a
  lockfile-pin-only project takes the redirect_only_project skip.
- vex_pipenv_pip_steps: a reverted checkout with no ledger is the plain
  manifest_not_found error; apply --vex fetches the record online when
  no ledger supplies it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
…ract

Hosted scan/get write no .socket/vendor/redirect-state.json: assertions on
the ledger become "no ledger" plus lockfile facts; pre-v5 (incl. corrupt)
ledgers are pinned as ignored and left byte-identical; ledger-write failure
tests become "a read-only .socket/vendor or a squatting dir no longer
blocks the run"; re-runs are pinned idempotent on the lock bytes.

Rollback round trips now name the mock host with --patch-server-url and
mock the upstream registry (SOCKET_NPM_REGISTRY / SOCKET_PYPI_JSON_API);
berry CRLF/BOM, bun digestless, pnpm, poetry, pdm and pipenv restores are
checked against the pristine locks. bun.lockb rollback pins the refusal.
Manifest-less VEX legs attest from lock + API, and use a synthesized
pre-v5 ledger as the extra local record source for the offline and
redirect_unwired legs. scan redirectState/hosted_wiring_retained/updates
are pinned to lockfile pins; vendor_supersedes_redirect is gone.

vlt_hosted_common gains assert_no_ledger, legacy_record_from_view and
write_legacy_ledger (additive).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
- remove's hosted_revert_failed message is the restore's own refusal (it
  already names the pin and the remedy) instead of wrapping it twice.
- Retiring a pre-v5 ledger prunes the emptied .socket/vendor/.
- The in-run hosted VEX summary says patches are attested from their patch
  records, not from a ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
list: hosted entries are lockfile pins (details.mode hosted,
details.lockfiles, no details.ledger); a pre-v5 ledger only details a
matching pin and never lists a record by itself. vex: a malformed pre-v5
redirect ledger is the redirect_ledger_corrupt warning, the run proceeds
and the file is left byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
v5 hosted mode writes no .socket/vendor/redirect-state.json, so the shared
harnesses (vex_pdm_hatch_common, vex_pipenv_pip_common, vex_e2e_common/bun
and vlt, npm_e2e_common/manifestless) now assert the hosted wiring run left
NO ledger, while vendored keeps its .socket/vendor/state.json cells.
Hosted cells that relied on the ledger now pin the new contract: offline
with no local record is record_unavailable, online attests from the API,
and a reverted hosted lock leaves nothing to discover. vendor over hosted
pins is the eject flow, so the manifest-less embedded cells drive apply
for hosted checkouts and a new cell pins the eject path. One focused cell
shows a committed pre-v5 ledger still lets a hosted pin attest offline
(new additive helpers: write_legacy_redirect_ledger,
assert_no_hosted_ledger, LEGACY_REDIRECT_LEDGER).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
The hosted Pipfile.lock rewrite dropped each entry's `index`, and the
ledger-free upstream restore then guessed it from sibling registry
entries (none -> the PyPI source name; all siblings index-less -> none).
Pipenv's own choice cannot be re-derived from the lock or the Pipfile:
for the same Pipfile it depends on the release and the locking
environment (measured with real `pipenv lock`):

  shape            2018.11.26  2020-2022  2023.12.1-2026.8.0
  direct           pypi        pypi       pypi
  extras table     pypi        pypi       (none)
  marker-excluded  pypi        (none)     (none)
  transitive       (none)      (none)     (none)

So the backtest's rollbackRestoresLockBytes failed on 2022.12.19
extras (transitive pysocks sibling has no index -> index dropped) and
on 2022.12.19 / 2026.8.0 marker-excluded (no siblings -> "pypi"
added), and rollbackAfterRelockRetires failed on 2026.8.0
marker-excluded: the relock hybrid (our `file` kept, `version` and
registry `hashes` restored, no `index`) was restored with an `index`
Pipenv never wrote.

The hosted rewrite now keeps `index` exactly as Pipenv wrote it
(present or absent) and only drops `version`; the restore carries the
entry's `index` back unchanged instead of choosing one, refusing when
it (or the Pipfile's explicit index) does not name a PyPI source in
`_meta.sources`. A `file` entry carrying `index` installs the
referenced wheel itself (direct_url.json present) on Pipenv
2018.11.26, 2020.11.15, 2021.11.23, 2022.12.19, 2023.12.1, 2024.4.1,
2025.1.3 and 2026.8.0 (`install --deploy`, `sync`, `verify`), and a
marker-excluded one stays uninstalled; Pipenv 7-11 ignore `index` on a
`path` entry (convert_deps_to_pip skips it for file/path deps).

Tests: real Pipenv 2018.11.26 / 2022.12.19 / 2026.8.0 locks for the
extras and marker-excluded shapes (tests/fixtures/pipenv-shapes) round
trip byte for byte in LF and CRLF, and the 2026.8.0 marker-excluded
relock hybrid restores the pristine bytes. The backtest's
allCategoriesRewritten now expects hosted entries to keep the pristine
`index` (vendored still drops it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

This update is at 08c30b7. It fixes most of the CI failures from dc634b3; the bun.lockb case is still in progress. The base branch passes the bun, PDM, Pipenv and yarn-berry jobs, so every failure below came from this PR.

Product fixes (hosted rollback must restore the lockfile byte for byte)

  • PDM static_urls (ef79e51): the restore wrote files in filename order, but PDM orders them by URL. The fix sorts by location. The golden test now uses real bucketed URLs, so it would catch this.
  • Poetry lock 1.0/1.1 (59a0834): with today's PyPI, Poetry 1.0/1.1 locks record [metadata.files] urllib3 = []. The hosted rewrite gave that entry the same inline shape as a populated one, so rollback always restored the full file list.
    • Now a populated entry keeps Poetry's one-file-per-line layout, and an inline one means the original was [].
    • A new golden test round-trips every native fixture from 1.0.10 to 2.4.3, with LF and CRLF line endings.
  • Pipenv (08c30b7) — needs sign-off, because it changes the hosted Pipfile.lock shape:
    • The hosted rewrite now keeps the entry's own index instead of dropping it and guessing it back on rollback.
    • Guessing can't work. With real Pipenv 2018.11.26 through 2026.8.0 and the same Pipfile, whether an entry gets index depends on the Pipenv version (extras: pypi up to 2022 and none from 2023; marker-excluded: pypi only in 2018).
    • install --deploy, sync and verify still install the patched wheel with file + index on all eight versions.
    • Rollback refuses when the entry's index doesn't name a PyPI source.
    • Updated CLI_CONTRACT, docs/testing/pipenv-compatibility.md and CHANGELOG to match.

Test and harness fixes

  • macOS build (89578ce): a macOS-only rollback test still read before_hash/after_hash, which this PR removed from the fixture.
  • Yarn berry e2e (6da01c0): the manifest-less VEX matrix still expected a hosted flow to leave .socket/vendor. It now expects no ledger, record_unavailable offline, and nothing discovered once reverted, the same as the npm and bun matrices. I couldn't run it locally because yarn berry can't be installed here.
  • Bun backtest (ef79e51): the lockb digest and the artifact downloads now send a User-Agent. Bun 1.1.0 went from 18/38 to 37/38.

Still open

  • Bun < 1.2 (bun.lockb), hosted-then-vendored: vendor over a live hosted lockb pin returns partial_failure, because upstream restore has no bun.lockb restorer. A fix is in progress.
  • vlt install-proof (vlt_pinned_matrix_agent_get_and_remove): red on the base branch at 8ae7dc3 too, as noted earlier.

Local checks: clippy is clean, and the full workspace test suite passes except 18 tests that assume a non-root user and fail only because this sandbox runs as root.


Generated by Claude Code

With the hosted ledger gone, `vendor` over a live hosted pin in a binary
bun.lockb (Bun 0.8.1-1.1.x's default lock, Bun 1.2's legacy lock) was
refused `redirect_revert_failed`: format_of() mapped bun.lockb to
Unsupported, so the backtest matrix's hosted-then-vendored cell exited
partial_failure.

The upstream restore gains a bun.lockb restorer
(patch/redirect/upstream/bun_lockb.rs). It rebuilds each hosted
remote-tarball record as Bun's npm registry record for name@version,
from the registry's dist.tarball / dist.integrity (SOCKET_NPM_REGISTRY
aware), via the new BunLockb::set_registry_package. That function
re-interns the URL (re-using the original pool offset), drops the
hosted URL string from the pool tail and re-derives the metadata hash.
The staged restore view now carries binary files.

To make the rebuild byte-exact, set_package keeps the registry record's
inactive bytes (padding, semver) when it writes a remote tarball
record. Early writers leave uninitialized padding there (Bun 0.8.1), so
this matters. A re-pin to a later grant's URL now drops the superseded
URL from the pool. A record without the retained bytes is rebuilt the
way Bun writes one; prerelease versions are refused in that case.

The rebuild is exact for every fixture writer except a format-1 lock
(kept promoted) and workspace locks (behaviors kept normalized). So
only the vendor takeover and eject opt in (RestoreOptions::bun_lockb):
their vendor ledger records the rebuilt record, and `vendor --revert`
returns the pre-hosted bytes. `rollback` / `remove` keep refusing a
hosted bun.lockb pin with the `git checkout -- bun.lockb` remedy, as
the harness's rollbackLockbRefused expects. An offline vendor still
refuses.

Tests: core restorer tests over every real fixture, codec tests for the
rebuild (retained and zeroed records, re-pin), a hermetic CLI test
(vendor_eject_bun_lockb.rs: takeover, eject, rollback and offline
refusals on Bun 0.8.1 / 1.1.38 / 1.2.0 locks), and the real-Bun
e2e_bun_lockb takeover now vendors online and reverts byte-exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
On Windows, Path.write_text turned the injected bun.lock's LF into CRLF, so
the LF pre-injection bytes could never match the (EOL-preserving) upstream
restore: custom-registry hosted rollbackOriginalFiles failed on Windows only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
The bun backtest's binary job failed only for the format-1 writers (Bun
0.1.1 / 0.1.6, readers 0.5.9 and 1.4.2) after 65aa074: the hosted rewrite
promotes a format-1 lock to format 2 (format 1 has no URL column), and
the new native upstream restorer rebuilt the registry record inside that
promoted lock. The vendor ledger then recorded the promoted bytes as its
pre-vendor original, so `vendor --revert` returned a format-2 lock and
e2e_bun_lockb's "the revert restores the pre-hosted bytes exactly"
assertion failed. A promoted lock is byte-for-byte indistinguishable from
one Bun 0.1.7+ wrote (same pool order, same metadata hash), and hosted
mode keeps no ledger, so the restorer could not tell.

The codec now marks a lock whenever an edit had to normalize it: seven
magic bytes and a flag byte in the last eight bytes of the root package's
resolution (the root resolution's value union, which no Bun reader reads;
early writers leave uninitialized bytes there, and 1.4.2 / 0.5.9 read such
locks). For a promoted lock those bytes are new, so the mark overwrites
nothing. promote_legacy_format sets NORMALIZED_FORMAT_1;
normalize_workspace_behaviors sets NORMALIZED_WORKSPACE when it changes a
dependency behavior or workspace literal. The vendor ledger's
layout_original path normalizes the original the same way, so its exact
revert is unchanged.

The bun.lockb upstream restorer then:
- demotes a NORMALIZED_FORMAT_1 lock back to format 1 once every hosted
  record is rebuilt (BunLockb::demote_legacy_format, which drops the URL
  column and the URLs the promotion appended to the pool, and succeeds
  only if promoting the result again reproduces the lock byte for byte).
  Otherwise the pins are refused with the `git checkout -- bun.lockb`
  remedy.
- refuses a NORMALIZED_WORKSPACE lock outright with that remedy, since
  clearing the workspace behavior bit cannot be undone. It no longer
  takes the lock over non-exactly.

Tests: the upstream restorer's byte-exact test now covers 0.1.1 / 0.1.6.
Workspace-normalized extension locks refuse, and so does a lock whose mark
carries an unknown flag. The codec rebuild test checks the exact
demotion. vendor_eject_bun_lockb adds the 0.1.1 / 0.1.6 takeover with an
exact revert and a workspace-lock refusal. Locally with real Bun, the
1.4.2 reader x 0.1.1 / 0.1.6 writer cells now pass the takeover and the
exact revert. They then stop at the 0.5.9 legacy reader, which segfaults
on any networked install in this sandbox. The 1.1.45 / 1.2.0 / 1.4.2
cells pass everything except the `extensions` shape, which needs
api.github.com (403 here).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
The Windows leg runs the same suite ~1.6x slower than macOS. On the base
branch it already took 34m40s of the flat 35-minute budget, and with this
PR's added tests it was cancelled at the limit mid-run (no failures).
Linux and macOS keep 35 minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
The Windows test leg's CRLF checkout (core.autocrlf) turned the LF-committed
tests/fixtures/poetry, pipenv and pipenv-shapes locks into CRLF, so the
byte-exact upstream-restore round trips (and their derived CRLF variants,
which became \r\r\n) and the Poetry VEX pin-spelling test failed on
Windows only. Mark them -text like the other captured-lock fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

CI status after ba7034f / fc9a52a:

  • Windows test leg timeout (ba7034f): the leg was cancelled at its flat 35-minute budget on 8ce40d9 with no failures so far. On the base branch it already took 34m40s. Windows now gets 50 minutes; Linux and macOS stay at 35.
  • Windows-only failures the timeout had hidden (fc9a52a): with the longer budget the leg finished on ba7034f and failed upstream_restore_golden (4 Pipenv/Poetry round trips) and e2e_vex_lockfile (poetry::every_hosted_pin_spelling_…). The cause is Windows' CRLF checkout of the LF-committed tests/fixtures/{poetry,pipenv,pipenv-shapes} locks. They are now -text like the other captured-lock fixtures. I reproduced this on Linux with a core.autocrlf=true worktree: 4 + 1 failures before the fix, 39/39 and 286/286 after.
  • covgap_commands_scan_mod on Windows is not from this PR: the base branch's Windows leg (8ae7dc3, run 36352437716) fails the same target, and it passes here even with a CRLF checkout, so it is not an EOL issue. I'm leaving it for a separate fix.
  • vlt install-proof: still the known vlt_pinned_matrix_agent_get_and_remove failure that is also red on release/v5-prerelease (commented earlier).
  • Everything else on 8ce40d9 / ba7034f is green, including all 52 Bun jobs (format-1 bun.lockb and Windows 1.2.x custom-registry fixes confirmed) and the Pipenv, Poetry, PDM, pnpm, npm and Go workflows.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

CI on fc9a52a has finished, and the only two red checks also fail on release/v5-prerelease:

  • CI: 38/38 jobs pass except test (windows-latest). That job fails only covgap_commands_scan_mod, the same target the base's Windows job fails (8ae7dc3, run 36352437716). The upstream_restore_golden and e2e_vex_lockfile failures are gone after the .gitattributes fix. The job took 34.5 minutes, inside the new 50-minute limit.
  • vlt: install-proof still fails vlt_pinned_matrix_agent_get_and_remove, which also fails on the base.
  • Green: Bun 52/52 (native (macos-latest, 1.1.45) hit a runner DNS failure, urlopen … Errno 8, on every hosted cell; its one re-run passed), plus the Pipenv, Poetry, PDM, pnpm, npm, Go and Pin check workflows.

Still open from earlier: owner sign-off on the Pipenv contract change (hosted Pipfile.lock keeps its index, see comment 5862392841).


Generated by Claude Code

@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review September 28, 2026 10:53
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit 686e5fb into release/v5-prerelease Sep 28, 2026
322 of 386 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the v5/ledger-free-hosted branch September 28, 2026 10:53

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

Fix All in Cursor

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.

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

Reviewed by Cursor Bugbot for commit fc9a52a. Configure here.

Err(e) => return Err(e),
};
files.push((rel, bytes));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Eject snapshot reads hang on FIFOs

High Severity

EjectSnapshot::take reads every project file it may restore with bare tokio::fs::read. A FIFO or device at a lockfile or config path blocks the wet eject in open(2) before the snapshot exists, so the run never reaches the planned restore or rollback.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fc9a52a. Configure here.

hosted_leg
.failed
.push(("hosted_wiring_contested".to_string(), refusal));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rollback writes then fails contested

High Severity

Unscoped rollback restores attributable hosted pins first, then fails the run when contested wiring remains. restore_upstream has already flushed, so a failed rollback leaves some locks upstream and the contested files still hosted. vendor eject refuses this layout before any write.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by learned rule: Safety refusals for unsupported configurations must run before calm early-returns

Reviewed by Cursor Bugbot for commit fc9a52a. Configure here.

)
.into_iter()
.map(|pin| canonical_purl(&pin.purl))
.collect();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Vendored refusals miss contested pins

Medium Severity

lock_text_refusals_for and hosted_state_from_lockfiles still build hosted claims from HostedPin::all, which only sees VEX-eligible refs. Contested hosted wiring is omitted, so those packages are lock-text-gated or classified as not hosted instead of taking the restore/takeover path.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by learned rule: Inventory/discovery must propagate unsupported-layout diagnoses as typed errors, never swallow into None

Reviewed by Cursor Bugbot for commit fc9a52a. Configure here.

Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Sep 28, 2026
…odels

Conflicts resolved toward #280's model: the yarn fragment-kind helper
goes with the ledger, the rollback fixture keeps base's shape, and the
scan test imports follow base. #280's new upstream restore now reads
through the format models this branch introduced: the Cargo.lock
restore splices source/checksum at CargoLock's spans instead of the
deleted block walker, and the gem, maven, nuget, composer and pnpm
readers are imported from formats::.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BYsX2VfVNKHvfFAJnFryc
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Sep 28, 2026
…setup-and-ui

Conflicts resolved toward #280's model: no hosted ledger, rollback/remove/
vendor restore hosted pins via patch::redirect::upstream. This branch's
changes stay on top: `setup` removed, human text says "hosted" and drops
warning codes, one numbered Next steps block, empty `list` exits 0 (the
contested-wiring error from #280 still exits 1), shared cancel line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Sep 28, 2026
… and docs

#280 added tests and contract lines with the pre-WS8 human strings
("Would redirect", "<purl> redirected, but its patch record ...",
`Warning (<code>): ...`). Switch them to this branch's conventions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Sep 28, 2026
…sted-engine

Resolved toward #280's model: v5 hosted mode keeps no ledger.

- hosted::ledger (the redirect-ledger merge, in-memory load and
  serializer) is deleted; neither the disk flow nor the in-memory engine
  reads or writes .socket/vendor/redirect-state.json.
- Ledgers' hosted store is now the hosted records: the lockfiles' hosted
  pins (or a run's fetched records), plus a pre-v5 ledger read only for
  migration. hosted_vendored_overlap drops the edits-only fallback.
- list, scan's updates[] and rollback run the shared owner rule over the
  manifest and the vendor ledger and take the hosted pins from the
  lockfiles; updates[] keeps #280's precedence (manifest > pins > vendor
  ledger). scan reads the pins through ProjectContext's one discovery.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHZ8uzdXfkG2zH8ZYDG8ju
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Sep 28, 2026
* Remove the setup subcommand and its install hooks (v5 WS7)

`socket-patch setup` (and --check/--remove/--exclude) is gone, with every
install hook it wired: npm postinstall/dependencies scripts, the
socket-patch[hook] .pth wheel, the in-tree Bundler plugin + Gemfile block,
and Composer post-install/update scripts. `apply` stays; agent mode in CI
is `scan --mode agent` once, then `socket-patch apply` after each install.

Deleted: commands/setup.rs, core setup/** and the setup-only package_json
helpers, the setup tests and setup-matrix suites, the setup-e2e feature,
the setup-matrix CI job, tests/setup_matrix and scripts/setup-matrix.sh.
vex's install-hook "Property 7" filter goes with it. The socket-patch-hook
wheel and socket-patch-bundler gem are dropped from the build and publish
workflows (sources kept, frozen, pending an owner decision).

Also the plan's small follow-ups: drop the core crate's deprecated
re-export aliases (and the CI grep that guarded them), the unused
utils::process::tool_command, the vacuous e2e_cargo/e2e_golang CI rows,
add the merged 01019627 and 9c2b4925 gem patches to the vendored
production e2e, and retire the backtest-poetry "known crawler gap" label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj

* Streamline the patch UI (v5 WS8)

- `-h` lists about eight options per command (`cli_command()` marks the
  rest hide_short_help; `--help` is unchanged); `scan --apply/--vendor`
  are hidden (still accepted).
- Human warnings drop the `(code)` tag (`Warning: …`, `GC: skipped: …`);
  JSON keeps every code. Error lines keep theirs.
- Human text says "hosted", not "redirect" (JSON keys unchanged).
- npm's allow-remote notice is one line; `--verbose`/JSON keep the full
  policy text.
- One `ui::next_steps` renderer for hosted and vendored results.
- Hosted and vendored `get` never prompt: top-ranked patch per package,
  like scan, in JSON too. Agent-mode `get` keeps its picker and confirm.
- `list` with nothing to list says `No patches in this project. Run
  \`socket-patch scan\`.` (exit codes unchanged: 1 missing, 0 empty).
- One cancel line (`ui::CANCELLED`) and one paid upsell (`ui::PAID_UPGRADE`).
- `get`'s self-enforced flag conflicts and `rollback --one-off` exit 2,
  like every other usage error.

Docs: CLI_CONTRACT (human output conventions, exit codes, get prompts),
README, CHANGELOG [Unreleased], v5 plan status. Tests updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj

* Pin the real-vlt get_and_remove leg to --mode agent

`get <uuid>` defaults to hosted since v5, so the leg's in-place
patched/pristine assertions need agent mode spelled out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj

* Match the hosted-ledger persist-failure wording in two covgap tests

These chmod-guarded tests skip under root, so the WS8 wording change
("hosted redirect ledger" -> "hosted ledger") only showed up in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj

* Empty list exits 0; group help by task; document the setup upgrade

Review follow-ups:
- `list` on a project with no manifest and no ledger record is an empty
  list: exit 0, the empty-project line (human) or the success envelope
  with `events: []` (`--json`). Only an unreadable or invalid manifest
  fails. Hosted mode writes no manifest, so this is the normal case.
- Root help groups the commands by task (patch, undo, ship, agent mode)
  instead of calling get/rollback/remove "older agent-mode commands";
  the subcommand list follows the same order. `-h` keeps --cwd,
  --ecosystems and --offline, and moves `scan --prune` to --help.
- README gains "Upgrading from `setup`": move to hosted or keep agent
  mode, and the exact hook to delete per ecosystem. The CHANGELOG and
  the frozen hook/plugin READMEs link it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj

* Carry the hosted wording and code-free warnings onto #280's new tests and docs

#280 added tests and contract lines with the pre-WS8 human strings
("Would redirect", "<purl> redirected, but its patch record ...",
`Warning (<code>): ...`). Switch them to this branch's conventions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj

* Give the cargo safety VEX baseline a vulnerability to attest

With setup's install-hook filter gone, the manifest-backed agent-mode
cargo patch attests, but the staged minimal manifest carries no
vulnerabilities, so vex ended no_applicable_patches (exit 1). Add one
vulnerability to the entry before the baseline run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 1, 2026
* Lead the CLI help with the v5 workflow

List scan, vex, vendor and list first, then the agent-mode commands
(get, apply, setup, rollback, remove, repair), and add a short
"Typical workflow" footer to the root help.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Prefer the newest merged patch when choosing one per package

A merged patch folds several advisories into one blob and is the
package's cumulative fix, so the newest one the user can download now
wins outright. Packages with no merged patch keep the old order:
highest severity, then newest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Make scan default to hosted mode and never prompt

A bare scan now runs hosted mode: it rewrites lockfiles so only the
patched dependencies resolve to Socket-hosted packages. A path-scoped,
--prune or global scan with no mode only reports, since it has no
lockfile to rewire.

scan asks nothing any more: no confirm prompt in any mode and no patch
menu. It takes the top-ranked patch per package.

New --package filter (repeatable or comma-separated, env
SOCKET_SCAN_PACKAGES) scopes a scan to packages by name or purl.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Detect hosted patch updates from the lockfile's own pins

scan's updates[] now also sees the hosted pins the lockfiles wire, not
only the redirect ledger's records, so a hosted project that never
committed its ledger still reports a superseding patch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Share the crawler-options and ecosystem-scope helpers across commands

GlobalArgs gains crawler_options(), is_global(), ecosystem_selected()
and purl_ecosystem_selected(). They replace eight hand-copied
CrawlerOptions literals, seven --ecosystems predicates and seven global
checks across scan, vendor, vex, apply, setup, rollback, get and
repair.

vendor's predicate also matched case-insensitively and accepted
for golang. clap validates the names first, so neither form ever
reached it. It now uses the same exact match as everything else.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Let hosted and vendored scans take project directories

In hosted and vendored mode, and so in a bare scan, positional PATHs
now name project directories: each directory, or directory glob such
as apps/*, is scanned on its own as if it were --cwd, under a
"== <dir> ==" header. The exit code is the worst of the runs. A PATH
that is not a directory is a usage error. --json takes a single
directory so stdout stays one document. Agent-mode PATHs keep their
installed-path glob meaning.

The changelog's v5 section now leads with the scan → vex → vendor
workflow and describes the new scan defaults.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Rank [UPDATE] detection by the same rule scan installs by

candidate_supersedes still put severity above merge state, so scan's
updates[] and [UPDATE] marker could name a different patch from the one
scan selects. It now calls a new ranking::batch_supersedes, which uses the
same merged-first key as the selection and ignores the tier and uuid
tiebreaks and missing dates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Default get to hosted mode, like scan

socket-patch get <id>, and the bare-UUID shortcut, now redirect the
package to its Socket-hosted patched copy. Agent mode (manifest + in-place
apply) is --mode agent. It stays the default with --save-only, which
records a manifest entry, and with --global/--global-prefix, since those
have no project lockfile to rewrite. The agent-mode test fixtures now pass
--mode agent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Rewrite the README, CLI contract and docs for v5

The README now leads with the scan → vex → vendor workflow: hosted mode
first, vendored for offline installs, agent mode as the older
install-hook flow, and the command reference in that order. The contract
and README now match the code on these points:
- a bare scan and get run hosted mode
- scan never prompts
- --package
- PATH semantics in each mode
- the merged-first patch ranking
- the exit-2 cases
- the env var table

The Bundler plugin README now says setup writes a path: source and that
failures warn by default. The docs/testing notes drop references to files
and flags that no longer exist.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Clean up stale and narrative comments across the tree

Comments and doc comments now describe the current code:
- references to renamed or deleted functions, files, tests and flags are
  fixed;
- notes about pre-v5 scan behavior (prompts, report-only non-TTY scans,
  severity-first ranking, PATH rejection) are gone;
- TODOs for finished work are removed;
- history narration ("used to", bug diaries, PR and audit tags, dated
  verification notes) is rewritten as the invariant it protects;
- doc comments attached to the wrong item are moved.

Two CI path filters that named the deleted vendor/lock_inventory.rs now
match vendor/lock_inventory/**. No code behavior changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Fix follow-ups from the stale-doc sweep

- An exported-but-empty SOCKET_SCAN_PACKAGES= filtered every package out
  of a scan. It is now scrubbed like the other local env vars, and blank
  --package specs are ignored.
- Manifest-less VEX liveness for a rebuilt cargo vendor entry (no
  recorded wiring) now also probes the root Cargo.toml, where v5 writes
  the [patch.crates-io] table, not only the pre-v5 .cargo/config files.
- setup names the dependency it writes, socket-patch[hook]. The
  repair_vendor hint and the hosted --prune warning name the current
  flags.
- Drop scan's unreachable patch-menu spacing and the vendored arm's
  leftover blank line from the removed confirm prompt.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Add the v5 plan: decisions and workstreams

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* v5 WS1+WS2: ledger-free hosted mode, upstream-restore rollback, vendor ejects hosted projects (#280)

* Stop writing the hosted redirect ledger from scan

Hosted scan keeps its edits and records in memory only; the lockfiles
are the record of a redirect. Replays the parked WIP (which was
snapshotted on an older tree) as just its hosted.rs delta.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Restore hosted pins to their upstream registry entries instead of replaying the ledger

Imports the stopped local WS1 agent's work-in-progress (backup/local-v5-
ledger-free-hosted): core patch::redirect::upstream re-resolves npm-family,
cargo and golang registry entries for every hosted pin vex::discover finds
in the lockfiles, and rollback/remove/vendor route their hosted legs
through it instead of the redirect ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Update rollback prompt unit test for the upstream-restore wording

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Derive list, scan updates and takeover state from lockfile hosted pins

v5 keeps no hosted ledger, so every reader that consulted
.socket/vendor/redirect-state.json now reads the hosted pins lockfile
discovery finds:

- list shows each hosted pin with the lockfiles wiring it (details.lockfiles);
  a pre-v5 ledger only supplies the details of pins it still describes.
- scan's updates[] fold, redirectState block and the agent-flow
  hosted_wiring_retained probe read the pins.
- The hosted-over-vendored takeover classifier reads the pins; the
  vendored-over-hosted ledger reconcile (vendor_supersedes_redirect) is
  gone: once the lock routes a package to .socket/vendor/ no hosted state
  is left to go stale.
- vex treats a malformed pre-v5 redirect ledger as an advisory.

scan/mod.rs unit tests still need rewriting (WIP).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Drop the hosted ledger from get, repair, the in-memory engine and the scan fold

- get's vendored lock-text gates read the lockfiles' hosted pins instead of
  the redirect ledger (DownloadParams carries --patch-server-url for it).
- repair's hosted-only no-op fires for hosted lockfile pins (or a pre-v5
  ledger).
- The in-memory hosted engine neither reads nor emits
  .socket/vendor/redirect-state.json.
- Disk hosted scan no longer folds edits into a throwaway ledger; its
  records feed only the stale-install probes and in-run VEX.
- The cargo vendor backend's hosted_redirect_live refusal names rollback /
  git checkout instead of a ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* vendor ejects a hosted project; vendoring over any hosted pin restores upstream first

WS2: standalone `vendor` with no manifest now takes its patch set from the
lockfiles' hosted pins (purl + the uuid in each hosted URL), fetches each
record from the API, vendors it into .socket/vendor/ through the same step
`scan --mode vendored` and `get --mode vendored` use, and rewires the lock
from hosted to vendored. Without hosted pins it keeps the no-manifest
no-op.

The vendor takeover now restores the upstream registry entry before
vendoring for every ecosystem, not only cargo/npm/golang, so the vendor
ledger always records the upstream entry as its original and
`vendor --revert` returns to upstream rather than to hosted. A pin whose
upstream entry cannot be restored is refused with the checkout remedy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite scan/mod.rs takeover unit tests for lockfile-derived hosted state

v5 hosted mode keeps no ledger, so the scan/mod.rs unit tests now make a
purl "hosted" by writing a lockfile that pins the hosted URL instead of
planting .socket/vendor/redirect-state.json:

- overlap / classify_overlap_takeover tests use hosted package-lock,
  yarn (classic + berry), bun and cargo sparse-index pins; the
  non-default-host test configures --patch-server-url and pins that an
  unconfigured host is no pin.
- New behavior pinned: a pre-v5 ledger on disk is never hosted state;
  a lock routed to vendored (npm or cargo) yields no pin and no overlap;
  an edits-only state names no package; a half-migrated project whose
  locks name both sides stays silent; the redirectState block has no
  ledger/ledgerKey fields.
- hosted_wiring_retained_purls / redirect_state_json tests read the
  lockfile-derived state, keeping the probe's own liveness gate covered.
- Removed tests of retired behavior: note_vendor_supersedes_redirect
  reconcile (wet/npmrc/dry-run/no-op/persist-failure), the edits-only
  fallback (degraded ledger, vlt tilde keys) and following the vendored
  remediation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Restore hosted gem and composer pins to their upstream entries

Add the RubyGems and Composer restorers to the v5 ledger-free hosted
unwind (`redirect::upstream`).

Gem (Gemfile.lock / gems.locked + Gemfile / gems.rb): a converged Socket
GEM section is removed and its spec moved back, in name order, into the
upstream section (the single remaining one, else the manifest's global
source or rubygems.org, else refused as ambiguous); a bundler <= 2.1
merged section loses only the Socket remote; the DEPENDENCIES `!` pin is
dropped; CHECKSUMS is re-pinned from the rubygems.org compact index. The
Gemfile source block becomes `gem "n", "v"[, opts]` again (the original
constraint is not derivable), or is removed with its DEPENDENCIES entry
only when provably a transitive append. The pre-2.6 mixed state is undone
when a pin is supplied, keeping the untouched lock's own constraint.

Composer: dist {type,url,reference,shasum} and the dropped source block
are rebuilt from packagist p2 metadata (composer/2.0 minified, expanded),
cross-checked against the lock's dist.reference, in the lock's indent,
slash style and line endings. Non-packagist entries are refused.

UpstreamClient gains cached rubygems and packagist lookups
(SOCKET_RUBYGEMS_URL, SOCKET_PACKAGIST_URL).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Fix clippy findings in the eject and takeover paths

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Restore hosted PyPI pins to their upstream registry entries

Add the Python family to the v5 hosted -> upstream restore: Pipfile.lock,
requirements.txt, Hatch direct references (pyproject.toml / hatch.toml),
poetry.lock, pdm.lock, and uv.lock / PEP 723 script locks / PEP 751 pylock
files with their paired pyproject / script metadata.

Each restorer rewrites only entries whose reference is a hosted URL for an
in-scope patch uuid and re-derives what the hosted rewrite overwrote from
PyPI's JSON API (UpstreamClient::pypi_files, base overridable with
SOCKET_PYPI_JSON_API, cached like the other lookups). Where a field is not
derivable the pin is refused instead of guessed: requirements hash-checking
mode that no other line settles, a Pipfile.lock index that is not PyPI,
PDM locks without cross_platform (or uv locks) when the release ships
platform- or interpreter-specific wheels, uv locks with no sibling
registry package to show the artifact shape, several or non-PyPI
registries, exclude-newer / no-binary / no-build filtering, multi-clause
uv specifiers with no spelling evidence, uv 0.2 [[distribution]] locks,
offline runs and registry failures.

The golden harness round-trips the native Poetry (1.0-2.4), PDM (every
supported lock_version) and Pipenv fixtures plus synthetic
requirements/Hatch/uv/pylock projects through the real hosted rewriter;
the shared requirements golden restores modulo the grant's name casing and
the uv golden (no registry sibling) is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Restore vlt, Maven and NuGet hosted pins to their upstream entries

vlt-lock.json: slot [2] from npm dist.integrity, slot [3] per the lock's
own convention (same-era default-registry siblings, else DepID era and
options.registries), every hosted instance of name@version together.
Maven (no network): base versions back, added dependencyManagement
entries, socket-patch repositories and emptied wrappers removed,
trusted-checksum lines dropped and .mvn files deleted only when nothing
but hosted content is left; module poms and stray suffix uses refuse.
NuGet: socket-patch source and mapping removed, a mapping that only fans
* out to every source dropped; packages.lock.json contentHash re-derived
from nuget.org's catalog packageHash (SOCKET_NUGET_URL), refusing when
the restored config does not resolve the id from nuget.org alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Drop dead fixture fields clippy flags in covgap_commands_rollback

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Retire a pre-v5 hosted ledger when rollback finds nothing else; update the cargo guard test

rollback in a project whose only state is a stale pre-v5
redirect-state.json (no manifest, no vendor ledger, no hosted pin in the
lockfiles) removes that file and exits 0 instead of failing on the missing
manifest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Feed the hosted scan's in-run VEX this run's records

The in-run `scan --mode hosted --vex` attestation read its hosted records
from the ledger the run had just written. With no ledger, the run's
fetched records reach the VEX builder in memory (VexBuildParams
hosted_records), merged over any pre-v5 ledger's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Refresh doc comments that still described the hosted ledger

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* vendor --revert restores hosted pins a pre-v5 vendor ledger re-creates

A package vendored over hosted wiring before v5 recorded the hosted
fragment as its pre-vendor original, so reverting it wired the lock back to
the patch server. After a wet revert, any hosted pin on a reverted purl is
restored to its upstream registry entry (warning
vendor_revert_restored_upstream; a refused restore is a failed event,
hosted_restore_failed, exit 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Refuse around contested hosted wiring; refuse offline eject before any request

Review follow-ups:

- core HostedInventory keeps raw hosted wiring apart from attributable
  pins: a hosted identity discovery recognizes but cannot attribute (locks
  that disagree, a malformed reference, a lockless registry pin) is
  contested wiring. rollback (unscoped), remove, list and vendor refuse
  around it with hosted_wiring_contested, naming the files and the
  git checkout remedy, instead of reporting a bare project.
- vendor's eject refuses offline (flag or env, wet or dry) with
  offline_eject_unavailable before it builds any request.
- Drop the stray upstream/client.rs.orig merge backup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Make eject one transaction: plan, restore upstream, vendor, or roll back

Review follow-ups on the hosted -> vendored eject:

- Every patch record is fetched first; one the API cannot serve refuses
  the whole eject (eject_refused) before anything is touched.
- The upstream restore is planned first (a dry resolve of every pin); a pin
  that cannot be restored refuses the whole eject with its remedy.
- A dry run stops at the verified plan (eject_planned events) and writes
  nothing, not even .socket/.
- The wet run takes the apply lock once, snapshots every file the eject
  can touch (root files, pin and restore files, cargo/maven config, the
  vendor ledger, vendored uuid dirs), restores the pins upstream BEFORE the
  vendor engine inventories sources (so a fresh checkout with nothing
  installed resolves the pristine registry package), vendors, and on any
  failure puts the snapshot back (eject_rolled_back): a failed eject
  leaves the project hosted, byte for byte.

Adds failure-injection and dry-run coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite hosted rollback/remove coverage-gap tests to the v5 upstream restore

Hosted state is the lockfile pin: rollback/remove restore the default
upstream registry entry (mock npm registry via SOCKET_NPM_REGISTRY), a
refused pin (offline, registry 404, missing integrity) fails closed with
the git-checkout remedy, and a pre-v5 ledger is never replayed but
retired once no pin remains. Ledger-persist failure tests become
restored-lockfile write failure tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite hosted rollback/remove/vex-step tests to the ledger-free v5 contract

- in_process_rollback_hosted (+ vlt): hosted pins come from the lockfiles
  and are restored from a mock npm registry; per-pin refusal, scoped
  restores, legacy-ledger retirement (never replayed), preserve-state,
  and the vlt heal following restored pins. A vlt project re-locked onto
  the registry has no hosted state left to roll back.
- coverage_fix_rollback_ecosystem_scoped_replay -> ..._scoped_hosted:
  --ecosystems never restores another ecosystem's pin nor retires the
  pre-v5 ledger while a pin remains.
- e2e_golang_hosted_state: rollback and vendor takeover restore go.sum
  from a mock checksum database; offline refuses.
- redirect_npm_allow_remote: no ledger records the .npmrc edit; restore
  deletes only a pristine scaffold .npmrc and reports a kept
  allow-remote=all line (npm_allow_remote_left).
- hosted_symlinked_files, repair_invariants: no ledger is written; a
  lockfile-pin-only project takes the redirect_only_project skip.
- vex_pipenv_pip_steps: a reverted checkout with no ledger is the plain
  manifest_not_found error; apply --vex fetches the record online when
  no ledger supplies it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite hosted scan/get integration tests for the ledger-free v5 contract

Hosted scan/get write no .socket/vendor/redirect-state.json: assertions on
the ledger become "no ledger" plus lockfile facts; pre-v5 (incl. corrupt)
ledgers are pinned as ignored and left byte-identical; ledger-write failure
tests become "a read-only .socket/vendor or a squatting dir no longer
blocks the run"; re-runs are pinned idempotent on the lock bytes.

Rollback round trips now name the mock host with --patch-server-url and
mock the upstream registry (SOCKET_NPM_REGISTRY / SOCKET_PYPI_JSON_API);
berry CRLF/BOM, bun digestless, pnpm, poetry, pdm and pipenv restores are
checked against the pristine locks. bun.lockb rollback pins the refusal.
Manifest-less VEX legs attest from lock + API, and use a synthesized
pre-v5 ledger as the extra local record source for the offline and
redirect_unwired legs. scan redirectState/hosted_wiring_retained/updates
are pinned to lockfile pins; vendor_supersedes_redirect is gone.

vlt_hosted_common gains assert_no_ledger, legacy_record_from_view and
write_legacy_ledger (additive).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Tidy hosted remove/rollback messages and legacy-ledger residue

- remove's hosted_revert_failed message is the restore's own refusal (it
  already names the pin and the remedy) instead of wrapping it twice.
- Retiring a pre-v5 ledger prunes the emptied .socket/vendor/.
- The in-run hosted VEX summary says patches are attested from their patch
  records, not from a ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite list and vex ledger tests for lockfile-derived hosted state

list: hosted entries are lockfile pins (details.mode hosted,
details.lockfiles, no details.ledger); a pre-v5 ledger only details a
matching pin and never lists a record by itself. vex: a malformed pre-v5
redirect ledger is the redirect_ledger_corrupt warning, the run proceeds
and the file is left byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite manifest-less VEX suites for the ledger-free hosted mode

v5 hosted mode writes no .socket/vendor/redirect-state.json, so the shared
harnesses (vex_pdm_hatch_common, vex_pipenv_pip_common, vex_e2e_common/bun
and vlt, npm_e2e_common/manifestless) now assert the hosted wiring run left
NO ledger, while vendored keeps its .socket/vendor/state.json cells.
Hosted cells that relied on the ledger now pin the new contract: offline
with no local record is record_unavailable, online attests from the API,
and a reverted hosted lock leaves nothing to discover. vendor over hosted
pins is the eject flow, so the manifest-less embedded cells drive apply
for hosted checkouts and a new cell pins the eject path. One focused cell
shows a committed pre-v5 ledger still lets a hosted pin attest offline
(new additive helpers: write_legacy_redirect_ledger,
assert_no_hosted_ledger, LEGACY_REDIRECT_LEDGER).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Teach the real-uv VEX matrix the ledger-free hosted mode

A hosted uv flow writes no ledger: assert that, run the embedded steps
online (no local record), expect nothing discovered once the wiring is
reverted, and take a hosted production leg's record from the public
patch API instead of the removed ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Run the real-uv hosted revert against the v5 rollback contract

A hosted uv flow commits no .socket/, so copy_tree tolerates a missing
source; rollback needs --patch-server-url for a pin on the mock origin,
and restores each pin to its upstream registry entry or refuses it with
the version-control hint (asserting nothing was written) instead of
replaying ledger bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Delete the hosted ledger replay engine; keep a read-only legacy loader

v5 derives hosted state from lockfile pins and restores upstream entries
by re-resolving them from the registry, so nothing reverts recorded ledger
fragments any more. Remove the replay machinery and the writers behind it:

- patch/redirect/replay.rs and takeover.rs (fragment replay, per-purl
  npm/cargo/golang revert, redirect_revert_supported)
- state.rs persist_redirect_state, drop_superseded_purl, quarantine and the
  unclassified-edit guards; load_redirect_state stays for migration reads
  and save_redirect_state stays doc(hidden) for laying down pre-v5 fixtures
- npmrc unwind planners, bun.lockb snapshot restore, pipenv/vlt/bun text
  inverses and the EOL fragment respelling that only replay used
- tests that only exercised replay; rewrite round-trips in the poetry/uv
  suites keep their forward and idempotency assertions (the upstream
  restore of those formats is covered by upstream_restore_golden)

hosted_url_names/hosted_url_version move to a small hosted_url module
shared by the bun rewriter and VEX discovery.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Document the ledger-free hosted contract and hosted eject

Update CLI_CONTRACT.md, README.md, CHANGELOG.md [Unreleased] and the
testing/ecosystem docs to v5's WS1/WS2 behaviour: hosted mode writes no
redirect ledger; rollback/remove restore hosted pins to their upstream
registry entries (per-format coverage, refusals incl. --offline and
bun.lockb, the git checkout remedy); list/vex/scan/repair derive hosted
state from the lockfiles (details.lockfiles, the new redirectState shape,
redirect_ledger_corrupt as a warning); vendor ejects a hosted project and
vendor --revert returns to upstream; vendor_supersedes_redirect and
hosted_revert_unsupported are gone; the pre-v5 ledger is read for
migration only and retired by rollback; new registry env overrides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite vendor takeover covgap tests to lockfile hosted pins; add WS2 eject tests

covgap_commands_vendor: a pre-v5 redirect ledger (even malformed) is now
ignored; the takeover guard is driven by hosted pins in package-lock.json
on a --patch-server-url origin (dry-run advisory against a mock registry,
wet takeover + revert back to the upstream registry entry, offline refusal
redirect_revert_failed, unconfigured origin is not hosted). The
redirect-ledger write-failure test has no subject anymore and is removed.

vendor_eject (new): standalone vendor in a hosted npm project ejects the
pins into .socket/vendor/ (no ledger, no manifest), vendor --revert returns
to upstream, a failed view fetch is patch_fetch_failed/exit 1, and no pins
keeps the no-manifest no-op.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite in_process_vendor hosted takeovers to the v5 upstream restore

The berry CRLF takeover and the pnpm/package-lock hosted->vendored
conversions no longer see a redirect ledger: vendor runs online against a
mock npm registry (SOCKET_NPM_REGISTRY) with the patch-server origin
configured, restores the upstream entry, and vendor --revert / rollback
land on the upstream registry entry. Adds an offline-refusal test
(redirect_revert_failed with the checkout remedy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* mode_migration_npm: hosted takeovers restore the upstream registry entry

Hosted mode writes no ledger; vendor over a hosted yarn pin now runs
online with --patch-server-url and restores the upstream entry first
(classic legs read a registry document mirrored from the pristine lock via
SOCKET_NPM_REGISTRY, so the unwind is hermetic). The reverse classic leg
replaces the ledger-originals check with a rollback back to the pristine
registry lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* mode_migration_cargo: hosted takeovers restore the crates.io entry

Vendor over a hosted cargo pin runs online with --patch-server-url and a
sparse-index mirror (SOCKET_CRATES_INDEX) of the pristine checksum; the
ledger-deletion fail-closed test becomes an offline-refusal test
(redirect_revert_failed + checkout remedy) keeping the half-reverted
hosted_redirect_live backstop, which now names rollback / git checkout.
The hosted leg of the manifest-less VEX matrix fetches its record from the
API (no hosted ledger).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* mode_migration_bun: no hosted ledger; unwinds restore the upstream 4-tuple

Every run carries SOCKET_PATCH_SERVER_URL (the mock patch origin) and
SOCKET_NPM_REGISTRY (a mirror of the pristine lock's integrities), so
vendor takeovers, rollback and remove restore the registry 4-tuple; the
ledger record/edit assertions become no-ledger assertions. VEX over a
stale manifest may name the superseded uuid only in vex_record_superseded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* in_process_vendor_bun_takeover: hosted pins without a ledger

bun: every run names http://patch.test the patch server and points
SOCKET_NPM_REGISTRY at one shared registry mirror of the pristine
integrities; hand-written hosted fixtures are the lock's URL 3-tuples
only, and the ledger record/edit assertions become no-ledger assertions
(the unwinds restore the registry 4-tuple byte-exactly).

vlt: scan/vendor/get/rollback over a hosted pin run online against the
mock origin and a registry mirror; the fixture lock records
options.registries so the upstream restore re-derives slot [3] exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e_bun_lockb: binary hosted pins are refused by rollback and vendor takeover

v5 restores a hosted pin's upstream entry instead of replaying a ledger,
which a binary bun.lockb cannot get: vendor over the hosted pin (dry and
wet) and rollback of it now refuse with the git-checkout remedy and write
nothing; the tests apply that remedy and continue the round trip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e_redirect_bun_build: no hosted ledger; rollback restores the upstream 4-tuple

The hosted run writes only bun.lock (asserted: no ledger). Rollback and
its dry run carry the mock origin as the patch server and a registry
mirror of the pristine integrity, so the upstream restore lands on the
pre-redirect lock byte for byte; the repeat-run heal is checked in the
lock alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e_redirect_yarn_classic_build: hosted run writes only yarn.lock

Assert no redirect ledger is written, and carry .socket/ into the fresh
checkout only when it exists (v5 hosted mode writes nothing there).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e_redirect_gem_stale_install: no ledger fallback in hosted mode

The re-fire-from-ledger test becomes a re-scan with a failing record fetch:
record_fetch_failed with the v5 VEX-omission detail, exit 0, wiring
byte-identical, no ledger. Manifest-less VEX drops the ledger-kept cells:
a stale unconverged pair and a reverted pair attest nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e_redirect_pnpm_build: no hosted ledger; rollback restores the resolution

The hosted run and its idempotent re-run write no ledger (the trust
setting and lock splice are checked on disk). The v5/v6 synthetic legs
replace the ledger-original checks with a rollback that re-resolves the
upstream integrity from a registry mirror and lands on the pristine lock
byte for byte; the pinned matrix rollback runs online the same way, and
its vex checks fetch the record from the API (no ledger to read offline).
.socket/ is copied into fresh checkouts only when present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e_redirect_cargo_shapes: remove restores crates.io entries without a ledger

The post-install vex fetches records from the API (no hosted ledger), and
remove runs with a sparse-index mirror of the pristine checksums and the
mock origin named the patch server, restoring every shape byte for byte.
One inherent v5 difference is pinned: a .cargo config that lacked a final
newline gets it back, since without a ledger fragment the rewriter's
separator is indistinguishable from a terminated file's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e_redirect_cargo_build: the three-file rewrite is the whole hosted state

Assert no ledger after scan/get --mode hosted; the post-install vex and
the manifest-less matrix fetch the record from the API (the ledger-kept
cells go), and reverted locks attest nothing. .socket/ travels into the
fresh checkout only when present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* golang hosted e2e: go.mod/go.sum is the whole hosted state

get --mode hosted in a module and in a go.work root now asserts no
redirect ledger is written; the manifest-less VEX tail already covers the
ledger-less shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Format the rewritten vendor/hosted test files

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Pin that a transactional eject emits no per-purl takeover warning

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Document the transactional eject, offline refusal and contested wiring

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Drop a stale comment on the hosted unwind error

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Prove eject from a fresh hosted checkout; verify hash-pinned requirements

The eject restores every hosted pin's upstream registry entry before the
vendor engine takes its source inventory, so a fresh checkout (no
node_modules, empty CARGO_HOME, no virtualenv) fetches the pristine
source from the registry and verifies it against the restored checksum.
New subprocess tests pin that for npm, cargo and pypi.

The pypi case exposed that requirements.txt `==` pins never carried their
`--hash=sha256:` digests into the lock inventory, so a hash-pinned pin
with no installed copy was unverifiable. Carry them as the entry's
integrity (any-of, like Pipfile.lock) while the file resolves from the
public index; an index option keeps every pin unverifiable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Give the stale-Pipfile rollback test a derivable hash mode

A requirements.txt whose every line is a hosted pin is refused by the
upstream restore (hash-checking mode is not derivable). Add an unhashed,
unpatched sibling so the test still pins a successful restore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* node addon smoke: a wet hosted session writes no ledger

The in-memory hosted engine emits only lockfile/config edits in v5, so
assert no .socket/ output instead of requiring the redirect ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* covgap scan_hosted: unreadable-workspace case asserts no ledger

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite the CI-only e2e suites and backtests to the ledger-free hosted contract

v5 hosted mode writes no .socket/vendor/redirect-state.json: every
#[ignore]d real-toolchain capstone and every compatibility backtest that
read the redirect ledger now asserts it is absent and reads the facts
from the lockfile (or the public API record) instead.

- npm / yarn berry / composer / gem / maven / nuget / rush / poetry /
  pdm / hatch / pypi-real / yarn-classic matrices: no-ledger assertions;
  vex offline with no local record is record_unavailable; a reverted
  hosted lock names the patch nowhere (manifest_not_found, exit 2);
  a manifest-less `vendor --vex` is no longer run over hosted pins
  (it ejects them now).
- Mock-origin hosted pins pass --patch-server-url to vex / rollback /
  remove / vendor / scan --mode vendored; the vlt harness gains
  rollback_upstream / Fixture::rollback with SOCKET_NPM_REGISTRY pointed
  at the harness registry.
- Rollback legs expect the upstream registry entry back: npm (real
  registry, JSON-equal lock, .npmrc removed), composer 2 (byte-identical
  from packagist; composer 1's inline repository refuses with the
  git checkout remedy), maven (--offline, byte-identical pom), poetry
  (real PyPI), vlt (byte-exact per era); the production npm leg gains a
  real-registry rollback on a copy.
- vlt: URL-less / update-dropped pins now find no state ("Manifest not
  found"); the TS-written lock restores to its input and the pre-v5
  ledger is retired; mode migrations restore upstream before vendoring.
- Backtests (bun, pdm, pipenv, poetry, vlt, uv docs): hosted records come
  from the public /patch/view/<uuid>; noLedger checks are unconditional
  for hosted; rollback checks expect the upstream entry (bun custom
  registry slot comes back as "", bun.lockb refuses with the remedy);
  the vlt downgrade leg asserts the pin with no ledger.

Core fix: the vlt upstream restore added a slot [3] tarball URL to a
single-node lock whose options record `registry` (vlt rc.33 .. 1.0.4
with config.registry), which vlt itself never writes (save.ts omits the
resolved URL when it starts with the configured registry); rollback was
not byte-exact on those eras. The no-siblings fallback now honours that
rule (unit-tested).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Hold the vlt configured-registry slot [3] rule pending real-vlt evidence

The rule in 44240a2 drops slot [3] when a lock records options.registry,
matching vlt rc.33..1.0.4, but it breaks the vlt-lock-v1-both-registry-keys
golden, and newer vlt releases have not been checked yet. Restore the
previous behaviour until real captures decide it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Backtests: send a User-Agent on public patch-view fetches

The hosted backtest legs now read the patch record from the public proxy's
/patch/view/<uuid>, and CI got HTTP 403 for Python's default urllib
User-Agent (the poetry 1.4-2.x native legs). Send an explicit agent, as the
vlt backtest's api_get already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Re-land the vlt configured-registry slot [3] rule on real-vlt evidence

vlt's lockfile save (identical in 1.0.0-rc.33, 1.0.4, 1.0.10 and 1.2.0)
writes a default-registry node's resolved URL (slot [3]) only when no
`registry` is configured or the URL does not start with it, and records
that `registry` in `options.registry`. Real installs of left-pad@1.3.0
with `config.registry` + `registries.npm` set to the default registry
produce a byte-identical 3-tuple lock on all four releases.

- upstream::vlt: the no-siblings fallback of `records_url` no longer
  adds slot [3] under a recorded `options.registry` the node resolves
  under (unit test covers both sides).
- fixtures: `lock-v1-both-registry-keys` was hand-written with slot [3];
  its input (and expected-edits original) now match the real lock byte
  for byte, and the real capture is added as
  `capture-1.2.0-config-registry` (vex-discover golden extended).
- docs/testing/vlt-compatibility.md records the rule and the evidence.

Without the rule the real-vlt legs that restore a single-node lock of a
`config.registry` project (the harness configures it for rc.33 .. 1.0.4)
are not byte-exact: hosted idempotence / crlf_lock and the migration
scoped_unwind / rollback_from_mixed / agent_rollback_after_takeovers
legs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* hosted-e2e uv leg: serve the record of the uuid the lock pins

pypi_uv_lock_hosted_install_proof handed all three PYPI_UUIDS to
uv_vex::production_manifestless. With no local record (v5 hosted keeps
no ledger) production_record takes the FIRST of those the public proxy
answers for, so the VEX stand-in served de58c8b8 while the resolver had
granted (and uv.lock pinned) e828efa5: every manifest-less cell then
asked the stand-in for e828efa5 and got `vex_record_not_found` /
`record_unavailable`. Pass exactly the wired uuid instead; if production
ever grants a uuid its public view will not serve, production_record now
fails naming it rather than masking it behind another patch's record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* PDM static_urls restore writes files in URL order; bun backtest User-Agent on artifact fetches

PDM orders a static_urls entry's files by URL (the sdist under 0c/39
precedes the wheel under b0/53 for urllib3 1.26.18); the upstream restore
wrote them in filename order, so hosted rollback wasn't byte-exact. The
golden now uses real bucketed URLs so the order differs from filename order.

The bun backtest's lockb digest and asset downloads now send the same
User-Agent as the patch-view fetch (patch.socket.dev 403s urllib's default).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* covgap rollback: macOS-only blob-pin check derives hashes from the fixture bytes

The fixture no longer carries before_hash/after_hash; the macos-gated
manifest-write-failure test still read them and broke the macOS build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Round-trip hosted rollback of unpopulated Poetry 1.0/1.1 locks

Poetry 1.0/1.1 record `[metadata.files] <name> = []` against today's PyPI
JSON API, while locks written earlier list every release file. The hosted
rewrite replaced either with the one-entry patched array, so rollback could
not tell them apart and always re-derived the full release list: the
backtest's `direct` and `crlf` shapes (literal `urllib3 = []`) failed
rollbackRestoresLockBytes on Poetry 1.0.10 and 1.1.15.

The rewriter now keeps that bit in the patched entry's layout: one file per
line (Poetry's own rendering) when the original listed files, inline when
it was `[]`; a re-run keeps the layout it finds. The restore reads it back
and writes the full release list or `[]`.

Adds a golden round-trip over every native fixture generation (1.0.10 to
2.4.3), LF and CRLF, alongside the existing populated-shape one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* yarn berry e2e: manifest-less VEX matrix expects no hosted ledger

The berry matrix still asserted the pre-v5 .socket/vendor redirect ledger
after a hosted flow. v5 hosted mode writes none: the offline cell now
expects record_unavailable, a reverted hosted checkout discovers nothing
(exit 2 manifest_not_found), and manifest-less apply --vex is a calm
no-op. The yarn4 pnpm-linker and workspaces fresh checkouts copy .socket/
only when it exists, as the node-modules berry test already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e vex poetry: accept the one-file-per-line metadata.files pin

A populated lock-1.0/1.1 [metadata.files] entry now keeps Poetry's
multi-line layout after the hosted rewrite (so rollback can tell it from
an originally empty one); the pin-spelling matrix strips that form too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Keep Pipenv's index on hosted entries so rollback restores it exactly

The hosted Pipfile.lock rewrite dropped each entry's `index`, and the
ledger-free upstream restore then guessed it from sibling registry
entries (none -> the PyPI source name; all siblings index-less -> none).
Pipenv's own choice cannot be re-derived from the lock or the Pipfile:
for the same Pipfile it depends on the release and the locking
environment (measured with real `pipenv lock`):

  shape            2018.11.26  2020-2022  2023.12.1-2026.8.0
  direct           pypi        pypi       pypi
  extras table     pypi        pypi       (none)
  marker-excluded  pypi        (none)     (none)
  transitive       (none)      (none)     (none)

So the backtest's rollbackRestoresLockBytes failed on 2022.12.19
extras (transitive pysocks sibling has no index -> index dropped) and
on 2022.12.19 / 2026.8.0 marker-excluded (no siblings -> "pypi"
added), and rollbackAfterRelockRetires failed on 2026.8.0
marker-excluded: the relock hybrid (our `file` kept, `version` and
registry `hashes` restored, no `index`) was restored with an `index`
Pipenv never wrote.

The hosted rewrite now keeps `index` exactly as Pipenv wrote it
(present or absent) and only drops `version`; the restore carries the
entry's `index` back unchanged instead of choosing one, refusing when
it (or the Pipfile's explicit index) does not name a PyPI source in
`_meta.sources`. A `file` entry carrying `index` installs the
referenced wheel itself (direct_url.json present) on Pipenv
2018.11.26, 2020.11.15, 2021.11.23, 2022.12.19, 2023.12.1, 2024.4.1,
2025.1.3 and 2026.8.0 (`install --deploy`, `sync`, `verify`), and a
marker-excluded one stays uninstalled; Pipenv 7-11 ignore `index` on a
`path` entry (convert_deps_to_pip skips it for file/path deps).

Tests: real Pipenv 2018.11.26 / 2022.12.19 / 2026.8.0 locks for the
extras and marker-excluded shapes (tests/fixtures/pipenv-shapes) round
trip byte for byte in LF and CRLF, and the 2026.8.0 marker-excluded
relock hybrid restores the pristine bytes. The backtest's
allCategoriesRewritten now expects hosted entries to keep the pristine
`index` (vendored still drops it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Restore hosted bun.lockb pins natively for the vendor takeover

With the hosted ledger gone, `vendor` over a live hosted pin in a binary
bun.lockb (Bun 0.8.1-1.1.x's default lock, Bun 1.2's legacy lock) was
refused `redirect_revert_failed`: format_of() mapped bun.lockb to
Unsupported, so the backtest matrix's hosted-then-vendored cell exited
partial_failure.

The upstream restore gains a bun.lockb restorer
(patch/redirect/upstream/bun_lockb.rs). It rebuilds each hosted
remote-tarball record as Bun's npm registry record for name@version,
from the registry's dist.tarball / dist.integrity (SOCKET_NPM_REGISTRY
aware), via the new BunLockb::set_registry_package. That function
re-interns the URL (re-using the original pool offset), drops the
hosted URL string from the pool tail and re-derives the metadata hash.
The staged restore view now carries binary files.

To make the rebuild byte-exact, set_package keeps the registry record's
inactive bytes (padding, semver) when it writes a remote tarball
record. Early writers leave uninitialized padding there (Bun 0.8.1), so
this matters. A re-pin to a later grant's URL now drops the superseded
URL from the pool. A record without the retained bytes is rebuilt the
way Bun writes one; prerelease versions are refused in that case.

The rebuild is exact for every fixture writer except a format-1 lock
(kept promoted) and workspace locks (behaviors kept normalized). So
only the vendor takeover and eject opt in (RestoreOptions::bun_lockb):
their vendor ledger records the rebuilt record, and `vendor --revert`
returns the pre-hosted bytes. `rollback` / `remove` keep refusing a
hosted bun.lockb pin with the `git checkout -- bun.lockb` remedy, as
the harness's rollbackLockbRefused expects. An offline vendor still
refuses.

Tests: core restorer tests over every real fixture, codec tests for the
rebuild (retained and zeroed records, re-pin), a hermetic CLI test
(vendor_eject_bun_lockb.rs: takeover, eject, rollback and offline
refusals on Bun 0.8.1 / 1.1.38 / 1.2.0 locks), and the real-Bun
e2e_bun_lockb takeover now vendors online and reverts byte-exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* bun backtest: inject the custom-registry slot with byte I/O

On Windows, Path.write_text turned the injected bun.lock's LF into CRLF, so
the LF pre-injection bytes could never match the (EOL-preserving) upstream
restore: custom-registry hosted rollbackOriginalFiles failed on Windows only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Demote hosted format-1 bun.lockb locks exactly on the vendor takeover

The bun backtest's binary job failed only for the format-1 writers (Bun
0.1.1 / 0.1.6, readers 0.5.9 and 1.4.2) after 65aa074: the hosted rewrite
promotes a format-1 lock to format 2 (format 1 has no URL column), and
the new native upstream restorer rebuilt the registry record inside that
promoted lock. The vendor ledger then recorded the promoted bytes as its
pre-vendor original, so `vendor --revert` returned a format-2 lock and
e2e_bun_lockb's "the revert restores the pre-hosted bytes exactly"
assertion failed. A promoted lock is byte-for-byte indistinguishable from
one Bun 0.1.7+ wrote (same pool order, same metadata hash), and hosted
mode keeps no ledger, so the restorer could not tell.

The codec now marks a lock whenever an edit had to normalize it: seven
magic bytes and a flag byte in the last eight bytes of the root package's
resolution (the root resolution's value union, which no Bun reader reads;
early writers leave uninitialized bytes there, and 1.4.2 / 0.5.9 read such
locks). For a promoted lock those bytes are new, so the mark overwrites
nothing. promote_legacy_format sets NORMALIZED_FORMAT_1;
normalize_workspace_behaviors sets NORMALIZED_WORKSPACE when it changes a
dependency behavior or workspace literal. The vendor ledger's
layout_original path normalizes the original the same way, so its exact
revert is unchanged.

The bun.lockb upstream restorer then:
- demotes a NORMALIZED_FORMAT_1 lock back to format 1 once every hosted
  record is rebuilt (BunLockb::demote_legacy_format, which drops the URL
  column and the URLs the promotion appended to the pool, and succeeds
  only if promoting the result again reproduces the lock byte for byte).
  Otherwise the pins are refused with the `git checkout -- bun.lockb`
  remedy.
- refuses a NORMALIZED_WORKSPACE lock outright with that remedy, since
  clearing the workspace behavior bit cannot be undone. It no longer
  takes the lock over non-exactly.

Tests: the upstream restorer's byte-exact test now covers 0.1.1 / 0.1.6.
Workspace-normalized extension locks refuse, and so does a lock whose mark
carries an unknown flag. The codec rebuild test checks the exact
demotion. vendor_eject_bun_lockb adds the 0.1.1 / 0.1.6 takeover with an
exact revert and a workspace-lock refusal. Locally with real Bun, the
1.4.2 reader x 0.1.1 / 0.1.6 writer cells now pass the takeover and the
exact revert. They then stop at the 0.5.9 legacy reader, which segfaults
on any networked install in this sandbox. The 1.1.45 / 1.2.0 / 1.4.2
cells pass everything except the `extensions` shape, which needs
api.github.com (403 here).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* ci: give the Windows test leg a 50-minute budget

The Windows leg runs the same suite ~1.6x slower than macOS. On the base
branch it already took 34m40s of the flat 35-minute budget, and with this
PR's added tests it was cancelled at the limit mid-run (no failures).
Linux and macOS keep 35 minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Check out the Poetry and Pipenv lock fixtures byte-exact on Windows

The Windows test leg's CRLF checkout (core.autocrlf) turned the LF-committed
tests/fixtures/poetry, pipenv and pipenv-shapes locks into CRLF, so the
byte-exact upstream-restore round trips (and their derived CRLF variants,
which became \r\r\n) and the Poetry VEX pin-spelling test failed on
Windows only. Mark them -text like the other captured-lock fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* WS5: one VendoredBackend for vendored apply/revert/repair; cut repair's ledger rebuild (#283)

* Consolidate vendored apply/revert/repair into one VendoredBackend

vendor, scan/get --mode vendored, vendor --revert, rollback's vendored
leg, remove and repair now go through one VendoredBackend { apply,
revert, repair } over the shared engine (vendor_records_reusing,
dispatch_revert_one_opts). The boxed_* scan shims collapse into one
boxed_vendor_step; the engine future stays boxed inside apply for the
Windows 1 MiB main-thread stack.

repair no longer re-synthesizes vendor ledger entries from lockfiles. A
lockfile reference with no ledger entry fails with vendor_ledger_missing
(artifact-level event: uuid + details.{ecosystem,path}); the remedy is
restoring state.json from version control. Missing or corrupt artifacts
are re-vendored through the same engine as vendor, so the patch
service's prebuilt artifact is downloaded first under --vendor-source
auto, with the local build as the fallback. The fingerprint post-verify,
set-aside of corrupt bytes and carried-inventory refresh are kept.

Removed with the rebuild: repair_vendor.rs, gem Gemfile wiring
reconstruction, and registry_fetch::fetch_npm_unverified. The packing
code (npm_pack, pypi_wheel, berry_zip, registry_fetch, prestage) stays:
depscan does not call it (verified against depscan master 784013d6), but
it is the CLI's own --vendor-source build/auto fallback.

Tests for the reconstruction path are replaced by vendor_ledger_missing
pins per flavor; CLI_CONTRACT, README, CHANGELOG and the v5 plan are
updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

* Point the vlt coverage map at the renamed repair test

vlt-coverage.json still named vlt_repair_reconstructs_the_ledger_from_the_lock,
and vendor_vlt_lock_out_of_sync lost the only assertion the coverage
check could see when the lock-only reference test switched to
vendor_ledger_missing. vendor_vlt_out_of_sync now asserts the refusal
detail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

* Drop the ledger-less mirror leg from the bun binary workspace test

native_binary_alias_and_transitive still expected repair to rebuild a
workspace mirror after deleting state.json. Repair now reports that as
vendor_ledger_missing (pinned in native_binary_hosted_vendored_takeover_roundtrip),
so the leg is gone; the missing/corrupt mirror legs keep running and
assert the ledger stays byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

* Pass --mode agent to the gated agent-mode get fixtures

get defaults to hosted since 5e5f5ed, which moved the agent-mode
fixtures to --mode agent but missed the #[ignore]d real-toolchain
suites (e2e_vlt, e2e_npm, e2e_pypi, e2e_gem, e2e_safety_pnpm). Their
plain `get <uuid>` now redirects instead of applying in place, so e.g.
vlt_pinned_matrix_agent_get_and_remove saw the copy Absent. This PR
touches the vlt-compatibility path filter, which surfaced it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

* Keep an unpersistable inventory refresh loud, drop reconstruction persist tests

A carried-inventory refresh whose ledger write fails now reports
vendor_inventory_refreshed next to vendor_state_write_failed and keeps
the member-verified rebuild on disk, instead of falling through to
vendor_artifact_rebuild_failed and removing it. The persist-failure
tests for the removed backfill / anchored / soft reconstruction paths
go with them; they only run as non-root, which is why the root sandbox
skipped them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

* Restore the macOS-only PatchedFixture hash fields

The dead-field clippy fix removed before_hash/after_hash, but the macOS
immutable-flag rollback tests read them, so test (macos-latest) no
longer compiled. Keep the fields and allow dead_code off macOS only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

* Keep repair on the recorded artifact identity

Review of #283 found two regressions in the shared-engine repair:

- A corrupt artifact was moved aside before staging, so its
  afterHash-verified members were no longer harvested: an offline repair
  that the previous implementation completed failed with "no local
  source". The members are now harvested first and passed as the seed.
- The post-verify reloaded the ledger the re-vendor had just written, so a
  service archive with different bytes (same members, new gzip mtime) was
  committed as `rebuilt` with a rewired lock and new fingerprint. Repair
  now verifies against the original entry; when the result is not the
  recorded artifact, that candidate's wiring files and ledger entry are
  put back from a pre-run snapshot, and a service copy falls back to a
  build-only rebuild. Legitimate backend migrations of a verified rebuild
  (the cargo version retag) are kept.

Both reproductions are pinned in repair_vendor_e2e.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

* Make repair's wiring undo FIFO-safe, atomic and symlink-preserving

Bugbot on #283: the identity-undo snapshot read lockfiles with bare
tokio::fs::read (a FIFO at a wiring path blocks open(2)) and skipped
symlinked lockfiles, and the put-back wrote them in place (no
stage+fsync+rename, mode bits dropped).

The snapshot now reads through read_regular_to_bytes and records a
symlinked lockfile's link text; the undo re-links a link that the
engine's rename replaced, then writes the target through
atomic_write_bytes_preserving_mode. Pinned by
repair_identity_undo_follows_a_symlinked_lockfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

---------

Co-authored-by: Claude <noreply@anthropic.com>

* v5 fix: unblock the e2e tier on the v5 base (#288)

* Cancel only superseded PR runs in CI

A CI or compatibility run is now cancelled only when a newer push to
the same pull request replaces it. Push, dispatch and scheduled runs
always finish, so a manually dispatched run on the v5 base branch (its
only CI verdict, since push CI runs on main alone) is no longer killed
by a later dispatch or by the non-main cancel rule, and main keeps
finishing its rust-cache saves.

CI now groups PR runs by PR number, like the compatibility workflows
already do.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp

* Expect native path separators in scan headers

scan_hosted_paths_run_once_per_project_directory compared the
per-directory `== apps/a ==` header against a literal forward-slash
path, but scan prints the directory glob matched, which Windows
spells `apps\a`. The Windows test leg failed on this since 62f07c7,
and because every e2e job waits on `test`, the whole e2e tier was
skipped on v5 PRs. Build the expected header from path components.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp

* Run the vlt agent get test in agent mode

get defaults to hosted mode since 5e5f5ed, so the real-vlt
get_and_remove leg ran a hosted get and found the installed copy
unpatched (Absent, expected Patched). Pass --mode agent, as the other
agent-mode fixtures already do. Same change as cc5f1b6 on #285; it
blocked the e2e tier's e2e_vlt jobs now that they run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp

* Run the pnpm safety e2e gets in agent mode

get defaults to hosted mode since 5e5f5ed, so the three pnpm safety
tests ran a hosted redirect and found proj_a's installed copy
unpatched and no pnpm-layout note. They test the in-place apply
path, so pass --mode agent. These e2e-tier tests had not run since
that change because a red base test skipped the tier.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp

* Let the NuGet hosted e2e run without .socket/

v5 hosted mode writes no `.socket/` directory (the lock pins are the
whole hosted state), so the hosted leg's fresh_checkout panicked with
NotFound copying a tree that no longer exists. Copy it only when the
run left one; the vendored leg still carries its ledger through.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Add the NuGet vendoring design (docs only) (#285)

* Drop unused fields from the rollback covgap fixture

`cargo clippy --all-targets -- -D warnings` failed on the dead
before_hash/after_hash fields of PatchedFixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R11RZRvYFL3fzmEnkFAU4A

* Add an opt-in NuGet fallback vendoring layout

Setting SOCKET_PATCH_NUGET_LAYOUT=fallback vendors a patched NuGet
package under a Socket-only version V' (the upstream version plus a 4th
part derived from the patch uuid). The package is committed already
extracted, as a NuGet fallback package folder under
.socket/vendor/nuget/<uuid>/. It is wired through a generated
socket-patch.targets file…
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.

2 participants