Repository navigation
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
Description
[agent] Found by the scheduled vlt bug-hunt routine (ledger #307).
Summary
#1134 (fixed by #1161) made hosted and vendored runs from a vlt workspace member refuse when the member holds a stray vlt-lock.json that vlt never reads. The member check matches the root's workspaces patterns with WorkspacesDialect::VLT, and that dialect sets extglob: false. vlt itself does expand extglobs: with "workspaces": ["packages/+(a|b)"] or ["packages/@(a|b)"], vlt 1.2.0 and 1.3.8 install packages/a as a member from the root lock. socket-patch doesn't see packages/a as a member, so the #1134 refusal never fires and the original #1134 failure comes back:
- hosted: exits 0 with "Switched 1 package to hosted patches; rewrote 1 file". It pins the member's stray lock, which vlt ignores, so
vlt cifrom the root installs pristine bytes.vexcorrectly refuses here (not_applied). - vendored: exits 0 with "Vendored 1 package". It rewrites the member's
package.jsontofile:./.socket/vendor/..., which makes the root'svlt cifail ("Lockfile is out of sync with package.json"). The installed copy stays pristine, butvexexits 0 and attestsnot_affected.
With the plain glob packages/* in the same shape, both modes refuse (redirect_workspace_lockfile_elsewhere / vendor_lockfile_missing) and write nothing, which is the #1161 behaviour.
Impact
It's a false VEX attestation for a patch that isn't installed, and the vendored write breaks the workspace's next frozen install. The scan reports success in both modes, so a CI step run from the member directory passes.
Repro (vlt 1.3.8, main fc5458e)
Use the mock registry and patch API attached to #1494 (PORT=18555 node mock.mjs & and PORT=18556 node mock.mjs &).
export SOCKET_API_URL=http://127.0.0.1:18556 SOCKET_PATCH_SERVER_URL=http://127.0.0.1:18556 \
SOCKET_NPM_REGISTRY=http://127.0.0.1:18555 SOCKET_ORG_SLUG=test-org SOCKET_API_TOKEN=fake
REG='{"registries":{"npm":"http://127.0.0.1:18555/"}}'
mkdir -p ws/packages/a ws/packages/b && cd ws
echo '{"name":"root","version":"1.0.0","private":true}' > package.json
echo '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > packages/a/package.json
echo '{"name":"b","version":"1.0.0","dependencies":{"right-pad":"1.3.0"}}' > packages/b/package.json
# leave a stray lock in the member (e.g. from a standalone install before the workspace existed)
(cd packages/a && echo "{\"config\":$REG}" > vlt.json && vlt install --allow-scripts :scripts && rm -rf vlt.json node_modules)
echo "{\"workspaces\":[\"packages/+(a|b)\"],\"config\":$REG}" > vlt.json
vlt install --allow-scripts :scripts
jq -r '.edges|keys[]' vlt-lock.json # includes "workspace~packages+a left-pad": vlt treats a as a member
cd packages/a
socket-patch scan --mode vendored --yes # exit 0, "Vendored 1 package."
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
cd packages/a && socket-patch vex --output vex.json && jq '.statements[].status' vex.json # "not_affected", exit 0For hosted, use --mode hosted instead: it exits 0, rewrites packages/a/vlt-lock.json, the root vlt ci installs pristine, and vex exits 1 (not_applied).
Expected vs actual
- Expected: vlt reads the member from the root lock, so the run refuses as 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 / Fix Bun and vlt stray member locks skipping refusal (#1101, #1134) #1161 specify ("vlt installs it from …/vlt-lock.json and ignores its own ./vlt-lock.json";vendor_lockfile_missingfor vendored). It should refuse whatever glob syntax the root uses to list the member.docs/ecosystems.mdand the CLI contract's VEX rows say VEX attests only patches that are actually applied. - Actual: with an extglob pattern, both modes exit 0 and write a lock or
package.jsonthat never takes effect, and vendored VEX attestsnot_affected.
OS × version (Linux, main fc5458e; every cell run 2×)
root workspaces |
where | vlt 1.2.0 | vlt 1.3.8 |
|---|---|---|---|
["packages/+(a|b)"] |
vlt.json |
fail (hosted + vendored; vex not_affected) |
fail (hosted + vendored; vex not_affected) |
["packages/@(a|b)"] |
vlt.json |
fail (vendored) | fail (vendored) |
["packages/+(a|b)"] |
package.json (vlt.json without workspaces) |
n/a: vlt 1.2.0 doesn't list a |
fail (vendored) |
["packages/*"], {a,b}, **, [ab], {"g":"packages/a"} |
vlt.json |
pass (refused) | pass (refused) |
I didn't bisect it: it's a gap in the #1161 matcher, not a regression. macOS and Windows weren't probed because this routine's probe branches are blocked, but the matcher is OS-independent.
Suspect code
crates/socket-patch-core/src/utils/workspace_globs.rs:89:WorkspacesDialect::VLTsetsextglob: false. vlt 1.2.0 and 1.3.8 listpackages/+(a|b)andpackages/@(a|b)(verified withvlt install: both members get edges andnode_modules).crates/socket-patch-core/src/hosted/governing_root.rs:732(vlt_workspace_root, used by the hosted refusal) andmember_stray_lock(used byvendor/npm_flavor.rs:412member_stray_lock_refusal) both go through that dialect.- Related, but fail-closed: vlt applies a
!pattern regardless of order (["!packages/a","packages/*"]excludesa), while the VLT dialect is last-match-wins, so socket-patch countsaas a member there. That only adds a refusal from a directory vlt would install from the root anyway, so it isn't harmful today. It's worth fixing alongside, so the dialect matches what vlt actually lists.
Same class as #1097 (yarn classic extglob) and #1071 (npm braces / classes).
Activity
vlt: run 21 ledger (#1542)
mikolalysenko commentedon Oct 14, 2026
[agent] Triaged: priority:p1 (vlt, npm family). Confirmed on main
306ec81:WorkspacesDialect::VLTincrates/socket-patch-core/src/utils/workspace_globs.rssetsextglob: false, sopackages/+(a|b)/packages/@(a|b)never match the member and the #1161 stray-lock refusal (hosted/governing_root.rsvlt_workspace_root,vendor/npm_flavor.rsmember_stray_lock_refusal) doesn't fire. Not a duplicate: #1097 and #1071 fixed the same gap for the yarn classic and npm dialects only. The fix is to setextglob: trueon the VLT dialect and add the reported negation ordering (vlt excludes a!pattern whatever its position, asNegation::Unordereddoes).Generated by Claude Code
mikolalysenko commentedon Oct 15, 2026
[agent] Claiming this issue (shared root cause: the vlt workspaces dialect disables extglob and orders
!patterns, unlike vlt). Branch: agent/fix-vlt-workspace-extglob. Claim-ID: 2026-10-15T00:20:59Z-393c1cGenerated by Claude Code
mikolalysenko commentedon Oct 15, 2026
[agent] Draft PR: #1555
Generated by Claude Code
Add failing tests for vlt workspace globs
Match vlt's workspace globs in member checks
Test vlt extglob member refusals end to end