Skip to content

WIP: feat(install): resolve dependencies from local disk (salvage feat/local-project-registry) - #457

Merged
the1mills merged 7 commits into
mainfrom
feat/local-project-registry-salvage-20260918
Sep 30, 2026
Merged

the1mills merged 7 commits into
mainfrom
feat/local-project-registry-salvage-20260918

Conversation

@ORESoftware

Copy link
Copy Markdown
Contributor

Part of #378 — the "look on local disk for deps/packages" half of the requirement (the github.com half merged in #379).

Salvages feat/local-project-registry (3 commits from 2026-08-23, unmerged for ~3 weeks) onto current main with a real merge, no rebase.

What it adds

A machine-wide index of local project checkouts that zed install can resolve from, so a dependency that already exists as a directory on this filesystem needs no network at all:

  • zed local register|list|unregister|scan|prune (src/local_cli.rs, src/local_registry.rs)
  • index at <ZED_PKG_HOME>/local-registry/index.json, schema zed.local-registry.v1
  • opt-in modes via --local-registry / ZED_PKG_LOCAL_REGISTRY (auto / only / off)
  • registration is per path, not per package name: several checkouts of one package are legitimate, so a name-level tie that cannot be broken deterministically is a hard error, never a coin flip
  • resolution is live (the manifest on disk wins; the recorded version is a drift-detection snapshot), and entries whose path vanished, whose manifest stopped parsing, or whose identity changed are reported unhealthy and never selected

State: WIP — 2 known failures, wiring incomplete

