fix: keep warehouse connection registry state per project directory

Merged
AN
2h ago
+987-268

Issue for this PR

Closes #1566

Follow-up to #1559. That PR kept an empty-registry re-read from loading another project's file into the shared registry. This one removes the sharing itself.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Why. The warehouse connection registry was process-global, but one server process serves several project directories. The first project to load decided every project's connections. Projects that used the same connection name also shared one connector and one SSH tunnel, so reloading or removing in one project closed the other's.

Design (in registry.ts and ssh-tunnel.ts):

  • Per-project state. Configs, cached connectors with their tunnels, in-flight connects and the load latch live in a Map<projectDir, State>. The global connections file and the connection env vars are still shared sources, merged into each project's state. The #1559 rule still holds, now per project: a project's sources are re-read only while its registry is empty, and a populated registry is never replaced automatically.
  • Each connect attempt owns its tunnel. Tunnels are no longer looked up by name. Each open returns a handle that can be closed while still opening, and every exit path (error, early close, clean disconnect) uses one cleanup routine. An attempt holds its in-flight slot from before any await and is current only while it holds it. Reload, remove, add, dispose and reset drop the slot and close that attempt's tunnel in one synchronous step. A superseded attempt publishes nothing, closes only its own tunnel, and fails with a retry hint.
  • Instance disposal. One disposer closes only the disposed project's connectors, in-flight connects and tunnels. Its configs are kept, so session-only credentials survive config-update disposals. The registry stays synchronous and uses the disposer hook, not InstanceState, which would make about 30 sync callers async.
  • Add and remove act on the current project's state. When the OS credential store is unavailable, add keeps the in-memory credentials in that project only. Overwriting a connection now also closes its old tunnel. Remove writes the global file before changing state, so a failed write changes nothing.
  • Credentials. Keychain entries are still keyed by name and written only by add, for the global file. Remove now keeps them while an env var or any project this process serves names the connection (before, only the current project counted). Projects it does not serve are not consulted.
  • Test helper. setConfigs() sets one injected state shared by every project until reset or load; reset clears all projects. projectConfigApplies() (now always true) and the keyed tunnel API (registry-only) are removed.

Not changed, and replied to in the review threads:

  • Other projects that already have connections loaded pick up a global add or remove on their next reload, per the #1559 rule.
  • Sign-in notice origins and reconnect-log names stay process-wide by name, as before this PR.

How did you verify your code works?

  • registry-per-project.test.ts, 25 tests. Two project directories in one process, through both chdir and an instance context. It uses a fake ssh2 whose readiness the test controls; the fake is restored after the file. Coverage:
    • isolation in both load orders, including files added later
    • empty-registry re-read per project
    • shared global and env entries
    • one connection name giving different configs and connectors per project
    • reload or remove in B leaving A's connector open
    • a stale tunnel start that finishes after a newer start
    • a failing attempt
    • SSH replaced by a non-SSH connection, both cached and in flight
    • a clean SSH disconnect
    • reload and reset during an in-flight connect
    • instance disposal, including session credentials surviving it
    • a failed global write during remove
    • credential retention across served projects
    • in-memory add credentials
  • Full suite. All 61 opencode test files that reference the registry or tunnels: 2445 pass, 79 skip, 0 fail, both together and file by file. The drivers package security test: 29 pass.
  • Static checks. Typecheck is clean. The marker check passes; these paths are Altimate-owned.
  • Review. Local read-only Codex review of head 1c37bc772314d4b5244e9a756ca6dcc4005e49e7: no P0/P1.

Cost side effects

None. This adds no queries or connections. Each project reads its sources the same way a single project did before.

Screenshots / recordings

N/A

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR
  • I checked for cost side effects (model calls, queries or cloud resources that grow with user data) and described them, or wrote "none"

๐Ÿค– Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Connection settings, cached connections, and SSH tunnels remain isolated between projects, including during reloads and connection attempts.
    • Connection configurations load from global, project-local, and environment sources.
    • Connection hints consistently show the project-local configuration path.
    • Closing an outdated SSH tunnel no longer disrupts a newer connection.
    • Credentials are retained while a served project or environment configuration still references the connection.
  • Tests
    • Added coverage for project switching, reloads, SSH tunnel handling, and credential retention.

[!NOTE]
Medium Risk
Changes core connection lifecycle, credential deletion rules, and SSH tunnel ownership across reload/remove/dispose paths; incorrect per-project isolation could leak connections or delete secrets too early, but behavior is heavily tested.

Overview
Fixes multi-project servers where the first loaded project owned every connection: configs, cached connectors, and SSH tunnels are now keyed by project directory, so reload/remove in one project no longer tears down another's.

Registry lifecycle adds per-project State, instance disposal that closes only that project's connectors/tunnels (configs kept for session-only credentials), and reload() that affects the current project only. Connect races give each in-flight attempt its own slot and tunnel handle; superseded attempts fail with a retry hint instead of caching stale connectors.

SSH drops name-keyed startTunnel/closeTunnel for openTunnel() handles with idempotent early close and safer forwarding error cleanup.

Credentials on remove retain OS-store secrets while any served project (open instances + loaded registries) or env still names the connection; warehouse_list always documents the project-local path. Instance.directories() supports that served-set scan.

Extensive tests cover per-project isolation, tunnel ownership, disposal, and credential retention.

Reviewed by Cursor Bugbot for commit 9dee23a4c417a2c6a3978162b960a0321d04c751. Bugbot is set up for automated code reviews on this repo. Configure here.