Skip to content

Support vlt in hosted, vendored and agent modes - #269

Merged
Mikola Lysenko (mikolalysenko) merged 52 commits into
mainfrom
feat/vlt-support
Sep 27, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 52 commits into
mainfrom
feat/vlt-support

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

What this does

socket-patch now supports the vlt package manager (vlt-lock.json, vlt.json) in every mode and command. vlt purls stay pkg:npm/….

Hosted (scan --mode hosted, get --mode hosted)

  • Every default-registry instance of a patched package is rewritten, including peer and modifier variants. The rewrite changes only two tuple slots: slot 2 becomes the patched sha512 and slot 3 the patch.socket.dev URL.
  • DepIDs, edges, options and vlt.json are left untouched, and the lock is spliced line by line.
  • Supported lock eras: lockfileVersion absent (≤0.0.0-18), 0 (both the ·· and ·npm· default-registry forms) and 1 (rc.15+).
  • CRLF locks are preserved. Custom-registry, scoped-registry, jsr, git, remote and file nodes are never touched.
  • An existing node_modules is healed automatically: the hidden lock and stale .vlt/<DepID> copies are removed so the next vlt install fetches patched bytes.
    • Optional dependencies are never removed. vlt does not reinstall them from the lock, so the command prints an advisory instead.
    • Opt out with --no-vlt-install-cleanup / SOCKET_NO_VLT_INSTALL_CLEANUP.
  • A fail-closed artifact preflight (redirect_vlt_artifact_unverifiable) fetches each artifact the way vlt does. See "Production blocker" below.
  • Rollback, remove, replay, takeover, re-redirect and idempotent re-runs all work, through a new ledger kind redirect_vlt_lock_node.
    • This includes after vlt re-keys a pinned node's peer context (a routine peer bump on vlt ≥ 1.0.8).
    • A rescan collapses server-appended ledger chains into one edit per node.

Vendored (vendor, scan --mode vendored, get --mode vendored)

  • The artifact is a patched directory at .socket/vendor/npm/<uuid>/<name>-<version>/node_modules/<name>/.
    • vlt's file: tarball deps break when reinstalled from a lock. The node_modules/<name> leaf keeps self-referencing packages resolvable.
    • devDependencies are stripped from the vendored package.json; otherwise vlt installs hundreds of dev nodes.
  • Wiring is a package.json file: spec plus lock node and edge surgery. It is byte-stable through real vlt ci, --frozen-lockfile and vlt install <x>.
  • Supported targets: root and workspace-member direct deps, npm aliases, and packages with a single peer context (vlt ≥ 1.0.8 tags those with ~peer.<hash>).
  • Refused with stable codes:
    • packages vlt would build in the committed dir (install scripts or binding.gyp) — vendor_vlt_build_scripts_unsupported;
    • several peer/modifier variants of one name@version;
    • a same-name git, remote or file install elsewhere in the lock.
  • vendor_vlt_reinstall_required tells the user when plain vlt install would keep an already-installed upstream copy (optional deps, re-vendored or rebuilt dirs) and vlt ci is needed.
  • Transitive-only targets are refused (vendor_vlt_transitive_unsupported, which points to hosted mode). vlt silently reverts lock-only rewiring (exit 0) on vlt install <x>, uninstall, update and workspace edits in every current release.
  • Repair, reconstruct, the orphan sweep, the pristine-fetch ladder, service download/verify/reuse and the hosted↔vendored takeovers all handle the vlt flavor.

Agent (apply, rollback, remove, list, setup)

  • The crawler reads vlt's node_modules/.vlt/<DepID>/node_modules/<name> store for every DepID grammar era and patches every copy of name@version.
  • Writes are copy-on-write-safe (atomic rename). vlt 1.2.0 hardlinks from a global store on Linux, and an e2e test proves the store is not corrupted.
  • setup wires the same npx @socketsecurity/socket-patch apply postinstall hook for vlt projects, reading vlt.json workspaces with root-only wiring.
  • vlt_root_scripts_not_run is emitted for vlt < rc.13, which never runs a root postinstall.

VEX

  • Manifest-less VEX attests hosted and vendored vlt patches straight from vlt-lock.json.
  • It withholds attestation when the same lock also installs the package from git, a remote tarball or a local dir. It also withholds when the lock is one that some vlt releases discard (no lockfileVersion, the 0.0.0-16…24 ignored-lock window, rc.7…rc.29 with a scalar registry).

Forward compatibility

  • Replay, per-purl reverts and repair now fail closed on ledger kinds and vendor flavors the running release does not know. Older binaries used to drop the npm record and leave a lock half-redirected.

Production blocker: needs a depscan deploy

vlt cannot install patch.socket.dev artifacts today, on any version.

  • vlt sends accept-encoding: gzip, and the edge re-gzips the .tgz on the fly (content-encoding: gzip, a new body on every request).
  • vlt hashes the raw body, so every install fails EINTEGRITY. npm, pnpm and bun decode Content-Encoding first, which is why they are unaffected.

The server fix (Cache-Control: …, no-transform on archive routes) is a separate depscan PR. Until it deploys:

  • this CLI refuses vlt hosted redirects with redirect_vlt_artifact_unverifiable;
  • the vlt-serve-watchdog.yml workflow is continue-on-error.

Review and CI

  • Review: a final whole-branch adversarial review confirmed 17 findings, each checked by two independent verifiers. All are fixed in this PR with regression tests, including:
    • rollback getting stuck after a vlt peer re-key;
    • a git check-ignore deadlock on packages with about 2,000+ files;
    • VEX attesting over a same-name git/remote/file install;
    • vlt build writing into the vendored dir;
    • repair dropping vlt's dependency links.
  • Sync with main: merged main after Speed up hosted and vendored scans: concurrent API requests, parallel crawl, single-pass rewriters #257 ("Speed up hosted and vendored scans: concurrent API requests, parallel crawl, single-pass rewriters"). vlt was fitted into the new structure with identical output:
    • The vlt rewriter is its own group in the parallel rewriter groups, right after the npm-family group, so edits and warnings keep the serial order. The group-equivalence oracle covers vlt.
    • The parallel crawler walks the .vlt store in both the scan and the level-by-level resolver. The sequential oracle and its random trees include vlt stores.
    • The vendored flow uses Speed up hosted and vendored scans: concurrent API requests, parallel crawl, single-pass rewriters #257's lazy/deferred pristine sources, the download-plan pre-flight (a vlt arm), and the group commit. Under the group commit, a failed vlt-lock.json write is vendor_commit_failed with nothing committed.
    • The redirect goldens are byte-identical.
    • Later merges brought in Add in-memory hosted engine and napi addon #276 (the in-memory hosted engine and napi addon) and Fix four vendored/hosted correctness bugs found while profiling #268. vlt is wired into the in-memory engine: vlt-lock.json is a root marker, and the lock inventory and flavor probe read vlt through ProjectView. The rewrite withholds and confirms like the disk flow, and the ledger rebases vlt edits. The engine has no network for the artifact preflight, so it judges vlt artifacts as --offline does: withheld, never pinned.
  • Follow-up fixes (aaa9e8b, fc8d442):
    • FIFO-safe reads of vlt-lock.json in hosted rollback, the vendored takeover and the repair wiring snapshot (Bugbot), plus setup's vlt member reads.
    • The redirect_vlt_artifact_unverifiable detail no longer leaks the grant token. The URL's token level is spelled <redacted>.
    • The post-rollback vlt heal follows the vlt replay group's own outcome.
    • The artifact preflight moves bodies instead of copying them.
    • A git failure in the vendored ignore check warns vendor_artifact_gitignore_unchecked.
  • CI: green on every workflow, including the full vlt-compatibility.yml version × OS matrix, apart from CodeQL.

