Skip to content

Fix vlt 1.4 pnpm-workspace.yaml members (#1564) - #1574

Merged
Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-vlt-pnpm-workspace-members
Oct 15, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-vlt-pnpm-workspace-members

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

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

Fixes #1564

Summary

From vlt 1.4.0, a workspace root can list its members in pnpm-workspace.yaml packages:. vlt reads that list only when neither vlt.json nor package.json has a workspaces field, and it installs those members from the root's vlt-lock.json. socket-patch now treats such a member like a vlt.json- or package.json-listed one:

  • Hosted from the member: refused with redirect_workspace_lockfile_elsewhere, naming pnpm-workspace.yaml and the root to run from. This applies with or without a stray member lock.
  • Vendored from a member with a stray vlt-lock.json: refused with vendor_lockfile_missing. Nothing is written.
  • VEX from that member: its stray lock is read as absent, so nothing is attested not_affected.

Before this change, every one of these runs exited 0. The vendored run's VEX attested not_affected while vlt installed the pristine package.

Root cause

hosted/governing_root.rs vlt_workspace_root read member globs only from an ancestor vlt.json, and package_json_workspace_root covered the package.json fallback. Nothing modelled vlt 1.4's third source. The hosted refusal, the vendored member_stray_lock_refusal (vendor/npm_flavor.rs) and VEX discovery (ignore_member_stray_locks) all go through vlt_workspace_root / member_stray_lock, so all three missed it.

Fix

vlt_workspace_root now asks vlt_workspace_groups for each ancestor's patterns. That function mirrors vlt 1.4's resolveWSConfig:

  1. vlt.json workspaces if the field exists.
  2. Otherwise, if package.json has no workspaces field (vlt keys on the field existing, whatever its value), the pnpm-workspace.yaml packages:, minus . / ./ root entries, as long as a positive pattern remains.

The patterns are globbed with WorkspacesDialect::VLT. vlt 1.4's splitNegatedPatterns is the same as 1.3's, so !packages/b excludes b.

Version gate. vlt 1.3.8 and older ignore pnpm-workspace.yaml, so there a listed directory is its own project, and writing its lock is correct (the issue's control row). Both versions write the same lock format, so the root lock decides. vlt 1.4 records a workspace~<dir> edge from every member it installed that has a dependency. A lock with an edge from a packages:-listed directory is therefore 1.4's (vlt_lock_reads_pnpm_workspace). A lock with no such edge is older vlt's (or every member has no dependency, so there's nothing to patch). A lock that can't be read keeps the refusing side, as vlt_lists_member already does for #1542. A pnpm-workspace.yaml root with no vlt-lock.json isn't shown to be vlt's, so the walk carries on to an outer vlt.json root.

I checked the edge rule against real vlt (public registry, root pnpm-workspace.yaml listing packages/* and !packages/b, both members depending on left-pad):

  • vlt 1.4.0 vlt install → edges ["workspace~packages+a left-pad"]
  • vlt 1.3.8 vlt install → edges []

docs/ecosystems.md vlt notes gain a short "Workspace members" paragraph. docs/testing/vlt-compatibility.md still lists 1.3.8 as the newest supported release. Adding 1.4.0 to the real-vlt CI matrix is a separate change.

Tests (red → green)

The first commit (84c671a) adds only tests. All four failed on main.

Issue shape Test Red on main
#1564 hosted, no member lock (#942 shape) and stray member lock (#1134 shape), root with and without vlt.json hosted::governing_root::tests::vlt_pnpm_workspace_yaml_member_is_refused yes, not refused
#1564 version gate and precedence: 1.4 lock refuses; 1.3.8 lock, empty lock, never installed, package.json workspaces (incl. []), vlt.json workspaces, ! in the yaml and unlisted dir don't; unreadable lock refuses hosted::governing_root::tests::vlt_pnpm_workspace_yaml_reads_the_root_lock yes, 1.4 lock not refused
#1564 vendored stray lock → vendor_lockfile_missing, nothing written vendor::npm_flavor::tests::bun_and_vlt_members_with_stray_lock_are_refused (new case) yes, vendor_lock_entry_not_found
#1564 VEX: stray member lock attests nothing vex::discover::member_stray_lock_tests::a_member_lock_its_manager_ignores_attests_nothing (new case) yes, pin attested
Review finding: uninstalled inner pnpm-workspace.yaml doesn't hide the outer vlt.json root hosted::governing_root::tests::vlt_uninstalled_pnpm_workspace_yaml_walks_on passes on main; failed on the first fix commit

Commands run locally (Linux, as root):

  • cargo fmt --all -- --check: ok
  • cargo clippy --workspace --all-features -- -D warnings: ok
  • cargo test --workspace --all-features --lib: 7,435 passed. The 4 failures are environment-only write-failure tests that can't fail as root (vlt_heal::an_unremovable_hidden_lock_keeps_every_store_entry, copy_tree::relax_loop_must_not_traverse_symlinked_root, pypi_poetry::wire_write_failure_maps_error_and_leaves_lock_untouched, pypi_requirements::wire_failure_rolls_back_already_written_files). They fail identically on main.
  • Hermetic suites in_process_redirect, in_process_vendor, in_process_rollback_hosted, in_process_vendor_bun_takeover, e2e_vex, e2e_vex_redirect, redirect_golden, vlt_locks: all pass except 3 root-only write-failure tests in in_process_redirect (partial_lockfile_write_failure_exits_1_and_writes_no_ledger, redirect_json_mode_write_failures_emit_error_envelope, vlt::scan_redirect_vlt_heal_invalidation_failure_warns), which also fail on main as root. The sandbox disk can't hold every test binary of the full cargo test --workspace, so CI runs the rest (non-root).
  • Real-vlt edge check as above (vlt 1.4.0 and 1.3.8). The real-vlt CI capstones pin releases ≤ 1.3.8, so a 1.4-only e2e leg would always skip in CI. I didn't add one.
  • Wrappers (npm/, pypi/, gem/) only dispatch to the binary and need no change.

/code-review high: findings not fixed

🤖 Generated with Claude Code

https://claude.ai/code/session_01P3dvgTvNF8SCp2PAoebdaA


Note

Medium Risk
Changes governing-root detection for npm-family workspaces (hosted, vendored, VEX); wrong classification could refuse valid runs or allow silent no-ops, but behavior is heavily tested and mirrors vlt 1.4 precedence.

Overview
vlt 1.4 can treat pnpm-workspace.yaml packages: as the workspace member list when neither vlt.json nor package.json defines workspaces. This PR teaches socket-patch to recognize those members so it no longer pins or vendors a member lock while vlt installs from the root vlt-lock.json.

Hosted runs from such a member now fail closed with redirect_workspace_lockfile_elsewhere, naming pnpm-workspace.yaml and the root. Vendored runs with a stray member vlt-lock.json get vendor_lockfile_missing. VEX treats the stray lock as absent (no false not_affected). Logic lives in vlt_workspace_groups / vlt_workspace_root, with precedence matching vlt (vlt.json → package.json field presence → yaml) and a lock edge gate so vlt ≤1.3 (which ignores the yaml) still allows patching the member unless the root lock shows vlt 1.4 installed from listed dirs.

Docs add a short vlt “Workspace members” note; tests cover refusal, version/precedence edge cases, and nested uninstalled inner yaml.

Reviewed by Cursor Bugbot for commit 3608d88. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
vlt 1.4 installs a member listed by the root pnpm-workspace.yaml
packages: from the root vlt-lock.json when neither vlt.json nor
package.json declares workspaces. These tests show hosted, vendored
and VEX runs from such a member treating it as its own project, and
pin the vlt 1.3.8 and precedence controls that must stay as they are.

Assisted-by: Claude Code:claude-opus-5-5
vlt 1.4 lists workspace members from the root pnpm-workspace.yaml
packages: when neither vlt.json nor package.json declares workspaces,
and installs them from the root vlt-lock.json. socket-patch read only
the first two sources, so hosted and vendored scans from such a member
rewrote a lock vlt never reads and exited 0, and vendored VEX attested
not_affected while vlt installed the unpatched package.

The vlt root lookup now reads the third source. vlt 1.3.8 and older
ignore the file, so its directories count as members only when the
root lock has an edge from one of them, which only vlt 1.4 writes.
Hosted, vendored and VEX runs share the lookup.

Fixes #1564

Assisted-by: Claude Code:claude-opus-5-5
A pnpm-workspace.yaml with no vlt-lock.json beside it, such as a
vendored pnpm sub-monorepo, is not shown to be vlt's, so it no longer
stops the search for an outer vlt.json root that installs the member.
Also shares one helper for vlt's per-group glob check and fixes the
refusal code's doc.

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

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@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.

✅ Bugbot reviewed your changes and found no new issues!

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

Reviewed by Cursor Bugbot for commit 3608d88. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 15, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: labeled Ready for review at head 3608d88.

  • CI: 36/36 run checks green (50 skipped by path filters), no main-wide failures.
  • Bugbot: reviewed 3608d88, no findings; no open review threads.
  • Mergeable, no CHANGELOG.md change.
  • Reviewer focus: the vlt 1.4 version gate in hosted/governing_root.rs (vlt_lock_reads_pnpm_workspace keys on workspace~<dir> lock edges to tell 1.4 locks from 1.3.8 ones).

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does. vlt 1.4 can read a workspace's members from pnpm-workspace.yaml packages: when neither vlt.json nor package.json has a workspaces field. socket-patch now recognises those members, so hosted runs from one are refused (redirect_workspace_lockfile_elsewhere), vendored runs with a stray member lock are refused (vendor_lockfile_missing), and VEX no longer attests not_affected from a lock vlt ignores. Before, all three exited 0 and did nothing useful.

Risk: low–medium. It only adds refusals, and only when the root has a vlt-lock.json whose edges show vlt 1.4 installed a listed directory. vlt ≤1.3.8 locks, never-installed roots, and roots whose vlt.json/package.json declare workspaces behave as before. A misread would refuse a run, not write a wrong lock.

Look here

Verified

  • Read the full diff (4 files, +390/−37): the logic, the vendored and VEX test cases, and the docs paragraph. No CHANGELOG.md change, no debug leftovers, no scope creep. Lock-edge paths are split on / and \\, so they work on Windows.
  • Ran the governing_root, vendored stray-lock and VEX member-stray-lock tests at 3608d88: 45/45 passed.
  • CI: ci-ok and clippy green on the head. Bugbot reviewed 3608d88 with no findings, and there are no open review threads.

Changes I made: none.

Open questions: none blocking. vlt 1.4.0 isn't in the real-vlt CI matrix yet; the PR says that is a separate change.

Auto-merge is armed (squash), so approving sends this straight to the merge queue.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants