You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit d129c70
Browse filesBrowse the repository at this point in the historyBrowse files
Never read or rewrite project files outside the git repository root (#2025)
* Claim only this project's vendor paths when rewriting wiring
`parse_vendor_path` found the `.socket/vendor/` anchor anywhere in a
string, so a lock entry naming a sibling checkout's store
(`file:../.socket/vendor/npm/<uuid>/x.tgz`) read as ours to every
vendored writer, reverter and GC, while discovery's `vendor_ref` already
required a root-anchored path. With the same patch vendored in both
checkouts, `vendor --revert` restored our recorded original over the
other project's wiring.
The ownership parse is now root-anchored and shared with discovery
(`vendor_rest_from`): only `.`/`..` may precede the anchor and they must
land exactly on the project root (`parse_vendor_path_from` for a file
below the root). Yarn berry locators cut their `name@` first
(`parse_vendor_locator`); pnpm <= 8 keeps its machine-absolute specifier
(`parse_vendor_path_or_machine_absolute`), which pnpm itself writes.
Readers that only ask "is this a vendored spec" (the lock inventory,
composer/pnpm registry listings, the hosted-wiring probe, the reference
scan behind repair and the orphan sweep) use
`recognize_vendor_path_anywhere`, which nothing rewrites by.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Never read project files outside the repository root
One shared bound (`utils::repo_root::ReadBoundary`): the git checkout
enclosing the project, else the project itself; `..` and symlinked
levels are resolved before the check. `bounded_project_path` refuses a
climbing or absolute project-relative path as `NotFound` (a reader sees
a missing file), and `is_refused_read` lets a writer refuse instead.
- go.work: an ancestor's or an absolute `GOWORK` outside the root is
only stat'ed; the workspace is Invalid, so writers refuse and
discovery withholds. `use` members and `replace` directories outside
the root read as missing (the selection stays Unknown).
- ProjectView reads (discovery, lock inventory, requirements walks,
yarn file:/portal:/link: copies) and the hosted restore's staged
reads are bounded; the requirements planner and the lock flavors'
out-of-root pin check refuse `pypi_requirements_outside_root`, the
restore refuses with the reason.
- NuGet parent configs above the root are not read; vendoring refuses
and hosted mode skips the dep with
`redirect_nuget_inherited_config_outside_root` (CLI_CONTRACT.md).
Fixtures that model files beside the project inside a checkout now
create the `.git` they assumed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bound upward workspace and settings lookups to the repository root
Inside a git checkout, the walks that search a project's ancestors for
a workspace root or a settings file never read above the repository
root (`repo_root::upward_bound`). When a file the walk would read sits
above it (stat only, never in the home directory or above), the tool may
honor it, so writers refuse with the existing workspace codes and
readers withhold: npm/yarn/Bun/pnpm/vlt governing roots and stray member
locks, the governing pnpm-workspace.yaml (and its gitBranchLockfile and
restore settings), uv workspace roots and uv settings, the cargo
workspace root, `.cargo/config` chain and `[patch]` paths, `.yarnrc.yml`
linker/store/env files, Berry registry settings, yarn classic rc files
and linked members, and Maven/Gradle/sbt build roots. Hosted cargo
refuses with the new `redirect_cargo_config_outside_root`. Without a
checkout every lookup behaves as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Make the repository-root bound a workspace-only mode
Owner clarification: the bound applies to workspace runs only; a global
run (`--global` / `-g`, `--global-prefix`) must read anywhere on the
system. `ReadScope { Workspace, Global }` is chosen once from the CLI
flags (`GlobalArgs::read_scope`, `CrawlerOptions::read_scope`) and
passed to every bounded read; `ReadBoundary` is now
`Workspace { root } | Unbounded`, and `ReadBoundary::of`,
`upward_bound`, `upward_readable` and `bounded_project_path` take the
scope explicitly instead of inferring it.
Global-reachable paths take the run's scope: the cargo crawler's source
replacement and `config_layer_paths`, agent-mode cargo build-cache
invalidation from apply/rollback, the npm crawler's relocated yarn
store, apply's PnP gate, `YarnEnv`, `go_workspace::resolve_in` and
Gradle `build_roots`. Hosted, vendored, VEX discovery/verify, the lock
inventory and the upstream restore never run globally and pass
`Workspace`. CLI_CONTRACT.md documents the distinction.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Thread the read scope into the npm install-tree lookups
The last lookups without the run's mode now get it, so a global run is
unbounded and a workspace run stays inside the repository root:
- `NpmCrawler` stores its scope (`for_options`, `with_scope`; `new()` is
a workspace crawler). `find_by_purls` and local `node_modules`
discovery pass it to the relocated yarn-store probe; the CLI's npm
crawls and resolvers build the crawler from their `CrawlerOptions`.
- The agent-mode engines (`apply_package_patch`,
`rollback_package_patch`, `rollback_store_copies_holding_patch`,
`gem_ext::apply_gem_package_patch`) take the scope from the CLI and
thread it into the store fan-out (`find_store_peer_variant_copies`)
and the shared-store write guard (`shared_store_of*`).
Test: a yarn project in a git checkout configured by a `.yarnrc.yml`
above it; the global scope sees the relocated store everywhere, the
workspace scope nowhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix the review findings on the repository-root bound
- Resolve paths physically (canonicalize, else component by component),
so `link/../x` climbs from the link's target as the OS does.
- Check every project-file read on its real path, not only climbing or
absolute names: an in-tree name through an outward symlink is refused.
- Agent-mode cargo: `apply` / `apply --check` refuse Cargo patches when a
`.cargo/config(.toml)` above the repository may replace crates.io
(`cargo_config_outside_root`); build-cache invalidation names that
file, whose target dir was not read.
- `with_store_peer_variant_copies` takes the run's scope, so a global
`apply --check` sees the copies a global apply writes.
- go.work / go native vendor / NuGet parent configs use upward-lookup
semantics: without a checkout, as before; drop the test `.git` dirs
that only worked around it.
- An ancestor `pnpm-workspace.yaml` at or above home never governs.
- Drive-absolute pnpm <= 8 specifiers are machine-absolute on every OS.
- `NpmCrawler` keeps one scope source (its own, from its options).
- Look up the ancestor uv config once; compute the go native vendor and
vex node-resolution bounds once per check / package.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refuse out-of-root symlinked hosted targets; keep fixtures in the repo
The repository-root bound made a hosted candidate lock symlinked out of
the root (uv.lock, package-lock.json, vlt-lock.json -> ../shared/) read
as missing on disk, so `scan --mode hosted` exited 0 having silently
skipped a lock the package manager still honors. A disk read the bound
refuses on a symlinked candidate now lands in `symlinked_reads`, and the
guard checks those on every view, so the run refuses with
redirect_symlinked_file_unsupported as before.
Three e2e fixtures relied on reading a file outside any repository:
- yarn-classic vendor symlinked lock: keep the link target inside the
project, as vendor_failure_paths already does;
- yarn-classic `file:../ext/lpdir` copy and the pip `-r ../shared/base.txt`
include: make the temp dir the repository, so the sibling stays outside
the project but inside the repository (the monorepo layout these tests
model). Out-of-repository includes are covered by
pypi_requirements::tests::includes_outside_the_repository_are_never_read.
Co-Authored-By: Claude <noreply@anthropic.com>
* Refuse hosted candidates reached through an outward directory link
Address Bugbot on 8250521: the hosted arm recorded a bound-refused
candidate only when the file itself was a link, so a candidate whose
real path left the repository through a directory link on its path
(`sub -> ../outside/sub`, then `sub/package-lock.json`) still read as
missing and hosted skipped a lock the package manager follows. Every
bound-refused candidate read is now recorded (candidate names never
climb, so only a link can take one out of the root), and the guard
names the directory-link case in its own message under the same code.
Co-Authored-By: Claude <noreply@anthropic.com>
* Revert "Refuse hosted candidates reached through an outward directory link"
This reverts commit 230cba3. Refusing every bound-refused candidate read
also refuses install-state reads such as node_modules/.modules.yaml when
node_modules links out of the repository, which broke the vlt heal tests
(in_process_redirect vlt::scan_redirect_vlt_heal_undeterminable_*,
vlt::scan_redirect_vlt_heal_invalidation_failure_warns). The directory-
link case Bugbot raised needs the refusal limited to rewrite targets
(locks and manifests), not install state; left for a follow-up round.
Co-Authored-By: Claude <noreply@anthropic.com>
* Refuse hosted rewrite targets reached through an outward dir link
Re-applies 230cba3 (reverted in f28eaef because it also refused install
state such as `node_modules/.modules.yaml` behind a linked
`node_modules`, breaking the vlt heal tests), with the refusal limited
to rewrite targets: a bound-refused candidate read counts as a linked
read when the file itself is a link, or when it is not under a
`node_modules/` segment. A lock or manifest whose real path leaves the
repository through a directory link (`sub -> ../outside/sub`, then
`sub/package-lock.json`) is refused by the guard instead of read as
absent while the package manager still follows the link.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Crawl no Cargo sources under a config above the repository root
A project crawl stops the cargo config chain at the git root, so a
.cargo/config.toml above it that replaces crates.io with a directory
source went unread and the crawler fell through to the registry cache.
vex could then verify a patched shared registry copy and emit
not_affected while cargo builds the unpatched vendor directory, and
rollback could restore a copy apply refuses to touch. Fail closed in
source_roots instead, matching apply's cargo_config_outside_root gate.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@@ -903,6 +905,19 @@ socket-patch creates `.socket/` and everything under it itself and never writes
903
905
904
906
Not covered: the agent-mode blob cache and manifest are checked from `.socket` down, so a `.socket` that is itself a link still redirects those writes (deciding that at lock time is deferred).
905
907
908
+
### Reads bounded to the repository root
909
+
910
+
**Workspace runs only.** The bound applies to project-scoped runs (the default `--cwd` project). A global run (`--global` / `-g`, or `--global-prefix`, on any command) reads anywhere on the system, so it can scan global `node_modules`, `site-packages`, `~/.m2`, the cargo and Go caches, and their settings wherever they live: nothing in this section applies to it. The mode is fixed once per run from those flags and passed to every bounded read (core `utils::repo_root::ReadScope`: `Workspace` or `Global`; `ReadBoundary::Workspace { root }` or `ReadBoundary::Unbounded`). It is never inferred from the filesystem. Hosted and vendored mode refuse global runs anyway, and VEX discovery and verification do not run in them.
911
+
912
+
VEX discovery, the lock inventory, hosted mode and vendored mode never read a PROJECT file outside the repository root: the git checkout enclosing the project (the nearest `.git`, by the same rules as the repository lookup), or the project directory itself when there is none. One check (core `utils::repo_root::ReadBoundary`) decides it on the REAL path: every project-file read is checked, an in-tree name included, and a level that resolves outside through `..` or a symlink (a `..` after a link climbs from the link's target, as the OS resolves it) counts as outside. A refused read behaves like a missing file to a reader; a writer refuses with the reason (`… is outside the repository root …; socket-patch does not read project files outside it`). Machine-level tool configuration (the user `~/.npmrc` / `.yarnrc.yml` / `NuGet.Config`, the `GOENV` file, `$CARGO_HOME`, module and package caches) is not a project file and is read as before.
913
+
914
+
- **Go** (an upward lookup: without a checkout, as before): a `go.work` outside the root (an ancestor's, or one an absolute `GOWORK` names) is only noticed, never read: the workspace is invalid, so vendored and hosted writes refuse with the Go workspace refusal and discovery withholds every Go ref. Inside the root, a `use` member or a `replace` directory outside it reads as missing, so the selection it would decide is unknown.
915
+
- **Requirements includes**: a `-r` include outside the root is never read. Discovery and the lock inventory see it as missing; vendored mode refuses `pypi_requirements_outside_root`, since the pins pip installs from it are unknowable.
916
+
- **NuGet parent configs** (an upward lookup: without a checkout, as before): a `nuget.config` in a directory above the root is never read. Vendoring refuses (`… also merges <path>, which is outside the repository root …`) and hosted mode skips each NuGet dep with `redirect_nuget_inherited_config_outside_root`, writing nothing for it.
917
+
- **Relative and absolute references read through the project view** (yarn `file:` / `portal:` / `link:` copies, include chains, any `../` path a lockfile names) and the hosted upstream restore's staged reads: outside the root they read as missing, and the restore refuses.
918
+
- **Upward lookups** (a workspace root or settings file the package manager searches the project's ancestors for): inside a git checkout none reads above the repository root. When one of the files the lookup would read exists above it (a stat, never a read, and never in the home directory or above), the tool may honor it, so writers refuse and readers withhold: a lockless npm-family member or a member's own npm/Bun/vlt lock under a `package.json`, `pnpm-workspace.yaml` or `vlt.json` above (`redirect_workspace_lockfile_elsewhere` in hosted mode, `vendor_lockfile_missing` in vendored mode, unattested in discovery); the nearest `pnpm-workspace.yaml` above (but never one in the home directory or above it) counts as governing (`redirect_pnpm_settings_elsewhere`, `vendor_pnpm_settings_elsewhere`, and a `gitBranchLockfile` that may be on); a uv workspace `pyproject.toml` above (`redirect_workspace_lockfile_elsewhere`, `pypi_uv_workspace_unsupported`) or uv settings above (`pypi_uv_ancestor_settings`; hosted `redirect_uv_project_unsupported` / `redirect_uv_script_unsupported` / `redirect_uv_lock_unsupported`); a `Cargo.toml` above (`cargo_manifest_not_workspace_root`) or a `.cargo/config(.toml)` above (`user_authored_patch_entry` in vendored mode; hosted mode refuses the run with `redirect_cargo_config_outside_root`; agent-mode `apply` and `apply --check` refuse a run with Cargo patches with `cargo_config_outside_root`, naming the file, since it may replace crates.io while apply would patch the registry cache, and the cargo build-cache invalidation warns `cargo_build_cache_stale` that a `build.target-dir` / `build.build-dir` set there was not read); a `.yarnrc.yml` above (its `nodeLinker` reads as unknown, so PnP; no `pnpmStoreFolder` store is admitted; the Berry upstream restore refuses the registry lookup); a yarn classic `.yarnrc` / `.npmrc` above, or a workspace member linked from outside the repository (`redirect_yarn_classic_offline_mirror`, naming the file or member); a `pom.xml`, Gradle settings or `build.sbt` above (`not_build_root`). Gradle settings above are not analysed by the crawler, an `injectEnvironmentFiles` dotenv file outside the repository (other than beside the home rc) injects nothing, a cargo `[patch]` path outside it reads as unknown (`user_authored_patch_entry`), and agent mode's build-cache invalidation stays inside it. Without a git checkout every upward lookup walks on as before.
919
+
- **Vendored ownership**: writers, reverters and the GC claim a lockfile reference only when it is root-anchored into THIS project's `.socket/vendor/` (`./.socket/vendor/…`, or `../` from a file below the root back to exactly the root). A sibling checkout's `../.socket/vendor/<eco>/<uuid>/…`, even for the same patch uuid, is never rewritten, reverted or deleted; the revert leaves it with `vendor_lock_entry_drifted` and keeps the artifact. The one exception is pnpm <= 8, which records override specifiers as machine-absolute paths itself: an absolute `…/.socket/vendor/npm/<uuid>/<leaf>` naming the entry's own uuid and leaf is still the entry's.
920
+
906
921
## Vendor command contract
907
922
908
923
`vendor` converts the hosted patches pinned in the project's lockfiles into committable
0 commit comments