Tests

  • Goldens: 68 npm/vlt hosted golden cases shared byte-for-byte with depscan's TS twin, plus a reverse-replay golden.
  • Captured locks: real vlt locks and layouts per era (tests/fixtures/vlt-locks, vlt-trees, vendor/npm/vlt). The vendored lock surgery is checked byte-for-byte against locks that real vlt wrote on 1.2.0, 1.0.10 and rc.14.
  • Real-vlt capstones: e2e_redirect_vlt_build, e2e_vendor_vlt_build, mode_migration_vlt, e2e_safety_vlt and e2e_vlt, run through scripts/check-vlt-legs.py against a leg manifest so that no leg can pass vacuously.
    • Run locally on macOS against vlt 1.2.0.
    • Across eras: hosted on 0.0.0-16, 0.0.0-32, rc.14, rc.32, 1.0.4 and 1.1.1; vendored on 0.0.0-32, rc.14, rc.32 and 1.0.4; an rc.14→1.2.0 upgrade migration; a macOS hardlink leg.
  • CI:
    • ci.yml gains vlt e2e rows on ubuntu/macos/windows.
    • New vlt-compatibility.yml runs a full version × OS matrix plus a production backtest (scripts/backtest-vlt.py).
    • New vlt-serve-watchdog.yml checks the artifact encoding.
  • Docs: docs/ecosystems.md vlt notes, docs/testing/vlt-compatibility.md (with its boundary table), README, CLI_CONTRACT codes and CHANGELOG.

Companion PRs:

  • SocketDev/depscan#26859 (SBOM annotations, the TS rewriter twin, flag-gated patch-PR flows, evals; stacked on depscan#26792) pins this branch's head (re-pinned after each push).
  • SocketDev/depscan#26856 is the serving fix.

Merge order: depscan#26856 (deploy and verify) → this PR → re-pin depscan#26859 to the merged main.

🤖 Generated with Claude Code

Resolve inherited dependency aliases through their workspace definitions
and match local lockfile owners by both package name and version. This
allows valid workspace patches while refusing consumers outside the
editable project.

Read quoted dependency values and annotated registry tables consistently
so repeated application cannot leave an alias unpatched or create an
invalid duplicate TOML table.

Validated with 70 Cargo redirect unit tests and real service-backed
hosted, vendored service, and vendored build installs using fresh Cargo
caches, locked offline builds, integrity rejection, and revert.

Assisted-by: Codex:gpt-6-astra
Validate planned dependency pins against parsed TOML before writing any
files. Legal root and target dotted or inline declarations that the
source-preserving editor cannot rewrite now refuse the entire patch
instead of leaving a mixture of original and patched sources.

Validate unchanged member manifests too, while preserving workspace
inheritance and declarations for other package versions. Malformed TOML
is refused without changing the project.

Validated with 73 Cargo unit tests and the real converter/API/CLI matrix,
including an actual Cargo build proving the dotted manifest is valid
and remains byte-identical after refusal.

Assisted-by: Codex:gpt-6-astra
Combine identical refusal branches for dotted dependencies without
changing which declarations are refused. Full workspace Clippy and all
73 Cargo redirect tests pass.

Assisted-by: Codex:gpt-6-astra
A redirect ledger written by a newer socket-patch (for example a vlt
lock edit) could be half-reverted: rollback dropped the npm record and
left the lockfile it did not understand still pointing at the hosted
artifact, with nothing tracking it.

Now an unknown hosted edit kind holds every record in the ledger, and
rollback of one package, remove and the hosted-to-vendored takeover
refuse with nothing written when that edit names the package. repair
skips vendored npm entries whose flavor it does not know instead of
judging or rebuilding them with the wrong layout rules. The contract
and changelog say vlt ledgers need the release that adds vlt support.

Assisted-by: Claude Code:claude-opus-5-5
The end-of-run hosted-to-vendored reconcile in `vendor` and
`scan --mode vendored` could still drop a redirect record together
with a ledger edit this release does not understand (for example a
vlt-lock.json edit), leaving that lockfile on the hosted URL with
nothing tracking it while reporting the migration as reconciled. It
now leaves such a package in the ledger and prints the manual cleanup
advisory instead.

An unknown edit now only blocks the package it actually names, so an
edit for left-pad or @scope/pad no longer refuses a rollback of pad.
Whole-ledger rollbacks that hit an unknown edit now say to upgrade
socket-patch rather than suggesting a rescan that cannot help. The
changelog and contract now say that such a rollback still unwinds the
lockfiles this release understands while keeping every record.

Assisted-by: Claude Code:claude-opus-5-5
repair and the vendored health check decide whether an npm flavor is
known from a hand-kept list that the revert dispatch did not share, so
a release adding a flavor could revert its entries yet still skip them
in repair as unknown. The known set, the revert routing and the in-use
probe now all come from a single string-to-flavor mapping with
exhaustive matches.

Assisted-by: Claude Code:claude-opus-5-5
Lays the groundwork for vlt support without changing any behavior yet.
socket-patch can now decode vlt's package ids in both lockfile
encodings (vlt up to 1.0.0-rc.14 and every later release), recognize
vlt's default registry the way vlt does, read vlt-lock.json strictly
(a BOM, a non-object or an unknown lockfileVersion is refused, never
parsed around), and read and write its one-entry-per-line node and
edge lines without re-serializing the file.

It also reproduces vlt's own lockfile ordering, pinned by a golden
generated with Node's collator, so later vendored edits leave a lock
that vlt ci keeps byte-identical. Fixture directories that hold tables
rather than projects are kept out of the VEX golden corpus.

Assisted-by: Claude Code:claude-opus-5-5
vlt lock ids now give a package identity to every name npm still
accepts for existing packages, the same rule depscan uses. Scoped
names whose scope or name starts with "." or "_" (such as the
published @_koii/web3.js) and names over 214 characters were
refused before, so hosted redirects, VEX and vendored-copy checks
would have missed them while depscan patched them. Names npm
blocks (node_modules, favicon.ico) and versions too large for npm
to publish no longer get an identity.

A refused lockfileVersion is now reported exactly as it is written
in the lockfile (1e0 rather than 1.0), and tests now pin the
default-registry scheme check and the fixture-corpus skip of the
vlt tables.

Assisted-by: Claude Code:claude-opus-5-5
socket-patch now recognizes a project installed by vlt (the
node_modules/.vlt store or node_modules/.vlt-lock.json) ahead of any
sibling bun, pnpm, yarn or npm marker, and apply prints a vlt layout
note in human mode.

