Skip to content

Fix vlt stray locks under a vlt.json root (#1584) - #1658

Merged
Mikola Lysenko (mikolalysenko) merged 6 commits into
mainfrom
agent/fix-vlt-project-root-stray-lock
Oct 22, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 6 commits into
mainfrom
agent/fix-vlt-project-root-stray-lock

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

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

Fixes #1584

What it does

A scan from a directory that vlt never treats as its own project no
longer rewrites that directory's stray vlt-lock.json. Hosted mode
refuses with redirect_workspace_lockfile_elsewhere, vendored mode with
vendor_lockfile_missing, and vex reads the ignored lock as absent.
Before this change all three exited 0, and VEX attested not_affected
while the install stayed unpatched.

Root cause

vlt installs whatever project find("project") in @vltpkg/vlt-json
returns, and Config sets projectRoot = dirname(find(...)). I checked
that this code is identical in 1.3.8 and 1.4.0. The walk goes up from the
cwd and works like this:

  • it returns the nearest ancestor that holds a vlt.json;
  • otherwise it keeps the outermost directory that holds a
    package.json or node_modules;
  • it ends after a directory that holds .git, and never reads $HOME.

None of that depends on a workspaces field.
member_stray_lock (hosted/governing_root.rs) only treated a vlt
lock as stray when an ancestor listed the directory as a workspace
member. That missed three cases from the issue:

  • an unlisted nested directory;
  • a member listed through a .. glob;
  • the vlt 1.3.8 pnpm-workspace.yaml directory.

Fix

  • New vlt_project_root mirrors vlt's walk, canonicalized the way
    Node's cwd is. member_stray_lock uses it for the vlt family:
    • When the walk lands on the directory itself (for example a
      submodule .git), the lock is vlt's own and is never stray.
    • When the root is another directory holding vlt-lock.json, the
      lock is stray.
    • When that root is nearer than the listing workspace root (a
      config-only vlt.json in between), the nearer root is the one
      named.
    • As with listed members, a root that was never installed (no
      vlt-lock.json) governs nothing.
  • The refusal names the root to run from, calls it the "vlt project
    root", and says a vlt.json in the directory makes it its own
    project. The hosted, vendored and VEX wrappers all share this text.
  • The .. glob shapes are covered without normalizing globs: the root
    vlt.json is the nearest one, so the project-root rule refuses them.
  • I updated CLI_CONTRACT.md's redirect_workspace_lockfile_elsewhere
    row to describe the new case.

Changed expectations in existing tests

Several tests asserted the old premise: that a directory a vlt root
does not list, or excludes with !, owns its vlt-lock.json. vlt's
source and the issue's repros (2/2 runs, 1.3.8 and 1.4.0) show that
premise is false. These tests now expect the lock to be stray:

  • only_the_locks_whose_manager_lists_the_member_govern_it
  • vlt_extglob_member_is_refused (packages/c)
  • vlt_negated_member_reads_the_root_lock, which still checks the
    listing logic on a lockless member
  • vlt_pnpm_workspace_yaml_reads_the_root_lock

The #1564 VEX fixture gains a root package.json. Without one, vlt
from packages/a really does install packages/a, so the fixture
could not have produced its root lock.

Test evidence

Issue shape Test
unlisted dir under vlt.json (hosted) vlt_project_root_governs_unlisted_directories
.. / ./.. / */.. globs same, 3 cases
no vlt.json, outermost package.json same
vlt 1.3.8 pnpm-workspace.yaml dir same + vlt_pnpm_workspace_yaml_reads_the_root_lock
vendored refusal and takeover preflight bun_and_vlt_members_with_stray_lock_are_refused (4 new cases)
VEX does not attest a_member_lock_its_manager_ignores_attests_nothing (2 new cases)
walk bounds (.git, $HOME, node_modules) vlt_project_root_walk_bounds
submodule .git / nearer vlt.json (review) vlt_project_root_overrides_the_listing_root

Red to green: with the fallback disabled, 6 tests fail
(vlt_project_root_governs_unlisted_directories,
bun_and_vlt_members_with_stray_lock_are_refused and the 4 updated
ones). With the fix, all 91 governing_root / npm_flavor / stray tests
pass.

Local runs:

  • cargo fmt --all -- --check and
    cargo clippy --workspace --all-features -- -D warnings are clean.
  • cargo test -p socket-patch-core --lib --all-features: 6568 passed,
    4 failed. The 4 failures are chmod-based permission-denial tests
    (copy_tree, vlt_heal, pypi_poetry, pypi_requirements) that can't fail
    in this sandbox, which runs as root. They don't touch this diff.
  • CLI suites in_process_scan, e2e_vlt, e2e_safety_vlt and
    mode_migration_vlt pass. in_process_redirect has 143 passing and
    3 root-only read-only-permission failures.
  • cargo test --workspace couldn't link everything within this
    session's disk allowance (30 GB of debug test binaries). The real-vlt
    *_vlt_build suites couldn't reach npmjs from the sandbox's
    test-harness HTTP client. CI covers both.

Review notes not acted on

  • /code-review high pointed out that vlt_project_root uses blocking
    exists() calls. I kept them, since member_stray_lock already
    checks existence synchronously, and the walk stops at .git.
  • It also noted that the walk is unbounded outside $HOME with no
    .git. That is vlt's own behavior. The walk test anchors itself with
    a .git so it stays deterministic.

Follow-ups (not in this PR)

  • A lockless directory under a vlt root that doesn't list it still goes
    through the listing-only vlt_workspace_refusal.
  • A directory under a vlt root that was never installed is still not
    refused, which matches the existing listed-member rule.

Note

Medium Risk
Changes governing-root detection for vlt layouts and fails closed on more scan/vendor/VEX paths; behavior is well-tested but affects npm-family refusal logic.

Overview
Treats nested vlt-lock.json as stray when vlt installs from a parent project root, fixing false success and incorrect VEX attestation when running from directories vlt does not treat as their own project (#1584).

member_stray_lock now mirrors vlt’s find("project") via vlt_project_root: nearest ancestor vlt.json, else outermost package.json/node_modules, stopping at .git and below $HOME. Any directory without its own vlt.json whose lock vlt would ignore is refused with redirect_workspace_lockfile_elsewhere (hosted), vendor_lockfile_missing (vendored), and VEX treats the lock as absent—including unlisted paths, .. globs, and vlt 1.3.8 pnpm-workspace.yaml layouts, not only explicit workspace members.

Refusal text names the correct directory to run from as either workspace root or vlt project root (MemberStrayLock::root_kind), shared across hosted, vendored, and VEX. CLI_CONTRACT.md documents the vlt #1584 case.

Reviewed by Cursor Bugbot for commit 20e3ae5. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
vlt installs whatever project its root lookup finds: the nearest
ancestor vlt.json, else the outermost directory with a package.json or
node_modules (stopping at .git, never reading $HOME). A directory with
no vlt.json of its own is never a project of its own, whether or not a
workspaces field lists it.

socket-patch only treated a nested vlt-lock.json as stray when the
directory was a listed workspace member. Scans from an unlisted nested
directory, a member listed through a `..` glob, or a vlt 1.3.8
pnpm-workspace.yaml directory rewrote a lock vlt never reads, exited 0,
and VEX attested not_affected while the install stayed unpatched.

Hosted and vendored scans now refuse such a directory
(redirect_workspace_lockfile_elsewhere / vendor_lockfile_missing) and
name the root to run from, and VEX reads the ignored lock as absent.
As for listed members, a root that was never installed (no
vlt-lock.json) governs nothing.

Fixes #1584

Assisted-by: Claude Code:claude-opus-5-5
Describe in CLI_CONTRACT.md the #1584 case of the workspace
refusal: a directory with no vlt.json of its own whose vlt project
root is another, installed directory.

Assisted-by: Claude Code:claude-opus-5-5
Follow-ups from review of the #1584 fix:

- A listed vlt member holding its own .git (a submodule) is its own
  vlt project, so its lock is no longer refused as stray.
- When a vlt.json sits between the member and the listing workspace
  root, the refusal now names that nearer root, which is the one vlt
  installs, and says "vlt project root" instead of "workspace root".
- The walk resolves symlinks and `..` like vlt's own cwd does.
- The message names only the vlt lock as ignored.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 21, 2026 17:23
@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.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/hosted/governing_root.rs
The vlt project-root walk compared canonicalized ancestors against the
raw HOME / USERPROFILE value. On Windows canonical paths carry a
`\\?\` prefix, and on any OS a symlinked or `..`-spelled home never
matched, so the walk could climb into the home directory and pick
the wrong vlt project root. Resolve the home path the same way.

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 20e3ae5. Configure here.

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

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: labeled Ready for review at 20e3ae5.

  • CI: 35/35 green, 19 skipped on the current head.
  • Mergeable with main (5 commits behind, no conflicts).
  • Bugbot: reviewed 20e3ae5; its one thread (governing_root.rs) is resolved, nothing open.
  • Reviewers: the vlt project-root walk in crates/socket-patch-core/src/hosted/governing_root.rs, especially the HOME/USERPROFILE stop on canonicalized paths (Windows).

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does. vlt installs its project root (find("project"): nearest ancestor vlt.json, else the outermost package.json/node_modules dir, stopping at .git and below $HOME), whatever workspaces lists. Before this, a vlt-lock.json in an unlisted nested dir was treated as the dir's own and rewritten (exit 0, VEX not_affected, install unpatched). Now hosted refuses with redirect_workspace_lockfile_elsewhere, vendored with vendor_lockfile_missing, and vex reads the lock as absent.

Risk: medium. Small, focused code change (4 files), but it widens a refusal: any vlt project with no vlt.json of its own that sits below another directory holding vlt-lock.json is now refused. That matches vlt's own behavior per the PR's source analysis, and an uninstalled root (no vlt-lock.json) still governs nothing, so false refusals need a real stray lock above.

Look here

  • governing_root.rs:410-430 — project root vs. listing root: the project root wins only when it is strictly below the listing root.
  • governing_root.rs:522 — vlt_project_root, the walk mirroring vlt (home and .git bounds, canonicalized like the sibling walks).
  • governing_root.rs:705 — refusal text now says "workspace root" or "vlt project root" (same in npm_flavor.rs:419 and vex/discover/mod.rs:1480).
  • governing_root.rs:3201-3341 — new tests; three older tests flip expectations (listed in the PR body), which is the main thing to agree with.

Verified

  • Read the full diff (4 files, +416/−37) and the PR body; the description matches the code. CHANGELOG.md untouched. No debug leftovers.
  • .git added to member_stray_lock_own_files is correct: VEX's overlay must know the check now reads <dir>/.git.
  • Ran cargo test -p socket-patch-core --lib for hosted::governing_root, vendor::npm_flavor, vex::discover::member_stray_lock_tests: 89/89 pass. (vlt_heal::an_unremovable_hidden_lock_keeps_every_store_entry fails locally only because my sandbox runs as root; the file is untouched and CI is green.)
  • CI on 20e3ae5: 35 success, 19 skipped, 0 failed; ci-ok and clippy green. Cursor Bugbot: success; its one thread (home-dir canonicalization) was fixed and resolved.

Changes I made: none.

Open questions (non-blocking): when there is no vlt.json and the project root is an outer package.json above a listing package.json workspaces root, the refusal names the inner listing root, while vlt would install the outer one. The refusal is still right; only the "run from" hint could point one level too low. That is rare enough to leave for a follow-up if anyone cares.

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


Generated by Claude Code

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 22, 2026
Resolve the CLI_CONTRACT.md conflict: keep main's Bun/vlt dot-dir
glob text (#1631) and re-apply this branch's #1584 project-root
sentence.

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

Copy link
Copy Markdown
Collaborator Author

[final reviewer] Re-added to the merge queue. It was removed at 17:15 UTC; main was then merged in at dadf23b (CLI_CONTRACT.md conflict only), CI is green on dadf23b and the approval still stands.


Generated by Claude Code

Merged via the queue into main with commit 85596dc Oct 22, 2026
53 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-vlt-project-root-stray-lock branch October 22, 2026 19:31
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