vlt 1.4.0 reads workspaces from pnpm-workspace.yaml, so the #942 / #1134 member refusals miss those members: hosted and vendored scans write a stray member lock and exit 0, and vendored VEX attests not_affected #1564
Description
[agent] Found by the scheduled vlt bug-hunt routine (ledger #307).
Summary
vlt 1.4.0 (published 2026-10-14) adds a third workspace source. vlt reads vlt.json workspaces, then the root package.json workspaces, and if neither is present it now reads the packages: list of pnpm-workspace.yaml (resolveWSConfig in vlt's src/workspaces; its new error text says "...the vlt.json config, the "workspaces" field of the root package.json, or the "packages" list of pnpm-workspace.yaml..."). socket-patch's vlt member checks only read the first two (vlt_workspace_root / package_json_workspace_root in hosted/governing_root.rs). As a result, a member that vlt 1.4.0 installs from the root's vlt-lock.json isn't recognised as a member:
- Stray member lock (Hosted and vendored scans from a vlt workspace member with a stray vlt-lock.json write to that lock, which vlt ignores, and exit 0 (vendored VEX then attests not_affected while
vlt cifails) #1134 shape): ifpackages/a/vlt-lock.jsonexists, hosted and vendoredscanrun frompackages/aboth rewrite that lock, which vlt never reads, and exit 0 withsuccess. vlt then installs pristine bytes. After the vendored run, the rootvlt cifails with "Lockfile is out of sync with package.json", because the member'spackage.jsonwas rewritten tofile:. Vendoredvexthen attestsnot_affected/inline_mitigations_already_existwhilerequire('left-pad')loads the pristine copy. - No member lock (Hosted scan/get run from a vlt workspace member reports success while pinning nothing: the #598 / #901 member refusal has no vlt.json case #942 shape): hosted
scanfrom the member exits 0 withstatus: success, pins nothing (oneskipped/redirect_unconfirmedevent plus aredirect_npm_no_lockfilewarning) and doesn't give theredirect_workspace_lockfile_elsewhererefusal that the same layout gets when the globs are invlt.json.
Impact
A pnpm-to-vlt migration that keeps pnpm-workspace.yaml is exactly the layout vlt 1.4 now supports. In that layout, socket-patch reports success, nothing patched gets installed, and the vendored VEX is a false not_affected attestation.
Repro (vlt 1.4.0, main 7b6371c, Linux)
Use the mock registry and patch API from #1494 (PORT=18555 node mock.mjs & PORT=18556 node mock.mjs &), with SOCKET_API_URL / SOCKET_PATCH_SERVER_URL=http://127.0.0.1:18556, SOCKET_NPM_REGISTRY=http://127.0.0.1:18555, SOCKET_ORG_SLUG=test-org and SOCKET_API_TOKEN=fake.
# a canonical single-project lock for the stray copy
mkdir solo && cd solo
echo '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > package.json
echo '{"config":{"registries":{"npm":"http://127.0.0.1:18555/"}}}' > vlt.json
vlt install --allow-scripts :scripts && cd ..
mkdir -p ws/packages/a && cd ws
echo '{"name":"root","version":"1.0.0","private":true}' > package.json
echo '{"config":{"registries":{"npm":"http://127.0.0.1:18555/"}}}' > vlt.json # no "workspaces"
printf 'packages:\n - "packages/*"\n' > pnpm-workspace.yaml
echo '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > packages/a/package.json
vlt install --allow-scripts :scripts
grep workspace vlt-lock.json # "workspace~packages+a left-pad": "prod 1.3.0 ~npm~left-pad@1.3.0"
cp ../solo/vlt-lock.json packages/a/ # stray member lock (omit for the #942 shape)
cd packages/a
socket-patch scan --mode vendored --yes --json # exit 0, success; rewrites packages/a/{package.json,vlt-lock.json}
cd ../.. && vlt ci --allow-scripts :scripts # exit 1: Lockfile is out of sync with package.json
cd packages/a && node -e "console.log(require('left-pad'))" # pristine
socket-patch vex --output vex.json # exit 0; statement not_affected / inline_mitigations_already_existWith --mode hosted instead, the scan exits 0, rewrittenFiles: ["vlt-lock.json"] (the member's), the root vlt ci exits 0 and installs pristine bytes, and vex exits 1.
Expected vs actual
- Expected: the same refusals as for a
vlt.json- orpackage.json-listed member. Hosted scan/get run from a vlt workspace member reports success while pinning nothing: the #598 / #901 member refusal has no vlt.json case #942 / Hosted and vendored scans from a vlt workspace member with a stray vlt-lock.json write to that lock, which vlt ignores, and exit 0 (vendored VEX then attests not_affected whilevlt cifails) #1134 / Hosted and vendored scans from a vlt workspace member with a stray vlt-lock.json still write to that lock and exit 0 when the root's workspaces glob uses an extglob (packages/+(a|b), packages/@(a|b)); vendored VEX then attests not_affected #1542 refuse from a member that vlt installs from the root's lock (redirect_workspace_lockfile_elsewherehosted,vendor_lockfile_missingvendored), andCLI_CONTRACT.mdrequires VEXnot_affectedonly for a patch that is actually installed. - Actual: exit 0
success, writes to a lock that vlt ignores, and vendored VEX gives a falsenot_affected.
OS × version
Each cell was run twice.
| OS | vlt | layout | hosted | vendored |
|---|---|---|---|---|
| Linux | 1.4.0 | pnpm-workspace.yaml + stray member lock |
fail (2/2) | fail (2/2), false VEX |
| Linux | 1.4.0 | pnpm-workspace.yaml, no member lock |
fail: exit 0, pins nothing (2/2) | pass (vendor_lockfile_missing) |
| Linux | 1.4.0 | same globs in vlt.json workspaces (control) |
pass (redirect_workspace_lockfile_elsewhere) |
pass |
| Linux | 1.3.8 | pnpm-workspace.yaml + stray member lock |
pass: vlt 1.3.8 ignores pnpm-workspace.yaml (the lock has no workspace~packages+a edge), so packages/a is its own project and writing its lock is correct |
pass (same) |
First bad vlt release: 1.4.0. This isn't a socket-patch regression; the cause is vlt's new workspace source. macOS and Windows weren't probed (this routine's probe branches are blocked). The fix will probably need to be gated on vlt's version or the lock: since 1.3.8 must keep treating the member as its own project, the root lock's workspace~<member> edges could decide, as vlt_lists_member already does for the ! dialect split.
Suspect code
crates/socket-patch-core/src/hosted/governing_root.rs:755vlt_workspace_root: reads onlyvlt.jsonworkspaces.crates/socket-patch-core/src/hosted/governing_root.rs:556: thepackage.jsonfallback addsvlt-lock.jsononly for apackage.jsonworkspacesroot. vlt 1.4's third fallback (pnpm-workspace.yamlpackages:whenpackage.jsonhas noworkspaces) isn't modelled.crates/socket-patch-core/src/hosted/governing_root.rs:106-111(member_stray_lock_refusal/nearer_root) andcrates/socket-patch-core/src/vendor/npm_flavor.rs:412(member_stray_lock_refusal, which the vendored engine uses) inherit the gap.docs/testing/vlt-compatibility.md: the Formats and Releases tables stop at 1.3.8.
Activity
mikolalysenko commentedon Oct 15, 2026
[agent] Triaged: priority:p1 (vlt, npm family). Confirmed on main
e76cb9b:vlt_workspace_root(crates/socket-patch-core/src/hosted/governing_root.rs:755) reads member globs only from an ancestorvlt.jsonworkspaces, and thepackage_json_workspace_rootfallback (line 520) only frompackage.jsonworkspaces; nothing models vlt 1.4's third source, thepackages:list ofpnpm-workspace.yaml. Bothmember_stray_lock_refusalpaths (hosted andvendor/npm_flavor.rs) inherit the gap. Not a duplicate: #942 / #1134 / #1542 cover thevlt.jsonandpackage.jsonsources. As the report notes, the fix must stay version-aware, since vlt 1.3.8 ignorespnpm-workspace.yaml(the root lock'sworkspace~<member>edges can decide).Generated by Claude Code
mikolalysenko commentedon Oct 15, 2026
[agent] Claiming this issue (shared root cause: vlt member detection ignores vlt 1.4's pnpm-workspace.yaml
packages:workspace source). Branch: agent/fix-vlt-pnpm-workspace-members. Claim-ID: 2026-10-15T10:21:02Z-837352Generated by Claude Code
mikolalysenko commentedon Oct 15, 2026
[agent] Draft fix PR: #1574
Generated by Claude Code
Test vlt 1.4 pnpm-workspace.yaml members (#1564)
Refuse vlt 1.4 pnpm-workspace.yaml members
mikolalysenko commentedon Oct 15, 2026
[agent] Verified fixed on main
4cc50f4(#1574) with real vlt 1.4.0, 2× per cell. From apnpm-workspace.yamlmember, hosted refusesredirect_workspace_lockfile_elsewhereand vendored refusesvendor_lockfile_missing, with and without a stray member lock. Nothing is written, and membervexhas nothing to attest.Refusals match vlt 1.4.0's own reading across 27 yaml shapes: BOM, CRLF, block / flow / multi-line flow / column-0 items, quoted key, single quotes,
./,**, braces, extglob, trailing/, comments,---/...markers, multi-doc, plus!exclusions (excluded dirs are not refused). Non-members vlt rejects (anchors, tabs, duplicate key, escapes) are not refused either. Also checked:package.jsonworkspaces([],{nohoist}, a listing pattern, a non-listing one) taking precedence over the yaml. Root scans of such a workspace pass in hosted, vendored and agent mode (scan →vlt cipatched, vexnot_affected, re-run, byte-exact rollback).One residue, filed separately as #1584: on vlt 1.3.8 a yaml-listed directory isn't its own project either (vlt's root lookup goes to the nearest ancestor
vlt.json), so the 1.3.8 branch still writes a stray member lock with exit 0.Generated by Claude Code