scan, get, apply, rollback and vex now find every package in vlt's
store in every DepID era: transitive-only packages, aliases, git,
remote and file: entries, and workspace members whose node_modules
hold only links. apply and rollback reach every store copy of a
patched package, including vlt's peer and modifier variants, and each
write replaces the file instead of writing through it, so vlt's
machine-wide store stays untouched. The store-copy failure note now
reads "store copy <path> failed to patch" for pnpm and vlt alike.

In a vlt project, --update suggests vlt install
@socketsecurity/socket-patch@latest, and in vlx's cache it suggests
re-running vlx with @latest.

The crawler tests stage real layouts captured from vlt 0.0.0-32,
1.0.0-rc.14, 1.0.10, 1.2.0 and a 1.0.0-rc.22 workspace.

Assisted-by: Claude Code:claude-opus-5-5
When a git, remote or file: dependency in a vlt (or pnpm) store
depended on a patched package, apply and rollback treated that
dependency link as a second installed copy: the run reported an extra
"already patched" or "already original" result, and get, vex, vendor
and repair could pick the link as the package's location. An importer
link into a git or tarball entry was also listed twice.

The resolver now follows the store-entry rule the scan already uses:
inside a store entry only real directories are copies, and a store
copy an importer link already points at keeps the importer path. A
node_modules/.vlt that is a link is never followed, and tests now pin
the hoist-dir skip and the policy that a same-version git copy is
patched as its own copy. Doc comments broken in the previous commit
are rewrapped.

Assisted-by: Claude Code:claude-opus-5-5
The hosted rewriter now understands vlt-lock.json. For each patched
package it points every default-registry node at the Socket-hosted
tarball by splicing only the integrity and URL slots, so the DepID, the
flags, the trailing slots, the edges and the options stay byte-identical
and vlt ci keeps the lock as written. It reads every vlt lock era
(no lockfileVersion, 0 with the legacy ids, 1), refuses a lock vlt
itself could not read (BOM, unknown version, non-canonical layout),
skips named-alias, scoped and jsr registry copies with a warning, and
warns when an old vlt would ignore the lock.

Rollback, remove and the vendored takeover now revert these edits,
including ledgers the depscan PR flow writes: the registry slots go
back on the node even after vlt re-laid the line, and a node vlt has
re-locked away counts as already reverted.

The scan and get commands start reading vlt-lock.json in a later change;
until then this is reachable through the shared golden fixtures and the
revert paths.

Assisted-by: Claude Code:claude-opus-5-5
Rolling back a vlt hosted redirect no longer refuses when vlt re-locked
one peer or modifier variant away while an earlier instance of the same
package still carries the pin. Every surviving instance is restored
first, and only then is a vanished one checked for a leftover hosted
URL, so rollback, remove and the vendored takeover succeed whatever
order the ledger holds.

The "vlt ignores this old lockfile" advisory now fires only for the
empty or registry-URL segments the spec names, so v0 locks that use a
named registry alias no longer get a spurious warning (and a withheld
VEX attestation). A bun.lockb on disk now counts as a sibling lock when
deciding whether vlt drives the install, without passing its bytes as
text.

New tests pin the slot-level revert through both revert entry points,
the install-state sentinel on its own, both advisory alias cases and a
CRLF copy of every captured vlt lock.

Assisted-by: Claude Code:claude-opus-5-5
`scan --mode hosted` and `get --mode hosted` now repoint vlt-lock.json
at Socket's hosted patches: every default-registry node of a patched
package keeps its DepID and gets the patched sha512 and hosted URL,
and vlt drives confirmation when its install state is present or no
other npm-family lock is.

Before anything is written, each artifact is fetched the way vlt
fetches it. A response vlt would reject (re-gzipped, wrong sha512,
HTTP error, unreachable) withholds the package instead of pinning a
lock `vlt ci` cannot install.

vlt never refreshes an installed copy, so after the rewrite socket-patch
removes stale store entries and the hidden lock, and rollback and remove
do the same once the registry pins are back. --no-vlt-install-cleanup
(SOCKET_NO_VLT_INSTALL_CLEANUP) keeps them; the
redirect_vlt_reinstall_required advisory says what to run. A same-run
--vex no longer attests a vlt package whose installed copy is stale or
unchecked, whose lock an older vlt may ignore, or which also resolves
from another registry.

Assisted-by: Claude Code:claude-opus-5-5
A vendored vlt package taken over by `scan --mode hosted` is now
fetched and verified before its vendored state is reverted, dry runs
included. An artifact vlt would reject keeps the package vendored
instead of leaving a lock pinned to bytes nobody checked.

A same-run --vex attests a vlt package only when the heal checked its
installed copy; a pin on a host socket-patch does not own is left to a
later `socket-patch vex`. When vlt-lock.json is withheld beside another
lockfile, its old pin no longer confirms the package, and the warning
says only vlt-lock.json was left unchanged.

The heal keeps every store entry when node_modules/.vlt-lock.json
cannot be removed, so vlt never trusts a hidden lock with dangling
links, and packages that bundle dependencies are no longer
re-invalidated on every run.

Assisted-by: Claude Code:claude-opus-5-5
`socket-patch vendor` now wires projects that install with vlt. A
vlt-lock.json (lockfileVersion 0 or 1) routes npm vendoring to a new
vlt backend ahead of every other lockfile, and a package already
vendored through another lockfile refuses with vendor_flavor_changed.

A direct dependency of the root or a workspace member is committed as
the patched package directory under
.socket/vendor/npm/<uuid>/<name>-<version>/node_modules/<name>/, so
packages that require their own name keep resolving. Its
devDependencies are dropped from the vendored package.json, and the
uuid dir's .gitignore and .gitattributes keep the payload committable
and byte-exact on every checkout. The lock node, its importer edges
and the importers' package.json specs move to the file: path, placed
where vlt itself writes them, so `vlt ci` leaves the lock unchanged;
this is checked against locks real vlt 1.2.0, 1.0.10 and 1.0.0-rc.14
wrote. Transitive targets, peer or modifier variants, foreign
registries, peer edges, multi-field declarations, stale specs,
unreadable locks and git-ignored payloads refuse before any write.

`vendor --revert` restores the registry node, edges and specs while
keeping whatever vlt re-laid since, or leaves everything on drift.
Health checks, reuse and repair judge the directory with its
package.json exemption, and lock inventory reads vlt-lock.json.

Assisted-by: Claude Code:claude-opus-5-5
`vendor --revert` on a vlt project no longer writes a lock with
duplicate keys when `vlt install` has since added a registry copy of the
vendored package beside it: the vendored entries fold into the matching
registry ones, and a mismatching copy is reported as drift with nothing
written and the artifact kept.

Re-vendoring a new patch over a dir whose ledger entry lost its
pre-vendor wiring now refuses with vendor_wiring_unknown and says how to
recover, instead of recording wiring that could never be reverted.

A rebuild of the committed package dir replaces the whole version-level
directory, so stray files beside the package no longer cause an endless
corrupt-then-rebuild loop, and it keeps vlt's dependency links (or says
to run `vlt install` when they had to be discarded). Prebuilt service
trees are pruned of node_modules and bundling packages refuse, as the
local build already did. The git-ignore probe now parses rule sources
that contain a colon, such as Windows drive-letter paths.

New tests pin every revert inverse's already-reverted branch, the
importer allowlist, dry-run revert, the rebuild branch, the reuse-time
.gitignore restore, the gitignored refusal, both bundleDependencies
refusals, the service layout fallback, local dir artifact staging and
the FIFO-safe vlt-lock.json inventory.