main refactored install resolution while this branch sat (#456, local_overrides), so the salvaged gate is no longer consulted on every path:

  • local_registry_off_ignores_registrations_entirely — --local-registry off still resolves locally instead of consulting the remote registry
  • local_registry_only_names_the_dependency_it_cannot_satisfy — --local-registry only falls through to the file registry instead of failing with the local-only diagnostic

Everything else passes: 653 lib tests, 16 adversarial, 15/17 CLI, plus portability and vs-remote suites compile and run. Conflicts resolved: src/lib.rs (keep local_cli + local_registry alongside main's local_overrides), and the salvaged fixtures gained main's new mirrors / signatures / signing_keys metadata fields.

Relationship to #381

#381 (DEN-3488-local-dev-repo-race) implements a different, automatic design: bounded discovery of checkouts under ~/codes, ~/code, ~/src … for non-frozen symlink installs. It is currently unmergeable (dirty) with 66 failing checks. This PR is the opt-in, explicitly registered counterpart that also covers copy mode. They are complementary, but the two resolution paths should be reconciled before both land.

Next

  • Re-wire the gate into main's current resolver so the two failing tests pass.
  • Then an offline end-to-end: register a checkout, install with the registry pointed at an unreachable URL and ZED_PKG_SOURCE_FALLBACK=0.

🤖 Generated with Claude Code

ORESoftware and others added 7 commits August 23, 2026 19:29
… registry

`zed install` could only ever ask the configured registry where a package
lives. That is the wrong answer while the registry itself is being built,
on an offline or air-gapped machine, and whenever the dependency is a
checkout sitting next to the project being edited. Workspace members solve
this inside one tree; nothing solved it across trees.

Add `zed local`, a shared index of on-disk projects under `ZED_PKG_HOME`
that installs consult before the network.

Entries are keyed by canonical filesystem path, not by package name. One
package legitimately exists in several checkouts at once — a release clone,
a feature worktree, a bisect tree — and a name-keyed registry would have to
silently pick one. Several registrations of one package are therefore
normal; `--priority` chooses between them, and a tie on both priority and
version is a hard error naming both paths rather than a coin flip.

Resolution is live: the recorded version is a registration-time snapshot for
reporting and drift detection, while the manifest on disk is authoritative
on every install. Selected projects are materialized through the same
source-link path as workspace members, so symlink mode writes an absolute
canonical directory symlink into the checkout (edits are visible to
consumers with no reinstall) and copy mode produces a standalone tree via
the existing guarded copier. Transitive dependencies of a linked project are
read from that checkout's manifest, so a whole local graph links at once.

`--local-registry` sets how much authority registrations have: `off`, `auto`
(default; ordinary installs only), `prefer` (also `--frozen`), or `only`
(fully offline — an unsatisfiable dependency errors instead of reaching the
network). `--frozen` opts out by default because the index is machine-global
ambient state and letting it override a pin would make "frozen" mean
something different on every laptop. Requirements satisfied from live source
are exempt from the frozen lockfile-pin check, exactly as workspace members
already are.

Hardening. Registration refuses a missing path, a non-directory, a missing
or symlinked `.zpkg.toml`, a manifest that fails to validate, a non-UTF-8
path, and anything inside (or containing) the Zed home directory — the store
holds extracted artifacts, and registering one would feed materialized
output back in as source. The index must be a regular non-symlink file under
8 MiB with a known schema, at most 4096 entries, each slug-valid, versioned,
absolute, and unique by path; a duplicate is reported rather than silently
collapsed. Every read-modify-write runs under an exclusive zed-lock guard
and is staged-then-renamed at 0600 under a 0700 directory, so concurrent
`zed local register` runs cannot lose one another's entry. `zed local scan`
is depth- and visit-bounded, follows no symlinks, and never descends into
dependency, build, or VCS trees. At install time an entry whose path,
manifest, or identity broke is reported by path and skipped; one that
overlaps the consuming project in either direction is refused before it can
put `zed_modules/<org>/<name>` inside its own link target; and a package
already pinned to a registry artifact is never switched to a source link
behind the version-conflict check.

Tests cover the index unit-level, the CLI end to end against an empty
`file://` registry (so any install that succeeds did so locally), and the
adversarial surface: 16 concurrent registering processes, hand-corrupted and
symlinked index files, oversized indexes, symlinked manifests, symlinked
project spellings, ambiguous ties, and overlapping registrations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fvi78rjurJXbT9XYJv72by
…containers

The local project registry assumed every registered checkout lives on the
system disk and keeps the same absolute path forever. Neither holds where it
is most useful: a checkout on an external SSD, a bind mount inside a container,
or a BuildKit mount that exists for one `docker build` step.

* Volume classification. Registration records where the checkout lives
  (`fixed`, `removable`, `network`, `container-mount`) from
  `/proc/self/mountinfo` where readable and from the conventional mount layout
  (`/Volumes/<label>`, `/run/media/<user>/<label>`, ...) everywhere else, which
  is what makes this work on macOS.

* An `unavailable` health state. An unmounted volume is no longer reported as a
  directory that was deleted. `zed local prune` keeps such entries -- an
  external drive that happened not to be attached this afternoon is not a
  mistake to clean up -- while a genuinely missing directory is still dropped.
  Neither fails an install; resolution falls through to the registry with a
  reason, unless `--local-registry=only` forbids that.

* Link policy. A checkout on removable, network, or container-mount storage is
  copied into `zed_modules/` even in symlink mode: a link into media that can
  vanish is a project that breaks when it does, far from the install that
  caused it. Linking stays allowed when the consuming project shares the volume
  -- nothing outlives it either way. `zed local register --link`,
  `--local-link-policy`, and `--local-ephemeral` override, most specific last.

* Path mapping. `--local-path-map /host/codes=/work`
  (`ZED_PKG_LOCAL_REGISTRY_PATH_MAP`) translates registered paths across a bind
  mount, in both directions: entries are rewritten into the local view on load
  and back into host form on save, so one index shared through a volume stays
  meaningful on the host and inside every container it runs. Longest source
  prefix wins, and whichever side exists here is canonicalized so a rule
  written through a symlink (`/tmp` is `/private/tmp` on macOS) still matches.
  Without a map, host-shaped entries simply do not resolve and the install says
  which path it looked for.

* `zed local doctor` prints that whole picture -- index location, container
  detection, mapping, policy, and each entry's volume and status -- which is
  the answer to "why did my install go to the network?".

Also: `zed r2g` now installs its mock consumer with `--local-registry=off`. A
roundtrip exists to prove the published artifact installs; letting a local
registration of the same package substitute source would turn that guarantee
into a tautology.

Tests: 19 new unit tests in `src/local_registry.rs` covering classification,
mount-table parsing, the unavailable/missing split, path mapping in both
directions, and link-policy resolution; `tests/local_registry_portability.rs`
drives removable media, unmounted volumes, bind-mount path maps, a mounted
index, and image builds through the real binary;
`tests/local_registry_vs_remote.rs` publishes the same package to a `file://`
registry and registers a different copy locally, so precedence, fallback,
requirement mismatch, `--local-registry off|only|prefer`, frozen replay, and
transitive resolution are each decided by which bytes land in `zed_modules/`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ViG27Hx7QA9mYQ2ernM5n
…e tree compile

Two commits landed on this branch from separate sessions, neither of which
could be compiled where it was written (crates.io is unreachable from those
sandboxes). Built and tested here, they do not hold up. This is that
reconciliation, plus the piece the feature was missing.

**Registrations are now resolved in the dependency solver.** `zed install`
runs `install_graph::prepare` before the materializer, and that solver went
straight to the registry for every key. A registered checkout therefore only
worked when the package *also* existed remotely — precisely the case the
local registry exists to avoid. The solver now consults the index at the same
point it consults workspace members: a locally satisfied key is recorded from
live source, never fetched and never prefetched. Because the solver is where
the network would first be contacted, it is also where `--local-registry=only`
is enforced and where the fall-through to the registry is reported; the
materializer keeps the bail as a backstop for the paths that skip solving, and
no longer duplicates the note. `select_with` lets the solver judge a candidate
from its whole manifest, since it accumulates several constraints and
interprets them under the package's own version scheme.

**Compile fixes.** `Config::from_globals` initialized `local` twice; the
`Globals` literal in its test predated three new fields; sixteen `register`
call sites predated the explicit link-policy argument; two
`validate_frozen_manifest_requirements` call sites predated its source-links
parameter; and `LinkPolicy::resolve` had two identical arms, which `clippy -D
warnings` rejects. The three new global flags were declared in
`.cli-flags.toml` but not in `.dev-cli-flags.toml`, so `zed develop`'s
contract no longer matched its typed parser.

**Two semantic corrections**, both cases where the code contradicted its own
documented rule.

`volume_is_present` treated "this directory is not a mount point" as proof
that a removable volume was detached. That is a sound heuristic only for
Linux's leftover mount directory, which is empty — it must not outvote a
checkout that is demonstrably readable underneath the mount point. It now
does not, so a registration on media that was classified generously is used
rather than reported unavailable, while a genuinely absent mount point is
still absent.

`link_decision` compared device numbers to decide whether the consuming
project "shares the volume" with the checkout. Two bind mounts of one
filesystem share a device id while having entirely different lifetimes, which
is exactly what a container mount is, so device equality is the wrong
question. It now compares recorded volume identity — mount point and kind —
and falls back to device ids only when neither side has a mount point.

Verified: `cargo clippy --all-targets -- -D warnings` clean, and 877 of 878
tests pass. The remaining failure is
`validation::tests::checked_in_schemas_are_pinned_to_the_dependency_revision`,
which asserts `Cargo.toml` names the pinned `zed-interfaces` revision; it fails
only because building here required temporarily pointing the three git
dependencies at local checkouts. `Cargo.toml` is committed unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fvi78rjurJXbT9XYJv72by
…m local disk

Salvage of the unmerged branch origin/feat/local-project-registry
(eeaf911, 8730f73, 2ebef8d) onto current main, part of #378.

Conflicts resolved by keeping both sides' intent:
- Config gains `local: LocalPortability` alongside main's `mirrors` and
  `fallback`; every test/literal Config sets all three.
- Globals keeps main's no_mirrors/trust_mirror_metadata/r2_public_*/
  source_fallback flags and adds the branch's local_path_map,
  local_link_policy, local_ephemeral; .cli-flags.toml carries both sets.
- resolver::prepare keeps main's MirrorContext-based registry and pool and
  threads the branch's local index and LocalRegistryMode into solve_install.
- lib.rs keeps main's lifecycle module plus local_cli/local_registry.
- r2g's mock consumer inherits main's mirrors/fallback and keeps the
  branch's LocalRegistryMode::Off.

Also drops the duplicate `use crate::registry::registry_for;` at the end of
src/install_graph/hardening_tests.rs that broke `cargo test --lib` on main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The branch's config_for (local_registry.rs) and config_for_test (ops.rs)
merged without textual conflict but predate main's `mirrors`/`fallback`
Config fields, so `cargo test --lib` failed with E0063. Set them to the
same inert values every other test Config on main uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ignatures, signing_keys)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@ORESoftware ORESoftware left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coordination note for DEN-3282 / merged zed-interfaces#138: local machine registrations must remain below explicit project-authored source authority. The resolver/materializer precedence should be documented and tested as:

  1. explicit [overrides.path] for that dependency;
  2. project-owned providers: ordinary [workspace].members and [interop.source-composition] entries with role = "workspace";
  3. opt-in machine-local registry according to --local-registry policy;
  4. remote registry/fallback.

A machine registration must never silently replace a manifest-authoritative source-composition workspace provider or an explicit path override. Conversely, local-registry=only may suppress remote access but should not disable already-declared project-local providers. Frozen mode must also retain DEN-3282's rule that mutable ordinary source-composition checkouts are refused until an immutable workspace-source lock exists; the machine registry's prefer mode must not be used to bypass that source-lock boundary.

Please add cross-feature tests before this PR leaves WIP, especially the same package present in [overrides.path], source composition, and the local registry.

@the1mills
the1mills merged commit 568c2d9 into main Sep 30, 2026
77 of 122 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants