Skip to content

feat(server): per-instance home dirs for parallel provider sign-in - #7

Merged
adampeterhiggins merged 10 commits into
mainfrom
t3code/parallel-provider-auth-selection
Sep 28, 2026
Merged

adampeterhiggins merged 10 commits into
mainfrom
t3code/parallel-provider-auth-selection

Conversation

@adampeterhiggins

@adampeterhiggins adampeterhiggins commented Sep 18, 2026 •

Copy link
Copy Markdown
Owner

Summary

POC for running multiple native logins per provider in parallel. Each provider's settings gain a homePath field that gives the provider instance its own credential location, so each instance can stay signed in to a different account. Since provider instances already appear as separate entries in the model picker, the picker becomes the account selector — no new auth-profile entity needed.

Per-driver mapping (all fields render automatically in the instance settings form):

Provider Field Mechanism Isolates
Cursor homePath Shadow HOME (see note) auth.json, CLI config, sessions
Grok homePath GROK_HOME auth.json, CLI config
Devin homePath XDG_DATA_HOME credentials.toml
OpenCode homePath XDG_DATA_HOME auth.json

Claude (CLAUDE_CONFIG_DIR), Codex (CODEX_HOME + shadow home), and Antigravity already isolate per instance and are unchanged.

Cursor needs a shadow HOME, not CURSOR_CONFIG_DIR. Cursor's credential store resolves auth.json from homedir() on macOS and $XDG_CONFIG_HOME on Linux — CURSOR_CONFIG_DIR only relocates cli-config.json, so a config-dir override leaves every instance sharing ~/.cursor/auth.json (verified: agent about reports the global account under a fresh config dir). Instead, homePath is materialized as a shadow home directory: .cursor and .config/cursor stay real/private while every other home entry (~/.ssh, ~/.gitconfig, ~/Library, …) symlinks back to the real home, so the agent's own tools keep working. The instance runs with HOME=<shadow> plus AGENT_CLI_CREDENTIAL_STORE=file so cursor-agent login writes auth.json into the shadow rather than the shared macOS keychain (reachable via the ~/Library symlink). Re-materialized on each instance start to fix stale links and sync entries added to the real home later.

  • Shared mergeProviderHomePathEnvironment resolves homePath (with ~ expansion) into the instance env; CursorHome materializes the shadow home and merges HOME
  • The instance env reaches every subprocess: status/about probes, ACP model discovery, adapter sessions, skills probes, login/logout, usage limits
  • cursorUsageLimits credential lookup is home-anchored on every platform (matching the CLI), so the instance HOME points it at the right auth.json; Grok and OpenCode probes already honor GROK_HOME/XDG_DATA_HOME
  • GROK_HOME, XDG_DATA_HOME, XDG_CONFIG_HOME, CURSOR_CONFIG_DIR tilde-expanded in the shared instance env merge (same as CODEX_HOME/CLAUDE_CONFIG_DIR)
  • Docs: providers-cursor.md covers the shadow-home behavior; "separate accounts" sections added to providers-devin.md and providers-opencode.md

OpenCode's auth login is an interactive TUI, so its second login is created in a terminal with the same XDG_DATA_HOME; every other provider signs in through the instance's own browser sign-in action.

Test plan

  • CursorHome.test.ts: env merge, ~ resolution, credential-store default, HOME precedence, shadow-home materialization (private dirs, shared symlinks, .config filtering, real-home rejection)
  • mergeProviderHomePathEnvironment tests: empty path passthrough, multi-var injection, config-over-env precedence
  • cursorUsageLimits test: reads auth.json from the instance HOME
  • ProviderInstanceEnvironment.test.ts: tilde expansion for all six profile-dir vars
  • Focused vitest run: all pass; 2 pre-existing "Cursor skills" failures confirmed on HEAD (macOS /var vs /private/var)
  • Manual: CURSOR_CONFIG_DIR=/tmp/… cursor-agent about still reports the global account (proves config-dir is insufficient); HOME=/tmp/… cursor-agent about reports logged-out with an empty home and the correct account with an auth.json present
  • Manual: XDG_DATA_HOME=/tmp/... devin auth status confirms credential path relocates
  • Manual: two Cursor instances, sign in via browser on each, verify distinct account per instance card

Generated with Devin

Users with multiple Cursor accounts had no way to keep them signed in at
the same time: every Cursor instance shared the global ~/.cursor profile
and the macOS keychain credential.

CursorSettings gains a homePath field exported to cursor-agent as
CURSOR_CONFIG_DIR, so each provider instance can own an isolated config
directory (sign-in, auth.json, CLI settings). The instance env defaults
AGENT_CLI_CREDENTIAL_STORE=file so logins land in that directory instead
of the shared keychain, and usage-limit credential lookup honors
CURSOR_CONFIG_DIR. The model picker is then the account selector.

Done with swe-2 via Devin.
Extends the Cursor profile-dir pattern to the remaining providers that keep CLI login state in a well-known location, so every provider can now hold more than one native sign-in across instances.

GrokSettings, DevinSettings, and OpenCodeSettings gain a homePath field: Grok exports it as GROK_HOME; Devin and OpenCode export it as XDG_DATA_HOME, which relocates credentials.toml and auth.json. A shared mergeProviderHomePathEnvironment helper resolves the path (with ~ expansion) into the instance env; CursorHome now builds on it. Grok's usage probe and OpenCode's auth.json lookup already honor those variables.

Done with swe-2 via Devin.
@adampeterhiggins adampeterhiggins changed the title feat(server): per-instance Cursor config dirs for parallel sign-in feat(server): per-instance home dirs for parallel provider sign-in Sep 18, 2026
adampeterhiggins and others added 6 commits September 18, 2026 16:59
CURSOR_CONFIG_DIR relocates cli-config.json but never auth.json, so the previous approach left every instance sharing ~/.cursor/auth.json (and the macOS keychain). Materialize homePath as a shadow HOME instead: .cursor and .config/cursor stay real/private while every other home entry symlinks back, so cursor-agent login, config, sessions, and usage-limit credentials all resolve per account without breaking agent tools like git or ssh.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…nfig

The providers sidebar rendered a default row for every known driver, so never-configured providers (Devin, Grok, OpenCode, Antigravity) cluttered the list as disabled noise. Now a default slot only lists when it is configured (dirty) or actually enabled, and the empty state offers an Add provider entry point. Default rows also get the delete affordance: deleting one resets it to factory defaults, which drops pristine slots back out of the list.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Resetting a default slot to factory config left enabled:true for Codex/Claude, so the row re-rendered and looked undeletable. Delete now also writes enabled:false and the unconfigured check ignores the enabled key, so a deleted default slot drops out of the provider list. It can be re-added via the + dialog like any provider.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The wizard is now Driver -> Identity -> Config -> Sign in. Add instance creates the instance, fires a targeted provider refresh so its auth methods arrive promptly, and lands on a sign-in step that reuses the provider card's auth sections (browser flows, paste-credential, Antigravity runtime). Navigation is capped at Config until the instance exists and locked on Sign in afterwards.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ProviderAuthSection and ProviderSetupSection grew a variant prop: panel keeps the SettingsRow two-column layout, dialog renders a stacked layout (status, method picker, action buttons, callback form) that fits the wizard popup. The wizard's sign-in step uses it so a new instance can be authenticated without the cramped settings-pane chrome.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
# Conflicts:
#	apps/web/src/components/settings/AddProviderInstanceDialog.tsx
A blank Home directory meant the CLI's real home, so every added Cursor,
Grok, Devin, or OpenCode instance shared the default login, and signing in
on one replaced it for all of them. An added instance with a blank home now
gets a private directory under T3's state dir, keyed by instance id. The
default instance keeps the user's existing login, and an instance
environment variable that already picks the home (GROK_HOME,
XDG_DATA_HOME, HOME) still wins.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

1 participant