Assisted-by: Claude Code:claude-opus-5-5
`socket-patch vex` and every embedded `--vex` now read vlt-lock.json:
a Socket-hosted registry node (patch URL plus sha512, any DepID era)
and a vendored vlt package directory become attestation inputs, so a
vlt project attests its patches without .socket/manifest.json or the
ledgers, and a redirect or vendor ledger stays live only while the vlt
lock still wires it. A lock vlt cannot read (BOM, unknown version)
wires nothing.

Hosted npm packages are now judged by every store variant of their
installed copies (pnpm and vlt peer, modifier and registry-alias
instances), and a vlt package that another registry also resolves at
the same version no longer attests before install. Vendored vlt
directories verify with the package.json exemption, including the
out-of-sync check of the installed link, and setup.manual accepts
`vlt`.

Assisted-by: Claude Code:claude-opus-5-5
A hosted vlt-lock.json node now attests only when its patch URL's
embedded package name and version (scope included) match the node, so
a lock pointing one package at another package's patch URL is no
longer read as patched. A Socket-shaped node that fails these checks
counts against its siblings of the same version: a hosted pin next to
it no longer attests before install, and a vendored directory next to
it is not attested.

A vendored vlt directory checked without its vendor ledger now
verifies a package.json with devDependencies stripped against the
patched blob in .socket/blobs. When that blob is missing, the package
is left out with the new reason vendor_manifest_unverifiable instead
of a misleading hash mismatch.

The warnings for an unreadable vlt-lock.json and for an unattributable
vendored node now describe the actual cause and a fix that works.

Assisted-by: Claude Code:claude-opus-5-5
vendor, scan --mode vendored and get --mode vendored now refuse every
vlt project the vlt backend would refuse (lock version or layout,
transitive, peer or foreign-registry targets, multi-field or
out-of-sync declarations, a flavor switch, bundled or duplicate
devDependencies, a git rule ignoring .socket/) before any patch is
downloaded, anything is written, or a live hosted redirect is reverted
for a takeover; dry runs preview the same codes.

A fresh clone re-vendors from the committed vlt directory artifact,
verified against the ledger inventory, and vlt's own link to that
directory is never used as a pristine source. A hosted to vendored
takeover removes the store copies vlt installed from the hosted pin.
repair finds, rebuilds and reconstructs vlt directory artifacts and
restores their .gitignore and .gitattributes; the human output names
the vlt files to commit and `vlt install`.

rollback now fetches a before-blob that only a pnpm or vlt store peer
variant still needs instead of failing that copy.

Assisted-by: Claude Code:claude-opus-5-5
A root .gitignore rule like `*.json` no longer makes vendored vlt
refuse `vendor_artifact_gitignored`: the preflight now asks git only
about the uuid directory, so it refuses just the rules (such as
`.socket/`) that the directory's own .gitignore cannot override.

Re-vendoring under a newer patch with nothing installed no longer
builds the new artifact from the old patch's committed artifact, for
every npm lockfile flavor. It fetches the pristine package instead,
and `--offline` skips it with the reason.

When vlt's link to the committed directory is the only installed
copy, vendor now names it instead of claiming nothing is installed.
A lost ledger entry fails `vendor_ledger_entry_missing` and points at
`socket-patch repair`.

A hosted to vendored takeover whose vendoring then fails still
removes the store copies vlt installed from the hosted pin, and prints
the reinstall advisory. Before, the redirect record was gone and
nothing could find those copies again.

The rollback before-blob fix is filed under Fixed, with a pnpm store
peer-variant test beside the vlt one. New tests cover the get uuid-path
refusal line, vlt references that only the lock or a workspace
package.json carries, and restoring vlt wiring files after a failed
repair post-verify.

Assisted-by: Claude Code:claude-opus-5-5
`socket-patch setup` now recognizes a vlt project (vlt-lock.json,
vlt.json, node_modules/.vlt-lock.json or a node_modules/.vlt directory
in the project root, ahead of any pnpm marker) and writes npm's npx
hook into its package.json. A vlt workspace, declared in vlt.json or
the legacy vlt-workspaces.json, is wired at the root only, because vlt
runs the root hook once per install. `setup --json` and the setup
telemetry report the package manager as "vlt".

vlt releases before 1.0.0-rc.13 never run a root postinstall, so setup
now warns `vlt_root_scripts_not_run`: definitely when the vlt on PATH
reports such a version (checked with telemetry off and a 5 second
limit), and as a "may" when no usable vlt is found and vlt-lock.json
has lockfileVersion 0 or none. The hook is still written.

The setup-matrix gains vlt single-project and workspace cases, and the
npm test image moves to Node 22 with vlt 1.2.0 installed.

Assisted-by: Claude Code:claude-opus-5-5
`setup` now also warns `vlt_root_scripts_not_run` as a "may" when the
vlt on PATH is recent but the committed vlt-lock.json is one it would
never write: a lock without lockfileVersion, or a v0 lock beside vlt
1.0.0-rc.15 or later (which refuses v0, so an older vlt elsewhere
installs the project). vlt rc.13 and rc.14 write v0 locks themselves
and stay silent.

`setup --remove` now also clears the hooks that earlier socket-patch
releases wrote into vlt workspace members, so a removal that reports
success no longer leaves member hooks running (and keeping npm counted
as set up for VEX). Excluded and clean members are still left alone.

The static check that the setup matrix routes vlt through the npm
round trip now runs in the default test suite instead of only with the
setup-e2e feature.

Assisted-by: Claude Code:claude-opus-5-5
The hosted warm-tree heal removed stale store entries of optional
dependencies too. vlt does not put a removed optional dependency back
on `vlt install` unless the same install also reinstalls a
non-optional package, so the dependency could silently vanish and its
node_modules link dangle (measured on vlt 0.0.0-32 through 1.2.0).

scan/get, rollback/remove and the vendored takeover now leave every
optional copy (lock flags 1 or 3, or unreadable flags) in place. Such
a copy stays out of the run's in-run VEX, and the
redirect_vlt_reinstall_required advisory tells the user to run
`vlt ci` (or delete node_modules and run `vlt install`), noting that
vlt before 1.0.5 installs no optional dependency from the lock of a
project that declares only optional dependencies.

Assisted-by: Claude Code:claude-opus-5-5
When a heal both removed stale vlt copies and kept an optional one, the
redirect_vlt_reinstall_required advisory reported only the optional
copy, so it no longer said node_modules was incomplete or that
`vlt update` drops the redirects. With cleanup skipped, it counted the
optional copies among those a re-run would refresh, which it never
does.

Every advisory now reports removed, skipped and unchecked copies as
before and adds how many optional copies were kept and that `vlt ci`
refreshes them. It also warns that on vlt before 1.0.5, in a project
whose dependencies are all optional, `vlt ci` would drop the installed
copy, so users should upgrade vlt first.

