Repository navigation
feat(wsl): accounts shim for agents on a WSL orcad host (WSL-via-orcad C9) - #27874
Conversation
67bff99 to
5e76a1b
Compare
03a2bce to
3f9b83d
Compare
2b419a3 to
caf2132
Compare
5817b05 to
3f949b6
Compare
7d5d2c4 to
4e4aa55
Compare
…d C9) The desktop prepares the selected Claude/Codex account's in-distro home with the existing WSL account code and sends it as env on terminal.create / session.tabs.createTerminal, or as agent.launch's new optional accountEnv. Structured sessions keep their host-derived home and warn. Launch-time CLI resolution inside a WSL guest skips /mnt/<drive> shims.
3f949b6 to
93113ed
Compare
|
Merge-train review (blast radius outside WSL): LOW. |
📝 Walkthrough
Priority: ➖ Normal Merge Risk: 🔵 Low · up to
|
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
61ec8437-a441-4494-a22a-4f07443d23b4
📒 Files selected for processing (28)
src/main/agent-launch/agent-launch-account-env.test.tssrc/main/agent-launch/agent-launch-executor.tssrc/main/agent-launch/agent-launch-surface-factories.tssrc/main/agent-launch/agent-launch-warnings.tssrc/main/claude-accounts/claude-profile-wsl-router.test.tssrc/main/ipc/runtime-environment-connectivity-handlers.tssrc/main/ipc/wsl-guest-windows-drive-path.tssrc/main/native-chat/structured-agent-command-guest-path.test.tssrc/main/native-chat/structured-agent-command-resolution.tssrc/main/runtime/rpc/methods/agent-launch-intent-resolution.tssrc/main/runtime/rpc/methods/agent-launch-surfaces.tssrc/main/runtime/rpc/methods/agent-launch-worktree-creation.tssrc/main/runtime/runtime-agent-terminal-startup.tssrc/main/runtime/runtime-managed-worktree-create-types.tssrc/main/runtime/runtime-terminal-contracts.tssrc/main/runtime/runtime-worktree-agent-startup.test.tssrc/main/runtime/runtime-worktree-agent-startup.tssrc/main/startup/main-process-runtime-service.tssrc/main/startup/wsl-orcad-account-env-wiring.tssrc/main/wsl/wsl-orcad-account-env.test.tssrc/main/wsl/wsl-orcad-account-env.tssrc/main/wsl/wsl-orcad-connection.test.tssrc/main/wsl/wsl-orcad-connection.tssrc/main/wsl/wsl-orcad-host-environments.tssrc/shared/agent-launch-account-env.tssrc/shared/agent-launch-intent.tssrc/shared/rpc-contract/agent-launch-params-account-env.test.tssrc/shared/rpc-contract/agent-launch-params.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.
| type WslOrcadHostEnvironmentRef = { environmentId: string; distro: string } | ||
|
|
||
| /** Lists `next` while open; a closed connection (`next` null) unlists `previous`. */ | ||
| export function trackWslOrcadHostEnvironment( | ||
| previous: WslOrcadHostEnvironmentRef, | ||
| next: WslOrcadHostEnvironmentRef | null | ||
| ): void { | ||
| if (next) { | ||
| registerWslOrcadHostEnvironment(next.environmentId, next.distro) | ||
| } else { | ||
| unregisterWslOrcadHostEnvironment(previous.environmentId, previous.distro) | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '1,80p' src/main/wsl/wsl-orcad-host-environments.ts
sed -n '100,320p' src/main/wsl/wsl-orcad-connection.tsRepository: stablyai/orca
Length of output: 9606
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- remaining connection manager implementation ---'
sed -n '320,520p' src/main/wsl/wsl-orcad-connection.ts
printf '%s\n' '--- lifecycle and tracking usages ---'
rg -n -F --glob '*.ts' -e 'trackWslOrcadHostEnvironment' -e 'closeEntry(' -e 'new WslOrcadConnectionManager' -e '.close()' src/main/wsl src/main | head -240
printf '%s\n' '--- environment ID and account-routing bindings ---'
rg -n -F --glob '*.ts' -e 'defaultWslOrcadEnvironmentId' -e 'wslOrcadHostDistro' -e 'accountEnv' -e 'environmentId' src/main/wsl src/main | head -320Repository: stablyai/orca
Length of output: 41777
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- exact manager lifecycle callers ---'
rg -n -F --glob '*.ts' -e 'WslOrcadConnectionManager' -e 'connectionManager.close' -e 'connections.close' -e 'wslOrcadConnections.close' -e '.ensure(' src/main/wsl src/main | head -220
printf '%s\n' '--- account environment implementation and consumers ---'
sed -n '1,190p' src/main/wsl/wsl-orcad-account-env.ts
rg -n -F --glob '*.ts' -e 'resolveWslOrcadAccountEnv' -e 'wslOrcadAccountEnv' -e 'wslOrcadHostDistro' -e 'accountEnvFor' src/main | head -180
printf '%s\n' '--- connection tests around close/replacement ---'
sed -n '1,130p' src/main/wsl/wsl-orcad-connection.test.ts
sed -n '250,330p' src/main/wsl/wsl-orcad-connection.test.tsRepository: stablyai/orca
Length of output: 31782
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- liveness close and ensure paths ---'
rg -n -F --glob '*.ts' -e 'manager.close(' -e '.manager.close(' -e 'close: (distro' -e 'ensure: (options' src/main/wsl
sed -n '1,210p' src/main/wsl/wsl-orcad-liveness-supervisor.ts
sed -n '300,345p' src/main/wsl/wsl-orcad-liveness-supervisor.ts
sed -n '1,115p' src/main/wsl/wsl-orcad-liveness-recovery.ts
sed -n '1,120p' src/main/wsl/wsl-orcad-liveness-model.tsRepository: stablyai/orca
Length of output: 17863
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- account-env integration ---'
rg -n -F --glob '*.ts' -e 'withWslOrcadAccountEnv' -e 'installWslOrcadAccountEnvDependencies' src/main src/shared
printf '%s\n' '--- exact changed boundary with line numbers ---'
nl -ba src/main/wsl/wsl-orcad-host-environments.ts | sed -n '1,45p'
nl -ba src/main/wsl/wsl-orcad-connection.ts | sed -n '150,180p;210,235p;255,275p;315,330p'
printf '%s\n' '--- exact supervisor lifecycle lines ---'
nl -ba src/main/wsl/wsl-orcad-liveness-supervisor.ts | sed -n '55,100p;315,335p'Repository: stablyai/orca
Length of output: 9236
Unregister the WSL environment before awaiting bridge shutdown.
WslOrcadConnectionManager.close removes the active entry, then awaits forward.close(). A concurrent reconnect can register the same environment ID and distro during that wait. The old close then calls trackWslOrcadHostEnvironment(old, null), and the distro-only check deletes the new mapping. Later account-routed requests pass through without the WSL account environment.
Suggested fix
this.active.delete(key)
- await entry.forward.close()
trackWslOrcadHostEnvironment(entry.connection, null)
+ await entry.forward.close()
this.dependencies.onChange?.(entry.connection.distro, null, { state: 'closed' })…ht back (stablyai#28402) * fix(ssh): a cancelled sign-in stays cancelled until the user reconnects Co-Authored-By: Claude <noreply@anthropic.com> * test(ssh): drive the credential cancel through the IPC harness Co-Authored-By: Claude <noreply@anthropic.com> * fix(ssh): an explicit server connect lifts the cancelled sign-in of its SSH host Co-Authored-By: Claude <noreply@anthropic.com> * fix(ssh): the cancelled attempt may still offer a password; a successful sign-in lifts the decline Co-Authored-By: Claude <noreply@anthropic.com> * test(ssh): build the managed-server link without the newer kind field Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Claude <noreply@anthropic.com> (cherry picked from commit ed18c2c) Release pick note: runtime-environment-connectivity-handlers.ts conflicted with main's WSL host series (stablyai#27874/stablyai#27940/stablyai#27933), which is not on this release. Kept only this PR's two lines there (the clearSshCredentialDeclinedForEnvironment import and call).
Stacked on #27916 (C6, liveness for the WSL orcad connection), which sits on #27834 (C5, connect to a WSL distro's orcad). Do not merge.
ELI5
When Orca runs agents inside a WSL distro through that distro's own Orca server (orcad), the server has no idea which Claude or Codex account you picked, because account management lives only in the desktop app. This change makes the desktop do the account work, as it already does for local WSL terminals, and hand the server the answer: "start this agent with this account folder". The server just passes it to the agent's process.
What Changed
src/main/wsl/wsl-orcad-account-env.ts). Before a renderer call reaches a WSL orcad host, the oneruntimeEnvironments:callhandler asks the existing WSL account code for the selected account's in-distro home:ClaudeRuntimeAuthService.prepareForClaudeLaunch({runtime:'wsl', wslDistro}), the sameClaudeWslProfileRouterpath local WSL panes use (setup, guest pointer file). A Claude launch getsCLAUDE_CONFIG_DIR+ORCA_CLAUDE_INJECTED_CONFIG_DIR+ORCA_CLAUDE_PROFILE_POINTER; any other process gets the pointer only, so a typedclaudestill follows the account.prepareForCodexLaunchAsync), converted from its UNC path to the guest path, sent asCODEX_HOMEonly when an account is selected.ORCA_CODEX_HOMEis deliberately not sent: orcad stripsCODEX_HOMEwhen it equals that marker. The desktop's Codex hook install is skipped, because agent status on a WSL host belongs to that host's orcad.terminal.create/session.tabs.createTerminalenvonly where adding env cannot change the call's meaning: a plain shell, or a create that already brings its own launch (env,launchConfigorlaunchAgent). A barecommandor an agent preset is left alone, sinceenvwould switch off the host's Settings-default launch.agent.launch/agent.launchReplayget the new optionalaccountEnv(below). A reused terminal is skipped.account_preparation_failedrather than started under another account, the same rule local launches follow.wsl-orcad-host-environments.ts).WslOrcadConnectionManagerregistersenvironmentId → distrowhile a connection is open and removes it on close. C7 must pair the runtime environment underconnection.environmentId.agent.launchaccountEnv(host side). New optional field, validated byAgentLaunchAccountEnv: only the four routing variables, only absolute POSIX paths (the pointer may be~/), no.., no control characters, and the injected marker must equalCLAUDE_CONFIG_DIR. A bad value refuses the launch rather than dropping it. It flows intent → executor → terminal surface (TerminalCreateOptions.accountEnv, merged over the startup plan's env) and → a created worktree's startup terminal (startupAccountEnv). It is outside the replay fingerprint.accountEnvreturns the warning "Account selection is not supported for this session type on WSL hosts yet; the chat uses the host default account." A validated client hint needs a host-side check of the path against the distro's account folders, which belongs with the orcad account-services port (D5)./mnt/<drive>at launch time.resolveStructuredAgentCommand(the structured host's Claude/Codex/ACP/Pi binary resolution) now resolves throughguestNativePathEnv, which drops PATH dirs on Windows drive mounts inside a WSL guest, the same rule C4's detection uses. It is a no-op everywhere else.Why
D10: orcad's account services depend on Electron (paths, network, secret store), so porting them is D5. The desktop already has working, tested WSL account code that talks to the distro over
wsl.exeand does not care how the agent is spawned. Reusing it and passing plain env keeps orcad unchanged; orcad already passes a client's env through, since it never installs account services.Hooking the single main-process handler, rather than threading account env through four renderer create paths, keeps the renderer and the wire unchanged for every non-WSL host. The alternative of setting the pointer in orcad's own process env would route Claude for every caller but cannot route Codex per launch.
Wire compatibility
agent.launch.accountEnvis a new optional field on a non-strict schema. An older host strips it and starts its default account, as before this change; a test parses a new payload with the pre-field schema. An older client never sends it, and a new host parses that payload identically (tested). It is excluded from the replay fingerprint, so a retry with a different account still replays (tested).terminal.create/session.tabs.createTerminalenvalready exist on every supported host. No new opcode, capability or published content.terminal.createAgentSession/ensureAgentSessionare.strict(), so adding a field there would break older hosts. They are not patched; on a WSL host those launches run the distro's default account until a capability-gated field is added (follow-up).Linked Issue
Internal WSL-via-orcad tracker, step C9 (R13; decision 3).
Visual Proof
N/A: no UI. C7 adds the WSL host surface.
Testing
Real host (win-lowspec, Windows 11, WSL 2.7.10). I used a throwaway Ubuntu 24.04 distro,
orca-c9-probe, withsystemd=falseplaced in the rootfs before its first boot (PID 1 wasinit), and a non-root useru. A harness bundled from this branch ran under the installedOrca.exeas Node:WslOrcadConnectionManager.ensure(C4/C5, daily orcad template, 8–30 s), thenwithWslOrcadAccountEnvwith stand-in account services that answer like the WSL Claude router and the Codex runtime home (Claude account A at/home/u/.c9-accounts/claude-a/config, Codex account B as the UNC path\\wsl.localhost\orca-c9-probe\home\u\.c9-accounts\codex-b), thenterminal.createover the bridge. Each pane echoed its env:CODEX_HOMEA=none P=~/.c9-accounts/selected-claude B=/home/u/.c9-accounts/codex-blaunchAgent: claude+ caller envCODEX_HOME, caller key keptA=/home/u/.c9-accounts/claude-a/config I=(same) P=… B=/home/u/.c9-accounts/codex-blaunchAgent: codex+ caller envCODEX_HOMEA=none P=… B=/home/u/.c9-accounts/codex-bcommandA=none P=none B=noneSo the daily orcad passes a client's
CLAUDE_CONFIG_DIRandCODEX_HOMEthrough untouched, and the UNC → guest conversion holds.ORCA_CODEX_HOMEwas never set in a pane. The host-sideagent.launchand/mntchanges are not in that orcad build, so they are covered by unit tests only. Cleanup: the holder was killed, the distro terminated and unregistered, and the temp dir and both terminals removed;wsl -l -vmatches the starting state and noorca-c9-processes remain.Automated.
wsl-orcad-account-env.test.ts: env per account (Claude launch, other process, System default, a host-routed or other-distro answer dropped, an unusable path refused), UNC → guest path, which methods are patched or left alone, refusal on a failed setup.agent-launch-params-account-env.test.ts: new/old host and new/old client parsing, fingerprint, and seven refused shapes.agent-launch-account-env.test.ts: the executor passesaccountEnvto a terminal agent and a created worktree's startup terminal, never to a structured session, and warns there.runtime-worktree-agent-startup.test.ts: the startup env merge.structured-agent-command-guest-path.test.ts: a drive-mount shim is skipped for the Linux CLI (explicit PATH, process PATH, Command-setting PATH), and nothing changes off a guest.wsl-orcad-connection.test.ts: the environment is listed only while connected.claude-profile-wsl-router.test.ts: usage polling (accountUsagePreparation,preparation) reads the same guest folder the launch patch names. Codex polling uses the samegetWslCodexHomePathForSelectionas launch (by reading).preflight-agent-detection"does not double-count…", which fails on the C5 base too, from C4's install-dir code).pnpm tc, oxlint,check:code-quality:changed, reliability-gates, dead-classes, max-lines / ts-nocheck / runtime-electron / focus-read / layout-writer ratchets, node-runtime-pin,verify:rpc-params-catalog, process-host and compat-boundary imports,pnpm format: all pass.AI Disclosure
Review
Agent skill upstream boundary
Notes
terminal.create. Structured sessions never read it.createAgentSession/ensureAgentSession(strict) need a capability-gated field; resumed sleeping agents re-launch without the account env (not persisted inlaunchConfig); structured sessions stay on the host default until D5.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)