Assisted-by: Claude Code:claude-opus-5-5
vlt users get hosted, vendored and agent-mode patches that are checked
against the real vlt releases they run, not only against hand-written
lockfiles. Five new capstone suites install projects with the vlt under
test and let vlt itself install what socket-patch wrote: fresh-checkout
`vlt ci`, frozen and ordinary installs, tamper and warm-cache cases,
rollback and heal, optional dependencies, registry shapes, takeovers
between modes, the Linux hardlinked global store, the setup hook and
the npm launcher. Two production legs prove the flow against the live
patch service (the hosted one refuses cleanly while the server still
re-encodes artifacts).

Every leg reports whether it ran or why it skipped, and
scripts/check-vlt-legs.py fails a run that skipped something it should
have run, so a CI row can no longer pass without testing anything. The
expected skips come from the release table in
docs/testing/vlt-compatibility.md, which records the vlt behavior
boundaries measured on every supported release.

Assisted-by: Claude Code:claude-opus-5-5
The Windows npx shim passed the package name through to socket-patch,
so every setup-hook leg on Windows would have aborted vlt's install.
It now forwards only the arguments after the package, and a harness
self-test runs the shim.

The capstones now also prove what they claimed but skipped: the
optional-only hosted to vendored takeover runs on every vlt release
that writes a lock, the vendored VEX of a package.json patch without
the vendor ledger fails closed without the patched blob and attests
with it, vendored to hosted takes over through both scan and get, the
heal and rollback legs check that no other store entry changed, and
the hook-failure leg pins exit 1. Two new vendored legs use the real
peer-bearing use-sync-external-store and record where vendored mode
still refuses it, so fixing that refusal flips a visible expectation.

check-vlt-legs.py reads Windows cargo output and fails a run whose
test binary crashed before printing its result line.

Assisted-by: Claude Code:claude-opus-5-5
Pull requests now run the real-vlt capstones on 35 release and OS
cells, each checked against the leg manifest, and the required
hosted-e2e job proves vlt hosted and vendored installs against
production. A new advisory workflow runs every capstone on every vlt
era of Linux, macOS and Windows, backtests the production service
per release, mode and project shape, compares the locks each OS
writes, and nightly tries vlt@latest, watches for new vlt releases
and checks that the published socket-patch never half-reverts a vlt
ledger. A watchdog probes the public patch artifact every six hours
the way vlt fetches it, so re-encoded artifacts are caught early.

vlt releases are installed from a sha512-checked npm pack, and the
docs now cover vlt in every mode: eras, the hosted heal, vendored
package directories, setup and the known vlt limitations. Every vlt
error code is documented in the CLI contract and mapped to the tests
that assert it.

Assisted-by: Claude Code:claude-opus-5-5
The vlt compatibility backtest no longer fails every hosted cell on
0.0.0-16 and 0.0.0-32 while the service re-encodes the artifact: a
blocked cell now owes only its refusal code, since the withheld
dependency never reaches a rewriter. The tampered-lock check accepts
vlt 0.0.0-16's optional-skip failure the way the depscan capture does,
so those cells pass once the serve fix ships.

Vendored cells now also prove the user's existing tree: a plain
`vlt install` and then `vlt ci` must land the vendored bytes. From
0.0.0-30 vlt keeps an installed optional dependency on a plain
install; that is pinned in the boundary table and documented, and
`vlt ci` must still link the patch.

The production e2e retries in CI now actually retry, a narrowed
compatibility dispatch no longer fails its lock diff, and the README
and vlt notes say vendored mode needs a 0.0.0-19 or later lock.

Assisted-by: Claude Code:claude-opus-5-5
From vlt 1.0.8 a root dependency with resolved peers, and from rc.15 a
workspace member's, gets a peer extra on its lock node even with a
single peer context, so vendored mode refused them. Such a node is now
vendored as a file node without the extra, exactly as vlt writes file:
dependencies, with its peer edges moved along; revert restores the
original node. Several instances or modifier variants still refuse.

Vendoring an optional dependency now prints
vendor_vlt_reinstall_required: from vlt 0.0.0-30 a plain vlt install
keeps the old installed copy, so users are told to run vlt ci. The
same advisory names any dependency whose link still points at vlt's
store. Era-A locks with scalar-registry ids now warn
vendor_vlt_legacy_lockfile, and the takeover advisory no longer calls
kept registry copies hosted artifacts.

The byte-stability fixtures now come from real vlt 1.2.0, 1.0.10,
1.0.4, rc.32 and rc.14, checked through vlt ci, warm and cold frozen
installs and vlt install <new>, and the vendored capstone gains legs
for a single peer context and an optional dependency on a warm tree.

Assisted-by: Claude Code:claude-opus-5-5
Reverting a vendored optional vlt dependency left node_modules linked
to the removed vendored directory, and from vlt 0.0.0-30 a plain vlt
install keeps that dangling link, so the dependency silently vanished.
vendor --revert, and the revert a vendored-to-hosted takeover runs, now
print vendor_vlt_reinstall_required telling users to run vlt ci; a
revert whose importer links still point into the vendored directory
says to reinstall too.

A vendoring run whose patch fails to apply no longer prints the
advisory, since nothing was vendored. The fixture oracle now requires
vlt install <new> to keep the vendored node's edges byte-identical on
every release except rc.14, and new tests cover the advisory on dry
runs, workspace members and both takeover directions.

Assisted-by: Claude Code:claude-opus-5-5
Two ledger states left a hosted vlt pin that rollback and remove
refused forever, and the refusal's advice (re-run the scan) could not
fix either one:

- From vlt 1.0.8 a dependency with resolved peers is keyed by its peer
  context, so bumping its peer (`vlt install react@18.3.1`) re-keys the
  pinned node and carries the hosted pin to the new id. Rollback now
  restores the registry pin on every default-registry node of the same
  package that holds exactly the recorded pin, and heals the patched
  copy installed under the new id. A rescan that pins such a node again
  merges into the old ledger edit and keeps its registry entry instead
  of recording the Socket pin as the original.
- The server's hosted PRs append one ledger edit per PR, and a later
  CLI rescan merged into the oldest one, leaving the newer link stale.
  The rescan now folds the whole chain into one edit.

A real-vlt leg pins a peer-keyed package, bumps its peer, rescans and
rolls back on 1.0.8 and later.

Assisted-by: Claude Code:claude-opus-5-5
A vlt project could install the same package twice: from the registry
and from a git, remote-tarball or local-directory dependency (an lp2
alias pointing at left-pad's tarball URL, say). Only the registry copy
is patched, yet hosted and vendored runs reported the package as
patched, and `vex` attested it as not affected while `require('lp2')`
still loaded the unpatched code.

- `vex` no longer attests a vlt package from its lock pin or vendored
  directory when such a node of the same name exists, since vlt records
  no version for it. A remote tarball whose `<name>-<version>.tgz` leaf
  names another version does not count.
- Hosted mode still pins the registry copy but warns
  `redirect_vlt_custom_registry_skipped` and keeps the package out of
  the run's `--vex`.
- Vendored mode refuses the package, as it already refuses a copy from
  another registry.

Assisted-by: Claude Code:claude-opus-5-5
Vendoring a new patch over an installed, already vendored vlt package
removes the old patch's directory, but node_modules still linked into
it, so `require()` failed until the next `vlt install`. The run
reported success with no advisory. It now emits
`vendor_vlt_reinstall_required` naming the links into the replaced
directory and asking for `vlt install` (or `vlt ci`).

Assisted-by: Claude Code:claude-opus-5-5
`repair` moved a corrupt vlt payload aside, rebuilt it, then deleted
the old copy, and vlt's links to the package's own dependencies went
with it: `require('debug')` failed with "Cannot find module 'ms'" while
repair reported success with no warning. Repair now moves those links
back into the rebuilt payload when they are only links.

When a rebuild cannot keep them (the old `node_modules/` held planted
files), vendor and repair emit `vendor_vlt_reinstall_required` asking
for `vlt ci`. The old hint said `vlt install`, which does not re-link
them on any tested vlt release.

The contract and coverage map also document the advisory for links
into the directory a re-vendor replaces.

Assisted-by: Claude Code:claude-opus-5-5
After `.socket/vendor/state.json` is lost in a vendored vlt project,
repair restores the entry without a file fingerprint and told the user
to run `socket-patch vendor` to record one. That run always refused:
the rewired lock no longer holds a registry resolution and the only
installed copy is the vendored dir. For vlt entries the advice now
reads: restore the registry version spec in package.json, run
`vlt install`, then run `socket-patch vendor`, which re-vendors the
package and records the fingerprint (checked with vlt 1.2.0).

Assisted-by: Claude Code:claude-opus-5-5
When a vendored vlt patch changes package.json dependencies,
`vendor_dep_manifest_stale` said `vlt install` would re-resolve them.
It never does: vlt installs the vendored node's recorded edges, so a
dependency the patch adds stayed uninstalled through `vlt install` and
`vlt ci`. The advisory now names `vlt update`, the command that
re-resolves them, and says it resolves the whole project.

After such an update, `vendor --revert` left the edges vlt had added
from the vendored node in the lock, pointing from a node that no
longer existed. Revert now drops them, along with nodes only those
edges kept, so the reverted lock matches the pre-vendor one and stays
byte-stable through `vlt ci` (checked with vlt 1.2.0).

Assisted-by: Claude Code:claude-opus-5-5
vlt builds a registry copy in the untracked `node_modules/.vlt` store
but a `file:` dependency in place. After vendoring a package with an
install script (esbuild's postinstall, a node-gyp addon) the `vlt build`
that vlt asks for rewrote the committed payload: esbuild's 9 KB JS shim
became a 9.7 MB Mach-O binary staged for commit, every later `vendor`
failed on the inventory, and `repair` restored the shim only until the
next build.

Vendored mode now refuses a package that declares a `preinstall`,
`install`, `postinstall` or `prepare` script or ships a `binding.gyp`
(`vendor_vlt_build_scripts_unsupported`, remedy `--mode hosted`), in
the preflight and on every build of the payload.

Assisted-by: Claude Code:claude-opus-5-5
Standalone `socket-patch vex` attested a hosted vlt pin from the lock
alone even when some vlt release ignores that lock and installs the
registry package instead: a lock with no lockfileVersion (vlt >= rc.15
re-resolves it), a pre-v1 legacy-id lock without vlt.json "modifiers",
or a scalar registry the lock cannot use. In-run `scan --vex` already
withheld those deps, so the two disagreed.

Discovery now runs the hosted rewriter's own lock-level conditions (one
shared predicate, reading vlt.json for "modifiers"). In such a lock
every hosted reference keeps no lock pin, so only an installed tree
whose copies verify attests it, and one patched_ref_unattributable
warning names the packages and why.

Assisted-by: Claude Code:claude-opus-5-5
The vendor_vlt_legacy_lockfile warning told every era-A vlt user that a
vendored lock cannot be deleted and re-created, but only vlt 0.0.0-31
through 1.0.0-rc.5 fail that way; 0.0.0-19 through 0.0.0-30 and rc.6
through rc.8 reinstall the vendored directory fine. The optional
dependency advisories likewise blamed every release before 1.0.5,
although vlt 0.0.0-23 and older install such a project from its lock.

The warning, the advisories, CLI_CONTRACT.md, docs/ecosystems.md and
the changelog now name the measured windows (0.0.0-31 ... 1.0.0-rc.5 and
0.0.0-30 ... 1.0.4), matching the compatibility boundary table.

Assisted-by: Claude Code:claude-opus-5-5
CLI_CONTRACT.md said a dep refused by the vlt rewriter is refused for
every lock only while vlt drives, and elsewhere that all four per-dep
vlt codes leave a dep unconfirmed whichever lock drives. Neither matched
the code: a dep the rewriter refuses (missing sha512, unsupported lock
key) is never confirmed, even when package-lock.json already carries its
hosted URL, while a dep vlt-lock.json merely lacks (entry not found or
vendored) is confirmed by a sibling lock when vlt does not drive.

Both passages now state that rule, and two in-process tests pin it.

Assisted-by: Claude Code:claude-opus-5-5
On vlt rc.7 and rc.8, the vendored era-A scalar-registry leg skipped
the frozen-install half of its fresh-checkout check and hard-asserted
that vlt ci re-keys the bystander package from public npm, a vlt quirk
unrelated to socket-patch that was measured on rc.8 only.

The leg now asserts that the vendored node and payload survive vlt ci,
then clears node_modules and requires a frozen install to land the
patch again without changing the lock vlt ci wrote. The bystander
re-key is only logged. Measured on rc.7 and rc.8 (both re-key it to a
`··` id); the boundary table row says so.

Assisted-by: Claude Code:claude-opus-5-5
The test harness hook that waits for vlt's detached children dropped a
child silently after 120 s, so a leg that then saw a half-finished tree
failed with no hint of why. The hook now writes a `vlt-settle:` line to
stderr naming the child it stopped waiting for. The cap can be set with
SOCKET_PATCH_VLT_SETTLE_CAP_MS, which a new test uses to check the line
and that a quick child is still awaited silently.

Assisted-by: Claude Code:claude-opus-5-5
Comment thread crates/socket-patch-cli/tests/e2e_vendor_vlt_build.rs Dismissed
The capture VEX test still expected every hosted pin of every captured
lock to be attested, but discovery now withholds the pins of a lock
some vlt release discards (the 0.0.0-1 and 0.0.0-16 captures have no
lockfileVersion). It now copies each capture's vlt.json beside the
lock and expects those locks to keep their refs unpinned with one
patched_ref_unattributable warning.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review September 26, 2026 22:45

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

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

  • ✅ Fixed: Unguarded vlt lockfile read
    • Replaced bare tokio::fs reads with guarded read_regular_to_string and read_regular_to_bytes from utils::fs to prevent FIFO/device hangs.

Create PR

Or push these changes by commenting:

@cursor push 34d9bcf6a3
Preview (34d9bcf6a3)
diff --git a/crates/socket-patch-cli/src/commands/repair_vendor.rs b/crates/socket-patch-cli/src/commands/repair_vendor.rs
--- a/crates/socket-patch-cli/src/commands/repair_vendor.rs
+++ b/crates/socket-patch-cli/src/commands/repair_vendor.rs
@@ -56,7 +56,7 @@
 use socket_patch_core::crawlers::CrawlerOptions;
 use socket_patch_core::manifest::schema::{PatchManifest, PatchRecord};
 use socket_patch_core::patch::copy_tree::remove_tree;
-use socket_patch_core::utils::fs::read_regular_to_string;
+use socket_patch_core::utils::fs::{read_regular_to_bytes, read_regular_to_string};
 use socket_patch_core::utils::purl::{
     normalize_purl, percent_decode_purl_component, strip_purl_qualifiers,
 };
@@ -1546,7 +1546,7 @@
                 names.extend(vendor::vlt_lock::vlt_importer_package_jsons(&common.cwd).await);
                 for name in names {
                     let p = common.cwd.join(name);
-                    if let Ok(bytes) = tokio::fs::read(&p).await {
+                    if let Ok(bytes) = read_regular_to_bytes(&p).await {
                         snap.push((p, Some(bytes)));
                     }
                 }

diff --git a/crates/socket-patch-cli/src/commands/rollback.rs b/crates/socket-patch-cli/src/commands/rollback.rs
--- a/crates/socket-patch-cli/src/commands/rollback.rs
+++ b/crates/socket-patch-cli/src/commands/rollback.rs
@@ -15,6 +15,7 @@
     VerifyRollbackResult, VerifyRollbackStatus,
 };
 use socket_patch_core::telemetry::{track_patch_rollback_failed, track_patch_rolled_back};
+use socket_patch_core::utils::fs::read_regular_to_string;
 use socket_patch_core::utils::purl::{patch_matches, strip_purl_qualifiers};
 use socket_patch_core::vendor::{save_state, RevertOpts, VendorState, VendorWarning};
 use std::collections::{HashMap, HashSet};
@@ -1054,8 +1055,8 @@
     } else {
         purls.to_vec()
     };
-    let vlt_lock = tokio::fs::read_to_string(
-        common
+    let vlt_lock = read_regular_to_string(
+        &common
             .cwd
             .join(socket_patch_core::constants::npm_family::VLT_LOCK),
     )

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

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

Reviewed by Cursor Bugbot for commit ff2ceb8. Configure here.

Comment thread crates/socket-patch-cli/src/commands/rollback.rs
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Claude (@claude) review

Brings in #257 (concurrent API requests, parallel crawl, single-pass
rewriters) and fits vlt support into its new structure with the same
output bytes, warnings and ledger edits.

- Hosted rewriters: the vlt rewriter is its own group in the parallel
  rewriter groups, right after the npm-family group, so edits and
  warnings keep the serial order. It gets the overrides minus the
  vlt-withheld uuids; vlt_drives is set once after the merge, and the
  group merge carries the vlt uuid sets. The group equivalence oracle
  now includes vlt (and a bun.lockb case). The hosted rewrite runs on
  the blocking pool with the vlt withholding. The confirmation probe
  searches a withheld dep's needles in every final text except
  vlt-lock.json, so the one-pass presence check still skips the stale
  vlt pin.
- Crawler: the `.vlt` store is walked by the new parallel scan (after
  the pnpm stores) and by the level-by-level resolver. Store entries
  carry their vlt- or pnpm-decoded name. Inside a store entry only real
  package dirs match, and a store copy an importer link already
  resolves to is not added again. The peer-variant finder is
  find_store_peer_variant_copies again. The sequential oracle gets the
  same vlt changes, and its random trees now include vlt stores.
- Vendored: vendor_vlt and stage_patch_dir take a PackageSource (lazy
  or deferred pristine sources). The download plan gets a vlt
  pre-flight (flavor change, lock analysis, wiring plan). The committed
  vlt dir artifact is staged in the new missing-purl local rung, only at
  the record's uuid. Links to vlt's own vendored dir are still dropped
  as pristine sources. strict tar extraction is threaded through
  walk_tar_gz. get's view prefetch skips vlt-refused purls.
- Under the group commit, a failed vlt-lock.json write is now
  vendor_commit_failed with nothing committed (the test is updated to
  that contract).

Assisted-by: Claude Code:claude-opus-5-5

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.

[agent] Claude review

This repo has no Claude review workflow, so the @claude review comment got no bot response. I reviewed origin/main...origin/feat/vlt-support at ff2ceb8f by hand. I read the code only and ran nothing. I looked for correctness bugs, grant-token exposure, fail-closed behavior, Windows/Linux differences and tests that pass without testing anything. Four findings, most severe first.

1. (Medium, security) redirect_vlt_artifact_unverifiable puts the full grant-tokenized artifact URL in the warning detail

  • Where: crates/socket-patch-cli/src/commands/scan/hosted/vlt.rs:76-95 (unverifiable_detail) formats vlt would fail to verify {url}: … with dep.artifact_url, which is pushed into Preflight.warnings at :160-169. hosted.rs:2962 then adds it to redirect.warnings[].
  • Scenario: A scan/get --mode hosted on a vlt project fetches an artifact that is served content-encoded, returns non-200, has a mismatched sha512, or the run is --offline. The detail then carries https://patch.socket.dev/patch/npm/…/<grant token>/<uuid>/<leaf>. It is printed to stderr in human mode and into the --json document, so it ends up in CI logs and in any service that captures CLI output. Depscan's hosted-autopatch path is one such consumer, and Bugbot flagged the same leak in depscan's TS preflight twin. The failing cases are the ones that land in logs: while the serve fix is still undeployed, every vlt project hits the content-encoding case.
  • Precedent: the same file already redacts this. hosted.rs:1924 writes detail.replace(&dep.artifact_url, "<hosted artifact>") for python_metadata_unavailable.
  • Fix: Leave the URL out of the detail. The purl and patch uuid identify the artifact without it; if the URL is useful, blank the grant-token segment (grant_token_path_segment(url, uuid) finds it). Keep the depscan twin's wording identical. Tests that pin the current text need updating: hosted/vlt.rs offline_withholds_every_probed_dep_without_a_request and a_vlt_vendored_dep_is_probed_and_withheld_everywhere, and tests/in_process_redirect/vlt.rs:260,337,486,534,565,883. Also tests/e2e_hosted_production.rs:2619-2626, which pulls the URL out of the warning text. It should get the URL from the API reference (or the lock) instead, so it keeps working once the detail stops carrying the URL.
  • Checked and fine: fetch_capped uses e.without_url() for send errors, and reqwest 0.12.28 body-read errors (error::decode) carry no URL, so the probe reason itself does not leak the URL. probe_artifacts sends no Authorization header, and a unit test covers that.

2. (Low, correctness) The rollback/remove vlt heal is decided per purl and per "any group failed", not by whether the vlt pins were actually restored

  • Where: crates/socket-patch-cli/src/commands/rollback.rs:1165-1175.
  • Scenario: An unscoped rollback on a project with both package-lock.json and vlt-lock.json, where one npm purl's per-purl revert fails. For example, its package-lock.json entry drifted. revert_npm_redirect_purl errors without writing anything, so that purl's redirect_vlt_lock_node edits are still in the ledger. The whole-ledger replay then restores them through Inverse::VltSlots and commits the vlt group, while the npm group refuses (PerPurlOnly) and pushes group:npm. The filter drops that purl's targets: it is not in out.reverted, and any(group:) is true. So vlt-lock.json is back on the registry pin, but node_modules/.vlt/<DepID> and the hidden lock still hold the patched copy. vlt install will not refresh it, and no redirect_vlt_reinstall_required advisory is printed. The reverse can also happen: the replay_eligible clause can heal, and count as "restored registry pins for N packages", a purl whose revert failed, as long as no group refused.
  • Fix: Decide eligibility per target from the result. Re-read vlt-lock.json after the reverts and heal a LedgerTarget when its DepID (or its carried id) now holds the recorded original slots. At minimum, check group:vlt instead of any group:.

3. (Low, efficiency) The artifact preflight keeps every verified artifact body in memory, twice

  • Where: hosted/vlt.rs:138-153 and core/src/patch/redirect/vlt_preflight.rs:37-38,131-137.
  • Scenario: Each probe keeps its full body, capped at MAX_VENDOR_PACKAGE_BYTES = 256 MiB. artifact_preflight then clones every passing body into Preflight.artifacts while probes still holds the originals. The map lives until the end of the hosted run, but only the heal's no-record fallback reads it, via artifact_check, and only when the dep has no record with files. On a wet run the records are fetched, so the bodies are usually never used. Peak memory is about 2x the sum of all patched npm artifacts on every vlt run.
  • Fix: Move bodies out of probes instead of cloning them (into_iter or remove). Keep a body only when the heal may need it, or keep just the sha512 and let the heal refetch in the rare no-record case.

4. (Low, fail-open) The vendor_artifact_gitignored probe treats any git failure as "not ignored"

  • Where: crates/socket-patch-core/src/vendor/npm_dir.rs:435-465 (gitignored), called from npm_dir.rs:661 and vlt_lock.rs:765.
  • Scenario: In a CI container where the checkout is owned by another uid, git rev-parse exits 128 with "detected dubious ownership" (stderr goes to null). inside is empty and the probe returns None. The same happens after the 30 s timeout, or when check-ignore fails. vendor then goes ahead with no signal at all. If .socket/ really is ignored there, the vendored file: directory never gets committed and a fresh checkout's vlt ci fails. This probe is the guard meant to prevent exactly that.
  • Fix: Keep "git absent" and "not a work tree" as silent passes. For a failed or timed-out probe, emit a non-fatal warning (e.g. vendor_artifact_gitignore_unchecked with the git exit status) so the user knows the check did not run.

Checked, no finding

  • vlt_heal invalidation: DepIDs go through is_safe_dep_id before any path join. The store must be a real directory inside the canonical root. remove_link / remove_dir_all do not follow junctions or symlinks, and there are Windows tests for both link shapes. Optional nodes are never removed. When the hidden lock cannot be removed, every entry is kept.
  • Hosted rewriter: it splices only slots [2]/[3]. It refuses when any instance line is outside the grammar, re-parses the whole lock as a residual gate, and preserves \r and commas. Slot-wise revert fails closed on drift, and a vanished DepID is resolved only after the rest of the group is staged.
  • Confirmation: refused_vlt_uuids is checked before the vlt-drives / binary-bun rules, and a lock withheld from the vlt rewrite is kept out of the confirmation texts.
  • VEX: discarding-lock codes, foreign or outside-registry instances, and stale or undeterminable heal results all keep the lock pin away from the attestation. vlt_installed_copy_matches only drives the out-of-sync disclosure, never the verdict.
  • Setup's vlt --version probe resolves only through absolute PATH entries, never the cwd.
  • Test vacuity: the real-vlt legs cannot skip silently. CI=true or a set _JS makes a missing toolchain fail, and check-vlt-legs.py fails on 0 passed, a missing ran or an unexpected skip. The production hosted leg accepts either outcome (clean refusal, or install proof) on purpose, so it does not go vacuous when the serve fix deploys.

A FIFO or device at vlt-lock.json could block `rollback` forever: it
read the lock with a plain read before any revert. `repair`'s wiring
snapshot, the vendored takeover and `setup`'s vlt member check did the
same. All of them now use the FIFO-safe regular-file reader. New tests
cover the rollback and the repair snapshot.

The `redirect_vlt_artifact_unverifiable` detail quoted the full hosted
artifact URL, including the org's grant token, on stderr and in
--json. The token level (the path segment before the patch uuid) is
now spelled `<redacted>`; host, uuid and leaf stay. The production
e2e gets the URL to probe from the public reference endpoint instead
of parsing it out of the warning.

After an unscoped hosted rollback, removing the stale vlt store copies
waited for every replay group to succeed. Now it follows the vlt group
alone, so a drifted package-lock.json no longer leaves copies behind
that the restored vlt-lock.json no longer names.

Smaller fixes:
- The artifact preflight moves verified bodies instead of copying
  them, so each body is held once.
- A git failure during the vendored ignore check now warns
  `vendor_artifact_gitignore_unchecked` instead of passing silently.
- Two test messages no longer print patch uuids (CodeQL
  cleartext-logging).

Assisted-by: Claude Code:claude-opus-5-5
CodeQL still flagged the vlt vendor test helper `read()`: its panic
printed the relative fixture path, and those paths carry the patch
uuid. The panic now names only the I/O error. The envelope dump in
`failure()` goes back to how it was, because the alert was never about
it.

Assisted-by: Claude Code:claude-opus-5-5
Brings in #276 (the in-memory hosted engine and its napi addon) and
wires vlt into it:

- vlt-lock.json is a project-root marker.
- The lock inventory and the in-memory flavor probe read vlt through
  the new ProjectView.
- The in-memory rewrite passes bun.lockb presence and the vlt
  withholding to the rewriters, and confirms vlt deps the way the disk
  flow does.
- The in-memory ledger merge rebases vlt edits like the disk merge.
- The engine has no network for the vlt artifact preflight, so it
  judges every in-scope vlt artifact as --offline does: the dep is
  withheld (redirect_vlt_artifact_unverifiable), never pinned. The disk
  and memory preflights share one judge. The node_modules/.vlt-lock.json
  entry is only an install-state sentinel, as on disk.

The lock-inventory conflicts are resolved onto #276's ProjectView
readers, and REBASE_KINDS keeps the vlt kind now that it is shared.

Assisted-by: Claude Code:claude-opus-5-5
Brings in #268 (four vendored/hosted correctness fixes found while
profiling). The only overlap with vlt is repair: #268 moved the
no-local-source reporting into report_no_local_source, and that now
passes the whole vendor entry to soft_restore_without_fingerprint, so
the vlt-specific remedy text survives. The CHANGELOG, CLI_CONTRACT and
lock-inventory test conflicts keep both sides.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit b32711f into main Sep 27, 2026
106 of 107 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the feat/vlt-support branch September 27, 2026 15:23
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Sep 27, 2026
Resolve the scan/hosted.rs next-steps conflict by keeping both sides:
the Composer reinstall hint from this branch and main's vlt ci line
(#269). Update the vlt next-steps test for the edits argument.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Sep 27, 2026
Adopt depscan's final shared vectors (4ecd8543d3) byte for byte. A date
release (6+ digit major) now drops its trailing .0 parts from its
identity, since SBOM padding erases whether the lock said X, X.0 or
X.0.0. Versions Composer rejects key into their own space, matching
depscan's composerVersionIdentityKey, so purl identity keys cannot
collide with a normalized one.

Also point the drop_superseded_purl composer test at the real
redirect_composer_dist edit kind: main's unknown-kind guard (#269)
correctly refuses the made-up kind it used.

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

3 participants