feat(wsl): accounts shim for agents on a WSL orcad host (WSL-via-orcad C9) (#27874)

* feat(wsl): accounts shim for agents on a WSL orcad host (WSL-via-orcad 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.

* refactor(wsl): track WSL host environments beside their registry
This commit is contained in:
Neil
2026-10-13 07:23:53 -07:00
committed by GitHub
parent fc7dff8ce1
commit 41e6ea530a
28 changed files with 840 additions and 42 deletions
@@ -0,0 +1,88 @@
/**
* `agent.launch`'s `accountEnv` reaches only terminal agents; a structured launch keeps its
* host-derived home and says so.
*/
import { describe, expect, it, vi } from 'vitest'
import { executeAgentLaunch, type AgentLaunchExecution } from './agent-launch-executor'
import type { AgentLaunchIntent } from '../../shared/agent-launch-intent'
import { STRUCTURED_ACCOUNT_ENV_IGNORED_WARNING } from '../../shared/agent-launch-account-env'
const ACCOUNT_ENV = { CLAUDE_CONFIG_DIR: '/home/u/.orca/claude/a', CODEX_HOME: '/home/u/.codex-b' }
function harness(structured: boolean) {
const createWorktree = vi.fn(async (args: { startupAgent: string | undefined }) => ({
worktreeId: 'wt-new',
connectionId: null,
startupTerminalHandle: args.startupAgent ? 'term_agent_first' : undefined
}))
const createStructuredSession = vi.fn(async () => ({
sessionId: 'sess-1',
handle: 'handle_structured',
fence: 4
}))
const createTerminalAgent = vi.fn(async () => ({ handle: 'term_1' }))
const runtime = {
getClientSettings: () => (structured ? { experimentalNativeChat: true } : null),
getStructuredAgentSessionCreateSupport: async () => ({ supported: true })
}
return {
createWorktree,
createStructuredSession,
createTerminalAgent,
run: (intent: AgentLaunchIntent) =>
executeAgentLaunch({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub implements the two runtime methods the executor reaches; a missing one throws.
runtime: runtime as unknown as AgentLaunchExecution['runtime'],
intent,
surfaces: { createStructuredSession, createTerminalAgent },
workspaces: { createWorktree }
})
}
}
describe('agent.launch account env', () => {
it('passes the account env to a terminal agent in an existing workspace', async () => {
const h = harness(false)
const result = await h.run({
agent: 'claude',
target: { kind: 'existing', worktree: 'wt-7' },
accountEnv: ACCOUNT_ENV
})
expect(h.createTerminalAgent).toHaveBeenCalledWith(
expect.objectContaining({ accountEnv: ACCOUNT_ENV })
)
expect(result.warning).toBeUndefined()
})
it('passes it to the startup terminal of a worktree the launch creates', async () => {
const h = harness(false)
await h.run({
agent: 'codex',
target: { kind: 'create-worktree', create: { repo: 'id:repo-1', name: 'task' } },
accountEnv: ACCOUNT_ENV
})
expect(h.createWorktree).toHaveBeenCalledWith(
expect.objectContaining({ startupAgent: 'codex', accountEnv: ACCOUNT_ENV })
)
})
it('keeps a structured session on its host-derived home and warns', async () => {
const h = harness(true)
const result = await h.run({
agent: 'claude',
target: { kind: 'existing', worktree: 'wt-7' },
accountEnv: ACCOUNT_ENV
})
expect(result.outcome.kind).toBe('structured')
expect(h.createStructuredSession).toHaveBeenCalledWith(
expect.not.objectContaining({ accountEnv: expect.anything() })
)
expect(result.warning).toBe(STRUCTURED_ACCOUNT_ENV_IGNORED_WARNING)
})
it('adds no warning to a structured launch that carried none', async () => {
const h = harness(true)
const result = await h.run({ agent: 'claude', target: { kind: 'existing', worktree: 'wt-7' } })
expect(result.warning).toBeUndefined()
})
})
+7 -32
View File
@@ -35,6 +35,10 @@ import type {
AgentLaunchTarget
} from '../../shared/agent-launch-intent'
import { withoutReservedAgentCreateFields } from '../../shared/agent-launch-intent'
import {
combineLaunchWarnings,
ignoredStructuredLaunchInputsWarning
} from './agent-launch-warnings'
import {
argvLaunchPrompt,
deliverTerminalLaunchPrompt,
@@ -307,26 +311,12 @@ async function createSurface(
...(session.tabId ? { tabId: session.tabId } : {})
},
structured: session,
...ignoredStructuredAgentArgsWarning(intent)
...ignoredStructuredLaunchInputsWarning(intent)
}
}
return createTerminalSurface(execution, workspace)
}
/**
* Structured chat uses saved Arguments, so a per-call override still needs a truthful warning.
*/
function ignoredStructuredAgentArgsWarning(
intent: AgentLaunchIntent
): { warning: string } | undefined {
return intent.agentArgs === undefined
? undefined
: {
warning:
'Started a structured chat session using saved agent Arguments; the per-launch argument override was ignored.'
}
}
/** What every route that builds a terminal agent passes on, so the startup terminal of a new
* workspace and the terminal of an existing one start the same agent. */
function terminalLaunchInputs(intent: AgentLaunchIntent) {
@@ -336,7 +326,8 @@ function terminalLaunchInputs(intent: AgentLaunchIntent) {
...(intent.agentArgs !== undefined ? { agentArgs: intent.agentArgs } : {}),
...(intent.cwd ? { cwd: intent.cwd } : {}),
...(intent.launchSource ? { launchSource: intent.launchSource } : {}),
...(intent.paneKey ? { paneKey: intent.paneKey } : {})
...(intent.paneKey ? { paneKey: intent.paneKey } : {}),
...(intent.accountEnv ? { accountEnv: intent.accountEnv } : {})
}
}
@@ -368,22 +359,6 @@ async function createTerminalSurface(
}
}
/**
* Two warnings, both true, neither droppable.
*
* Mirrors how the create combines its own failures — `appendFailure` in
* runtime-local-worktree-terminal-startup.ts — which append rather than replace.
*/
export function combineLaunchWarnings(
create: string | undefined,
surface: string | undefined
): string | undefined {
if (!create || !surface) {
return create ?? surface
}
return `${create} Also ${surface[0].toLowerCase()}${surface.slice(1)}`
}
function existingWorktreeId(target: AgentLaunchTarget): string {
return target.kind === 'existing' ? target.worktree : ''
}
@@ -39,6 +39,8 @@ export type AgentLaunchSurfaceFactory = {
paneKey?: string
/** The tab's first view, derived on the host by the window's own rule. */
viewMode?: 'terminal' | 'chat'
/** Account routing env merged over the agent's own (`agent.launch`'s `accountEnv`). */
accountEnv?: Readonly<Record<string, string>>
}): Promise<{
handle: string
/** The pane this create minted; a factory whose runtime reports none omits it, never invents. */
@@ -121,6 +123,8 @@ export type AgentLaunchWorkspaceFactory = {
paneKey?: string
/** The launch's session options, read as the startup terminal's model/effort/mode preferences. */
options?: Readonly<Record<string, unknown>>
/** Account routing env merged over the startup agent's own. */
accountEnv?: Readonly<Record<string, string>>
}): Promise<{
worktreeId: string
/** The new workspace's SSH connection; `null` is local. The executor carries it but nothing
@@ -0,0 +1,35 @@
import type { AgentLaunchIntent } from '../../shared/agent-launch-intent'
import { STRUCTURED_ACCOUNT_ENV_IGNORED_WARNING } from '../../shared/agent-launch-account-env'
/**
* Two warnings, both true, neither droppable.
*
* Mirrors how the create combines its own failures — `appendFailure` in
* runtime-local-worktree-terminal-startup.ts — which append rather than replace.
*/
export function combineLaunchWarnings(
create: string | undefined,
surface: string | undefined
): string | undefined {
if (!create || !surface) {
return create ?? surface
}
return `${create} Also ${surface[0].toLowerCase()}${surface.slice(1)}`
}
/**
* Structured chat uses saved Arguments and its host-derived home, so a per-call override of either
* still needs a truthful warning.
*/
export function ignoredStructuredLaunchInputsWarning(
intent: AgentLaunchIntent
): { warning: string } | undefined {
const warning = combineLaunchWarnings(
intent.agentArgs === undefined
? undefined
: 'Started a structured chat session using saved agent Arguments; the per-launch argument override was ignored.',
// Why: a structured home stays host-derived (no client-chosen credential dirs).
intent.accountEnv ? STRUCTURED_ACCOUNT_ENV_IGNORED_WARNING : undefined
)
return warning ? { warning } : undefined
}
@@ -147,6 +147,15 @@ describe.skipIf(!posixHost)('ClaudeWslProfileRouter', () => {
expect(f.setup.calls).toBe(3)
})
it('reads usage from the same guest folder a launch (local or WSL-host) runs in', async () => {
const f = fixture()
const launch = await f.router.prepareLaunch('Ubuntu')
const usage = await f.router.accountUsagePreparation('Ubuntu', 'a')
const poll = await f.router.preparation('Ubuntu')
expect(usage.wslLinuxConfigDir).toBe(launch.envPatch.CLAUDE_CONFIG_DIR)
expect(poll.wslLinuxConfigDir).toBe(launch.envPatch.CLAUDE_CONFIG_DIR)
})
it('makes a launch redo a first setup that was cut off after its ownership gate', async () => {
const f = fixture()
mkdirSync(f.profileHome, { recursive: true })
@@ -34,6 +34,7 @@ import {
getRuntimeEnvironmentStatus
} from './runtime-environment-transport-routing'
import { publicRuntimeEnvironmentWithHostKey } from './runtime-environment-host-key'
import { withWslOrcadAccountEnv } from '../wsl/wsl-orcad-account-env'
function manuallyDisconnectedResponse(
environment: ReturnType<typeof resolveEnvironment>
@@ -224,13 +225,23 @@ function registerPassiveCallHandler(getUserDataPath: () => string): void {
if (isRuntimeEnvironmentManuallyDisconnected(environment.id)) {
return manuallyDisconnectedResponse(environment)
}
// Why here: every renderer call to a paired host passes this one handler.
const accounts = await withWslOrcadAccountEnv(environment.id, args.method, args.params)
if (!accounts.ok) {
return {
id: args.method,
ok: false,
error: { code: 'account_preparation_failed', message: accounts.message },
_meta: { runtimeId: environment.runtimeId }
}
}
let response: RuntimeRpcResponse<unknown>
try {
response = await callRuntimeEnvironment(
getUserDataPath(),
environment.id,
args.method,
args.params,
accounts.params,
args.timeoutMs,
args.expectedEnvironmentPairingRevision,
undefined,
@@ -88,3 +88,16 @@ export function readGuestWindowsDriveMounts(): readonly string[] {
export function _resetGuestWindowsDriveMountsForTests(): void {
cachedGuestMounts = null
}
/** `pathEnv` (default: this process's PATH) without Windows drive dirs on a WSL guest; as given
* elsewhere. For launch-time CLI resolution, which must pick the Linux CLI as detection did. */
export function guestNativePathEnv(
pathEnv: string | null | undefined,
mounts: readonly string[] = readGuestWindowsDriveMounts()
): string | null | undefined {
if (mounts.length === 0) {
return pathEnv
}
const value = pathEnv ?? process.env.PATH ?? null
return value === null ? null : withoutWindowsDriveMountDirs(value, mounts)
}
@@ -0,0 +1,83 @@
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type * as DrivePath from '../ipc/wsl-guest-windows-drive-path'
const guest = vi.hoisted((): { mounts: string[] } => ({ mounts: [] }))
vi.mock('../ipc/wsl-guest-windows-drive-path', async (original) => {
const actual = await original<typeof DrivePath>()
return {
...actual,
guestNativePathEnv: (pathEnv: string | null | undefined) =>
actual.guestNativePathEnv(pathEnv, guest.mounts)
}
})
import { guestNativePathEnv } from '../ipc/wsl-guest-windows-drive-path'
import { resolveStructuredAgentCommand } from './structured-agent-command-resolution'
const scratch: string[] = []
afterEach(() => {
guest.mounts = []
vi.unstubAllEnvs()
for (const dir of scratch.splice(0)) {
rmSync(dir, { recursive: true, force: true })
}
})
function binDir(name: string): string {
const directory = mkdtempSync(join(tmpdir(), `orca-${name}-`))
scratch.push(directory)
for (const binary of ['codex', 'codex-custom']) {
writeFileSync(join(directory, binary), '#!/bin/sh\nexit 0\n')
chmodSync(join(directory, binary), 0o755)
}
return directory
}
// Why posix-only: the guest is Linux; Windows resolution never consults drive mounts.
describe.skipIf(process.platform === 'win32')('launch-time CLI resolution in a WSL guest', () => {
it('skips a Windows shim on a drive mount for the Linux CLI behind it', () => {
const windowsDrive = binDir('drive')
const linuxBin = binDir('linux')
guest.mounts = [windowsDrive]
expect(
resolveStructuredAgentCommand('codex', {}, { pathEnv: `${windowsDrive}:${linuxBin}` })
).toBe(join(linuxBin, 'codex'))
})
it("filters this process's PATH when the caller gives none", () => {
const windowsDrive = binDir('drive')
const linuxBin = binDir('linux')
guest.mounts = [windowsDrive]
vi.stubEnv('PATH', `${windowsDrive}:${linuxBin}`)
expect(resolveStructuredAgentCommand('codex', {})).toBe(join(linuxBin, 'codex'))
})
it('filters the PATH a Command setting resolves through', () => {
const windowsDrive = binDir('drive')
const linuxBin = binDir('linux')
guest.mounts = [windowsDrive]
const settings = {
agentCmdOverrides: { codex: 'codex-custom' },
agentDefaultEnv: { codex: { PATH: `${windowsDrive}:${linuxBin}` } }
}
expect(resolveStructuredAgentCommand('codex', settings)).toBe(join(linuxBin, 'codex-custom'))
})
it('keeps the first PATH match off a guest', () => {
const first = binDir('first')
const second = binDir('second')
expect(resolveStructuredAgentCommand('codex', {}, { pathEnv: `${first}:${second}` })).toBe(
join(first, 'codex')
)
})
})
describe('guestNativePathEnv', () => {
it('returns the PATH as given with no drive mounts', () => {
expect(guestNativePathEnv('/mnt/c/bin:/usr/bin')).toBe('/mnt/c/bin:/usr/bin')
expect(guestNativePathEnv(undefined)).toBeUndefined()
})
})
@@ -9,6 +9,7 @@ import {
structuredAgentCommandToken
} from '../../shared/tui-agent-launch-command-override'
import { AgentSessionPreSpawnError } from './agent-session-wire/structured-agent-session-adapter'
import { guestNativePathEnv } from '../ipc/wsl-guest-windows-drive-path'
export type StructuredAgentCommandSettings = Partial<
Pick<GlobalSettings, 'agentCmdOverrides' | 'agentDefaultEnv'>
@@ -26,7 +27,7 @@ function resolveOverride(
return token
? resolveExecutableCommand(token, {
...options,
pathEnv: options.pathEnv ?? overlay?.PATH ?? overlay?.Path
pathEnv: guestNativePathEnv(options.pathEnv ?? overlay?.PATH ?? overlay?.Path)
})
: null
}
@@ -47,7 +48,12 @@ export function resolveStructuredAgentCommand(
stock: { command?: string; resolve?: typeof resolveCliCommand } = {}
): string {
if (!hasExplicitTuiLaunchCommand(settings, agent)) {
return (stock.resolve ?? resolveCliCommand)(stock.command ?? agent, options)
// Why: inside a WSL guest the inherited PATH ends in /mnt/<drive> dirs holding Windows shims.
const pathEnv = guestNativePathEnv(options.pathEnv)
return (stock.resolve ?? resolveCliCommand)(
stock.command ?? agent,
pathEnv === options.pathEnv ? options : { ...options, pathEnv }
)
}
const command = resolveOverride(agent, settings, options)
if (!command || !spawnableOn(options.platform ?? process.platform, command)) {
@@ -54,7 +54,8 @@ async function agentLaunchIntent(
...(params.cwd ? { cwd: params.cwd } : {}),
...(params.launchSource ? { launchSource: params.launchSource } : {}),
...(paneKey ? { paneKey } : {}),
...(params.sessionId ? { sessionId: params.sessionId } : {})
...(params.sessionId ? { sessionId: params.sessionId } : {}),
...(params.accountEnv ? { accountEnv: params.accountEnv } : {})
}
}
@@ -131,7 +131,8 @@ export function agentLaunchSurfaceFactory(
launchSource,
paneKey,
options,
viewMode
viewMode,
accountEnv
}) => {
const launchPreferences = toAgentLaunchPreferences(options)
let promptRodeLaunchCommand = false
@@ -162,6 +163,7 @@ export function agentLaunchSurfaceFactory(
...(paneKey ? { ...paneIdentity(paneKey), requireFreshPane: true } : {}),
...(launchSource ? { launchSource } : {}),
...(viewMode ? { viewMode } : {}),
...(accountEnv ? { accountEnv: { ...accountEnv } } : {}),
...(presentation && (!movesHostWindow || !hasEarlyTab || presentation === 'background')
? { presentation: movesHostWindow ? presentation : ('background' as const) }
: {}),
@@ -18,7 +18,7 @@ import {
resolveAutomationWorkspaceProvenance
} from '../../../automations/workspace-provenance'
import type { AgentLaunchWorkspaceFactory } from '../../../agent-launch/agent-launch-surface-factories'
import { combineLaunchWarnings } from '../../../agent-launch/agent-launch-executor'
import { combineLaunchWarnings } from '../../../agent-launch/agent-launch-warnings'
import type { RpcContext } from '../core'
import { resolveRpcWorkspaceCreatorProvenance } from '../workspace-creator-context'
import { buildManagedWorktreeCreateArgs } from './worktree-create-args'
@@ -48,7 +48,8 @@ export function agentLaunchWorkspaceFactory(
cwd,
launchSource,
paneKey,
options
options,
accountEnv
}) => {
const startupLaunchPreferences = toAgentLaunchPreferences(options)
let promptRodeLaunchCommand = false
@@ -96,6 +97,7 @@ export function agentLaunchWorkspaceFactory(
...(launchSource ? { startupLaunchSource: launchSource } : {}),
...(paneKey ? { startupPaneKey: paneKey } : {}),
...(startupLaunchPreferences ? { startupLaunchPreferences } : {}),
...(accountEnv ? { startupAccountEnv: { ...accountEnv } } : {}),
// The launch owns the agent whichever surface it settles on, so the workspace records
// it even when no startup terminal was created for it.
createdWithAgent: agent,
@@ -82,10 +82,12 @@ export async function buildRuntimeAgentTerminalStartupOptions(
opts.onStartupPromptCarry?.(promptCarried)
}
// Account routing rides over the plan's own env, as a local launch's auth patch does.
const env = opts.accountEnv ? { ...startupPlan.env, ...opts.accountEnv } : startupPlan.env
return {
...opts,
command: startupPlan.launchCommand,
...(startupPlan.env ? { env: startupPlan.env } : {}),
...(env ? { env } : {}),
launchConfig: startupPlan.launchConfig,
launchAgent: agent,
startupCommandDelivery: startupPlan.startupCommandDelivery,
@@ -65,6 +65,8 @@ export type RuntimeManagedWorktreeCreateArgs = {
startupLaunchSource?: string
/** A caller-minted `tabId:leafId` for the startup terminal's pane. */
startupPaneKey?: string
/** Account routing env merged over the startup agent's own env. */
startupAccountEnv?: Record<string, string>
pendingFirstAgentMessageRename?: boolean
automationProvenance?: AutomationWorkspaceProvenance
/**
@@ -65,6 +65,9 @@ export type TerminalCreateOptions = {
* the runtime still builds, so overriding them does not take the launch away from it.
*/
agentArgs?: string | null
/** Account routing env merged over `startupAgent`'s plan env; an input to that plan, like
* `agentArgs`, so not part of `callerSuppliedLaunch`. */
accountEnv?: Record<string, string>
launchPreferences?: AgentLaunchPreferences
terminalKittyKeyboardProtocol?: boolean
terminalColorQueryReplies?: TerminalOscColorQueryReplyColors
@@ -280,6 +280,24 @@ describe('buildWorktreeStartupForAgent extra agent args', () => {
extraAgentArgs: '--effort high'
})
})
it("merges a launch's account env over the startup plan's env", () => {
const build = vi.fn(() => ({
agent: 'codex' as const,
startup: { command: 'codex', env: { CODEX_HOME: '/stale', KEEP: '1' } }
}))
const resolved = resolveWorktreeCreateAgentStartup(
{
repoSelector: 'repo-1',
name: 'Review',
startupAgent: 'codex',
startupAccountEnv: { CODEX_HOME: '/home/u/.codex-b' }
},
build
)
expect(resolved?.startup.env).toEqual({ CODEX_HOME: '/home/u/.codex-b', KEEP: '1' })
})
})
describe('resolveWorktreeStartupDraftAgent', () => {
@@ -224,10 +224,16 @@ export function resolveWorktreeCreateAgentStartup(
if (args.startup || !args.startupAgent) {
return null
}
return build(args.startupAgent, args.startupPrompt, args.startupLaunchPreferences, {
const built = build(args.startupAgent, args.startupPrompt, args.startupLaunchPreferences, {
...(args.startupAgentArgs !== undefined ? { agentArgs: args.startupAgentArgs } : {}),
...(args.startupExtraAgentArgs ? { extraAgentArgs: args.startupExtraAgentArgs } : {}),
...(args.startupLaunchSource ? { launchSource: args.startupLaunchSource } : {}),
...(args.onStartupPromptCarry ? { onPromptCarry: args.onStartupPromptCarry } : {})
})
return args.startupAccountEnv
? {
...built,
startup: { ...built.startup, env: { ...built.startup.env, ...args.startupAccountEnv } }
}
: built
}
@@ -26,6 +26,7 @@ import { getPreferredPairedRuntimeConnection } from '../../shared/runtime-enviro
import { fingerprintOrchestrationPeer } from '../runtime/orchestration/environment-transport'
import { callRuntimeEnvironment } from '../ipc/runtime-environment-transport-routing'
import { mainProcessState as state } from './main-process-state'
import { installWslOrcadAccountShim } from './wsl-orcad-account-env-wiring'
import {
codexStructuredLaunchHomeResolvers,
prepareCodexRuntimeHomeForLaunch
@@ -173,6 +174,7 @@ export function configureRuntimeServices(runtime: OrcaRuntimeService): void {
)
runtime.setSkillCloudService(new SkillCloudService(app.getPath('userData')))
runtime.setAccountServices({ claudeAccounts, codexAccounts, rateLimits })
installWslOrcadAccountShim(store)
runtime.setCommitMessageAgentEnvironmentResolvers({
// Why: Codex hooks/auth live in Orca's managed runtime home even for the default path, so every launch must resolve CODEX_HOME via runtime-home.
prepareForCodexLaunch: prepareCodexRuntimeHomeForLaunch,
@@ -0,0 +1,28 @@
import { withClaudeProfileTerminalEnv } from '../claude-accounts/claude-profile-installed-router'
import { getSelectedCodexAccountIdForTarget } from '../codex-accounts/runtime-selection'
import type { Store } from '../persistence'
import { installWslOrcadAccountEnvDependencies } from '../wsl/wsl-orcad-account-env'
import { mainProcessState as state } from './main-process-state'
/** The desktop's account services behind the WSL orcad accounts shim. */
export function installWslOrcadAccountShim(store: Pick<Store, 'getSettings'>): void {
installWslOrcadAccountEnvDependencies({
prepareClaudeLaunch: (target) => {
const auth = state.claudeRuntimeAuth
if (!auth) {
throw new Error('Claude account service is not initialized')
}
return auth.prepareForClaudeLaunch(target)
},
claudeTerminalEnv: (target) => withClaudeProfileTerminalEnv({}, target),
prepareCodexHome: async (target) => {
const runtimeHome = state.codexRuntimeHome
if (!runtimeHome || !getSelectedCodexAccountIdForTarget(store.getSettings(), target)) {
return null
}
// Why not prepareCodexRuntimeHomeForLaunch: its hook install targets this desktop's relay,
// while agent status on a WSL host belongs to that host's orcad.
return runtimeHome.prepareForCodexLaunchAsync(target)
}
})
}
+164
View File
@@ -0,0 +1,164 @@
import { describe, expect, it, vi } from 'vitest'
import type { ClaudeRuntimeAuthPreparation } from '../claude-accounts/runtime-auth/runtime-auth-types'
import {
guestCodexHomePath,
prepareWslOrcadAccountEnv,
withWslOrcadAccountEnv,
type WslOrcadAccountEnvDependencies
} from './wsl-orcad-account-env'
const POINTER = '~/.orca-claude-profile-pointer-test'
const CLAUDE_HOME = '/home/u/.orca/claude-accounts/acct-a/config'
function claudeLaunch(
overrides: Partial<ClaudeRuntimeAuthPreparation> = {}
): ClaudeRuntimeAuthPreparation {
return {
configDir: '\\\\wsl.localhost\\Ubuntu\\home\\u\\.orca\\claude-accounts\\acct-a\\config',
runtime: 'wsl',
wslDistro: 'Ubuntu',
wslLinuxConfigDir: CLAUDE_HOME,
envPatch: {
ORCA_CLAUDE_PROFILE_POINTER: POINTER,
CLAUDE_CONFIG_DIR: CLAUDE_HOME,
ORCA_CLAUDE_INJECTED_CONFIG_DIR: CLAUDE_HOME
},
provenance: 'profile:acct-a:wsl:Ubuntu',
...overrides
}
}
function dependencies(
overrides: Partial<WslOrcadAccountEnvDependencies> = {}
): WslOrcadAccountEnvDependencies {
return {
prepareClaudeLaunch: vi.fn(async () => claudeLaunch()),
claudeTerminalEnv: vi.fn(() => ({ ORCA_CLAUDE_PROFILE_POINTER: POINTER })),
prepareCodexHome: vi.fn(async () => '\\\\wsl.localhost\\Ubuntu\\home\\u\\.orca\\codex-b'),
...overrides
}
}
describe('prepareWslOrcadAccountEnv', () => {
it('gives a Claude launch the selected account folder and the Codex home', async () => {
const deps = dependencies()
await expect(prepareWslOrcadAccountEnv('Ubuntu', 'claude', deps)).resolves.toEqual({
ORCA_CLAUDE_PROFILE_POINTER: POINTER,
CLAUDE_CONFIG_DIR: CLAUDE_HOME,
ORCA_CLAUDE_INJECTED_CONFIG_DIR: CLAUDE_HOME,
CODEX_HOME: '/home/u/.orca/codex-b'
})
expect(deps.prepareClaudeLaunch).toHaveBeenCalledWith({ runtime: 'wsl', wslDistro: 'Ubuntu' })
})
it('gives every other process only the Claude pointer, never waiting on Claude setup', async () => {
const deps = dependencies()
await expect(prepareWslOrcadAccountEnv('Ubuntu', 'codex', deps)).resolves.toEqual({
ORCA_CLAUDE_PROFILE_POINTER: POINTER,
CODEX_HOME: '/home/u/.orca/codex-b'
})
expect(deps.prepareClaudeLaunch).not.toHaveBeenCalled()
})
it('sends only the pointer for System default on both', async () => {
const deps = dependencies({
prepareClaudeLaunch: async () =>
claudeLaunch({ envPatch: { ORCA_CLAUDE_PROFILE_POINTER: POINTER } }),
prepareCodexHome: async () => null
})
await expect(prepareWslOrcadAccountEnv('Ubuntu', 'claude', deps)).resolves.toEqual({
ORCA_CLAUDE_PROFILE_POINTER: POINTER
})
})
it("drops a preparation for another runtime or distro rather than send a host's path", async () => {
const deps = dependencies({
prepareClaudeLaunch: async () =>
claudeLaunch({ runtime: 'host', envPatch: { CLAUDE_CONFIG_DIR: '/Users/u/.claude-a' } }),
prepareCodexHome: async () => '\\\\wsl.localhost\\Debian\\home\\u\\.codex-b'
})
await expect(prepareWslOrcadAccountEnv('Ubuntu', 'claude', deps)).resolves.toEqual({})
})
it('refuses a home it cannot send', async () => {
const deps = dependencies({ claudeTerminalEnv: () => ({ ORCA_CLAUDE_PROFILE_POINTER: 'x' }) })
await expect(prepareWslOrcadAccountEnv('Ubuntu', null, deps)).rejects.toThrow(
'not a usable path'
)
})
})
describe('guestCodexHomePath', () => {
it('reads UNC and guest paths, case-insensitive on the distro', () => {
expect(guestCodexHomePath('\\\\wsl$\\ubuntu\\home\\u\\.codex', 'Ubuntu')).toBe('/home/u/.codex')
expect(guestCodexHomePath('/home/u//.codex', 'Ubuntu')).toBe('/home/u/.codex')
expect(guestCodexHomePath('C:\\Users\\u\\.codex', 'Ubuntu')).toBeNull()
})
})
describe('withWslOrcadAccountEnv', () => {
const wsl = (id: string) => (id === 'wsl:ubuntu' ? 'Ubuntu' : null)
const run = (method: string, params: unknown, deps = dependencies(), id = 'wsl:ubuntu') =>
withWslOrcadAccountEnv(id, method, params, deps, wsl)
it('leaves calls to any other environment untouched', async () => {
const params = { worktree: 'id:wt' }
const deps = dependencies()
await expect(run('terminal.create', params, deps, 'ssh-1')).resolves.toEqual({
ok: true,
params
})
expect(deps.prepareCodexHome).not.toHaveBeenCalled()
})
it('adds the env to a plain shell and over a caller-built agent launch', async () => {
await expect(run('terminal.create', { worktree: 'id:wt' })).resolves.toMatchObject({
params: { env: { ORCA_CLAUDE_PROFILE_POINTER: POINTER, CODEX_HOME: '/home/u/.orca/codex-b' } }
})
const created = await run('session.tabs.createTerminal', {
worktree: 'id:wt',
command: 'claude',
launchAgent: 'claude',
env: { CLAUDE_CONFIG_DIR: '/stale', KEEP: '1' }
})
expect(created).toMatchObject({
params: { env: { KEEP: '1', CLAUDE_CONFIG_DIR: CLAUDE_HOME } }
})
})
it('never turns a bare command or agent preset into a caller-built launch', async () => {
const bare = { worktree: 'id:wt', command: 'claude' }
await expect(run('terminal.create', bare)).resolves.toEqual({ ok: true, params: bare })
const preset = { worktree: 'id:wt', agent: 'codex' }
await expect(run('session.tabs.createTerminal', preset)).resolves.toEqual({
ok: true,
params: preset
})
})
it('sets agent.launch accountEnv, except for a reused terminal', async () => {
const launch = { agent: 'claude', target: { kind: 'existing', worktree: 'id:wt' } }
await expect(run('agent.launch', launch)).resolves.toMatchObject({
params: { accountEnv: { CLAUDE_CONFIG_DIR: CLAUDE_HOME } }
})
const reused = { ...launch, reuseTerminal: { handle: 'term_1' } }
await expect(run('agent.launchReplay', reused)).resolves.toEqual({ ok: true, params: reused })
})
it('refuses the call when the account cannot be prepared', async () => {
const deps = dependencies({
prepareClaudeLaunch: async () => {
throw new Error('The selected Claude account could not be set up.')
}
})
await expect(
run('agent.launch', { agent: 'claude', target: { kind: 'existing', worktree: 'x' } }, deps)
).resolves.toEqual({ ok: false, message: 'The selected Claude account could not be set up.' })
})
it('ignores methods that start no process', async () => {
const deps = dependencies()
await expect(run('terminal.list', {}, deps)).resolves.toEqual({ ok: true, params: {} })
expect(deps.prepareCodexHome).not.toHaveBeenCalled()
})
})
+170
View File
@@ -0,0 +1,170 @@
/**
* The accounts shim for a WSL distro's orcad (D10): orcad has no account services, so the desktop
* prepares the selected Claude/Codex account's in-distro home with the same WSL account code a
* local WSL pane uses, and sends the result as env on the calls that start a process there.
*
* Only calls whose env cannot change their meaning are patched: a create that already brings its
* own launch (env, launch config or launch agent), a plain shell, and `agent.launch`'s optional
* `accountEnv`. A bare `command` keeps the host's Settings-default launch untouched.
*/
import { posix } from 'node:path'
import {
AGENT_LAUNCH_ACCOUNT_ENV_KEYS,
AgentLaunchAccountEnv
} from '../../shared/agent-launch-account-env'
import { isTuiAgent } from '../../shared/tui-agent-config'
import type { TuiAgent } from '../../shared/tui-agent'
import { parseWslUncPath } from '../../shared/wsl-paths'
import type { ClaudeRuntimeAuthPreparation } from '../claude-accounts/runtime-auth/runtime-auth-types'
import { wslOrcadHostDistro } from './wsl-orcad-host-environments'
export type WslAccountTarget = { runtime: 'wsl'; wslDistro: string }
export type WslOrcadAccountEnvDependencies = {
/** `ClaudeRuntimeAuthService.prepareForClaudeLaunch`: waits for a first account setup. */
prepareClaudeLaunch(target: WslAccountTarget): Promise<ClaudeRuntimeAuthPreparation>
/** The pointer-only env a non-Claude pane gets, so a typed `claude` still follows the account. */
claudeTerminalEnv(target: WslAccountTarget): Record<string, string>
/** The selected Codex account's home (a UNC or guest path); null on System default. */
prepareCodexHome(target: WslAccountTarget): Promise<string | null>
}
let installed: WslOrcadAccountEnvDependencies | null = null
export function installWslOrcadAccountEnvDependencies(
dependencies: WslOrcadAccountEnvDependencies | null
): void {
installed = dependencies
}
export class WslOrcadAccountEnvError extends Error {
constructor(message: string) {
super(message)
this.name = 'WslOrcadAccountEnvError'
}
}
function sameDistro(a: string | null | undefined, b: string): boolean {
return a?.trim().toLowerCase() === b.trim().toLowerCase()
}
function accountKeysOf(env: Readonly<Record<string, string | undefined>>): Record<string, string> {
const picked: Record<string, string> = {}
for (const key of AGENT_LAUNCH_ACCOUNT_ENV_KEYS) {
const value = env[key]
if (value) {
picked[key] = value
}
}
return picked
}
/** The guest path of a Codex home; null when it belongs to another distro. */
export function guestCodexHomePath(home: string, distro: string): string | null {
const unc = parseWslUncPath(home)
if (unc) {
return sameDistro(unc.distro, distro) ? unc.linuxPath : null
}
return home.startsWith('/') ? posix.normalize(home) : null
}
/** The account env for one process started in `distro`; `agent` null is a plain shell. */
export async function prepareWslOrcadAccountEnv(
distro: string,
agent: TuiAgent | null,
dependencies: WslOrcadAccountEnvDependencies
): Promise<Record<string, string>> {
const target: WslAccountTarget = { runtime: 'wsl', wslDistro: distro }
const env: Record<string, string> = {}
if (agent === 'claude') {
const preparation = await dependencies.prepareClaudeLaunch(target)
// Why: off Windows the service routes a WSL target to the host, whose paths mean nothing here.
if (preparation.runtime === 'wsl' && sameDistro(preparation.wslDistro, distro)) {
Object.assign(env, accountKeysOf(preparation.envPatch))
}
} else {
Object.assign(env, accountKeysOf(dependencies.claudeTerminalEnv(target)))
}
const codexHome = await dependencies.prepareCodexHome(target)
const guestHome = codexHome ? guestCodexHomePath(codexHome, distro) : null
if (guestHome) {
// Why not ORCA_CODEX_HOME too: orcad strips CODEX_HOME when it equals that marker.
env.CODEX_HOME = guestHome
}
const checked = AgentLaunchAccountEnv.safeParse(env)
if (!checked.success) {
throw new WslOrcadAccountEnvError('The selected account folder in WSL is not a usable path.')
}
return checked.data
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function stringRecord(value: unknown): Record<string, string> {
const record: Record<string, string> = {}
if (isRecord(value)) {
for (const [key, entry] of Object.entries(value)) {
if (typeof entry === 'string') {
record[key] = entry
}
}
}
return record
}
function tuiAgentOf(value: unknown): TuiAgent | null {
return isTuiAgent(value) ? value : null
}
type EnvPatchPlan = { field: 'env' | 'accountEnv'; agent: TuiAgent | null }
/** Null leaves the call untouched. */
function envPatchPlan(method: string, params: Record<string, unknown>): EnvPatchPlan | null {
if (method === 'agent.launch' || method === 'agent.launchReplay') {
// A reused terminal starts no process, so it has no home to choose.
return params.reuseTerminal ? null : { field: 'accountEnv', agent: tuiAgentOf(params.agent) }
}
if (method !== 'terminal.create' && method !== 'session.tabs.createTerminal') {
return null
}
const launchAgent = tuiAgentOf(params.launchAgent)
const callerBuiltLaunch =
params.env !== undefined || params.launchConfig !== undefined || launchAgent !== null
const plainShell = params.command === undefined && params.agent === undefined
return callerBuiltLaunch || plainShell ? { field: 'env', agent: launchAgent } : null
}
export type WslOrcadAccountEnvResult =
| { ok: true; params: unknown }
| { ok: false; message: string }
/** `params` with the account env of the WSL distro behind `environmentId`; as is for any other. */
export async function withWslOrcadAccountEnv(
environmentId: string,
method: string,
params: unknown,
dependencies: WslOrcadAccountEnvDependencies | null = installed,
distroOf: (environmentId: string) => string | null = wslOrcadHostDistro
): Promise<WslOrcadAccountEnvResult> {
const distro = distroOf(environmentId)
const plan = distro && dependencies && isRecord(params) ? envPatchPlan(method, params) : null
if (!plan || !distro || !dependencies || !isRecord(params)) {
return { ok: true, params }
}
let env: Record<string, string>
try {
env = await prepareWslOrcadAccountEnv(distro, plan.agent, dependencies)
} catch (error) {
// Why refuse: starting without the prepared home would run another account.
return { ok: false, message: error instanceof Error ? error.message : String(error) }
}
if (Object.keys(env).length === 0) {
return { ok: true, params }
}
if (plan.field === 'accountEnv') {
return { ok: true, params: { ...params, accountEnv: env } }
}
return { ok: true, params: { ...params, env: { ...stringRecord(params.env), ...env } } }
}
@@ -16,6 +16,7 @@ import {
import type { WslOrcadLaunch } from './wsl-orcad-deploy'
import { createWslOrcadHostTransport } from './wsl-orcad-host-transport'
import { listenWslOrcadStdioBridge } from './wsl-orcad-stdio-bridge'
import { wslOrcadHostDistro } from './wsl-orcad-host-environments'
let first: { server: Server; port: number }
let second: { server: Server; port: number }
@@ -291,4 +292,12 @@ describe('WslOrcadConnectionManager', () => {
expect(onChange).toHaveBeenLastCalledWith('Ubuntu', null, { state: 'closed' })
await expect(dial(connection.localPort)).rejects.toThrow()
})
it('lists its environment as a WSL host only while the connection is open', async () => {
const { manager } = setup()
const connection = await manager.ensure('Ubuntu')
expect(wslOrcadHostDistro(connection.environmentId)).toBe('Ubuntu')
await connection.close()
expect(wslOrcadHostDistro(connection.environmentId)).toBeNull()
})
})
+5
View File
@@ -26,6 +26,7 @@ import {
} from './wsl-orcad-deploy'
import type { WslOrcadHostTarget } from './wsl-orcad-host-target'
import { createWslOrcadHostTransport } from './wsl-orcad-host-transport'
import { trackWslOrcadHostEnvironment } from './wsl-orcad-host-environments'
import {
listenWslOrcadStdioBridge,
type WslOrcadStdioBridgeOptions
@@ -166,6 +167,7 @@ export class WslOrcadConnectionManager {
}
this.active.delete(key)
await entry.forward.close()
trackWslOrcadHostEnvironment(entry.connection, null)
this.dependencies.onChange?.(entry.connection.distro, null, { state: 'closed' })
}
@@ -221,6 +223,7 @@ export class WslOrcadConnectionManager {
if (current) {
this.active.delete(key)
await current.forward.close()
trackWslOrcadHostEnvironment(current.connection, null)
this.dependencies.onChange?.(distro, null, status.pending)
}
const forward = await this.listen(transport, {
@@ -262,6 +265,7 @@ export class WslOrcadConnectionManager {
}
}
this.active.set(key, entry)
trackWslOrcadHostEnvironment(entry.connection, entry.connection)
this.dependencies.onChange?.(distro, entry.connection, status.ready)
return entry.connection
} catch (error) {
@@ -287,6 +291,7 @@ export class WslOrcadConnectionManager {
entry.connection = { ...entry.connection, pairingOffer, fullVersion, pid }
this.active.set(key, entry)
}
trackWslOrcadHostEnvironment(entry.connection, entry.connection)
if (changed || announce) {
this.dependencies.onChange?.(entry.connection.distro, entry.connection, announce ?? READY)
}
@@ -0,0 +1,34 @@
/**
* Which paired runtime environments are a WSL distro's orcad, so desktop-side shims (accounts)
* know the distro behind an environment id. Fed by `WslOrcadConnectionManager`: an environment is
* listed only while its connection is open.
*/
const distrosByEnvironment = new Map<string, string>()
export function registerWslOrcadHostEnvironment(environmentId: string, distro: string): void {
distrosByEnvironment.set(environmentId, distro)
}
export function unregisterWslOrcadHostEnvironment(environmentId: string, distro: string): void {
if (distrosByEnvironment.get(environmentId) === distro) {
distrosByEnvironment.delete(environmentId)
}
}
export function wslOrcadHostDistro(environmentId: string): string | null {
return distrosByEnvironment.get(environmentId) ?? null
}
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)
}
}
+62
View File
@@ -0,0 +1,62 @@
/**
* The account routing a desktop prepared for a terminal agent on a host with no account services
* of its own (a WSL distro's orcad): which Claude/Codex home the agent's process starts with.
*
* Deliberately narrow. Only the four routing variables, only absolute POSIX paths (the pointer may
* be `~/`-relative, as the `claude` shell function expands it), and never read for a structured
* session, whose home stays host-derived.
*/
import { z } from 'zod'
import {
CLAUDE_INJECTED_CONFIG_DIR_ENV,
CLAUDE_PROFILE_POINTER_ENV
} from './claude-profile-routing'
export const AGENT_LAUNCH_ACCOUNT_ENV_KEYS = [
'CLAUDE_CONFIG_DIR',
CLAUDE_INJECTED_CONFIG_DIR_ENV,
CLAUDE_PROFILE_POINTER_ENV,
'CODEX_HOME'
] as const
export type AgentLaunchAccountEnvKey = (typeof AGENT_LAUNCH_ACCOUNT_ENV_KEYS)[number]
const MAX_ACCOUNT_PATH_LENGTH = 4096
function isAccountEnvKey(key: string): key is AgentLaunchAccountEnvKey {
return AGENT_LAUNCH_ACCOUNT_ENV_KEYS.some((allowed) => allowed === key)
}
function isAccountPath(key: AgentLaunchAccountEnvKey, value: string): boolean {
if (value.length === 0 || value.length > MAX_ACCOUNT_PATH_LENGTH || /[\0\r\n]/.test(value)) {
return false
}
const relative = key === CLAUDE_PROFILE_POINTER_ENV && value.startsWith('~/')
if (!relative && !value.startsWith('/')) {
return false
}
return !value.split('/').includes('..')
}
/** Why a refusal, not a drop: a dropped home would start the agent under another account. */
export const AgentLaunchAccountEnv = z.record(z.string(), z.string()).superRefine((env, ctx) => {
for (const [key, value] of Object.entries(env)) {
if (!isAccountEnvKey(key)) {
ctx.addIssue({ code: z.ZodIssueCode.custom, path: [key], message: 'Not an account variable' })
} else if (!isAccountPath(key, value)) {
ctx.addIssue({ code: z.ZodIssueCode.custom, path: [key], message: 'Invalid account path' })
}
}
const injected = env[CLAUDE_INJECTED_CONFIG_DIR_ENV]
if (injected !== undefined && injected !== env.CLAUDE_CONFIG_DIR) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [CLAUDE_INJECTED_CONFIG_DIR_ENV],
message: 'Must name the injected CLAUDE_CONFIG_DIR'
})
}
})
export const STRUCTURED_ACCOUNT_ENV_IGNORED_WARNING =
'Account selection is not supported for this session type on WSL hosts yet; the chat uses the host default account.'
+2
View File
@@ -98,6 +98,8 @@ export type AgentLaunchIntent = {
/** The caller-minted id of the chat session a structured launch creates. Not a route input;
* refused when that session already exists. */
sessionId?: string
/** Account routing env for a terminal agent (`agent.launch`'s `accountEnv`). Not a route input. */
accountEnv?: Readonly<Record<string, string>>
}
/**
@@ -0,0 +1,55 @@
import { describe, expect, it } from 'vitest'
import { AgentLaunch, AgentLaunchFields } from './agent-launch-params'
import { computeAgentLaunchFingerprint } from '../agent-launch-operation'
const BASE = { agent: 'claude', target: { kind: 'existing', worktree: 'id:wt-1' } }
const ACCOUNT_ENV = {
CLAUDE_CONFIG_DIR: '/home/u/.orca-claude/a/config',
ORCA_CLAUDE_INJECTED_CONFIG_DIR: '/home/u/.orca-claude/a/config',
ORCA_CLAUDE_PROFILE_POINTER: '~/.orca-claude/pointer',
CODEX_HOME: '/home/u/.orca-codex/b'
}
// What a host that predates the field parses with: the same object, without it.
const OldHostAgentLaunch = AgentLaunchFields.omit({ accountEnv: true })
describe('agent.launch accountEnv wire compatibility', () => {
it('carries a validated account env on a new host', () => {
const parsed = AgentLaunch.parse({ ...BASE, accountEnv: ACCOUNT_ENV })
expect(parsed.accountEnv).toEqual(ACCOUNT_ENV)
})
it('is stripped, not refused, by an older host', () => {
const parsed = OldHostAgentLaunch.parse({ ...BASE, accountEnv: ACCOUNT_ENV })
expect(parsed).not.toHaveProperty('accountEnv')
expect(parsed.agent).toBe('claude')
})
it('leaves an older client launch unchanged on a new host', () => {
const parsed = AgentLaunch.parse(BASE)
expect(parsed).not.toHaveProperty('accountEnv')
expect(parsed).toEqual(OldHostAgentLaunch.parse(BASE))
})
it('stays outside the replay fingerprint', () => {
const withEnv = AgentLaunch.parse({ ...BASE, accountEnv: ACCOUNT_ENV })
expect(computeAgentLaunchFingerprint(withEnv)).toBe(
computeAgentLaunchFingerprint(AgentLaunch.parse(BASE))
)
})
it.each([
['a non-account variable', { PATH: '/usr/bin' }],
['a relative home', { CLAUDE_CONFIG_DIR: 'home/u/.claude' }],
['a parent segment', { CODEX_HOME: '/home/u/../root/.codex' }],
['a Windows path', { CODEX_HOME: 'C:\\Users\\u\\.codex' }],
['a newline', { CODEX_HOME: '/home/u/.codex\nX=1' }],
['a home-relative non-pointer', { CLAUDE_CONFIG_DIR: '~/.claude' }],
[
'an injected marker for another folder',
{ CLAUDE_CONFIG_DIR: '/home/u/a', ORCA_CLAUDE_INJECTED_CONFIG_DIR: '/home/u/b' }
]
])('refuses %s rather than starting another account', (_label, accountEnv) => {
expect(AgentLaunch.safeParse({ ...BASE, accountEnv }).success).toBe(false)
})
})
@@ -23,6 +23,7 @@ import { LaunchSourceParam } from './launch-source-param'
import { TerminalTabIdParam } from './agent-session-params'
import { SessionId } from './structured-agent-session-params'
import { isStructuredAgentSessionIdFor } from '../structured-agent-session-create'
import { AgentLaunchAccountEnv } from '../agent-launch-account-env'
const LaunchAgent = z
.unknown()
@@ -141,7 +142,13 @@ export const AgentLaunchFields = z.object({
.transform((value): 'focused' | 'background' | undefined =>
value === 'focused' || value === 'background' ? value : undefined
)
.optional()
.optional(),
/**
* The Claude/Codex home a desktop prepared for a host with no account services (WSL orcad).
* Terminal-only: a structured launch keeps its host-derived home and warns. Optional forever; an
* older host strips it and runs its default account. Outside the replay fingerprint.
*/
accountEnv: AgentLaunchAccountEnv.optional()
})
/** A caller-minted session id must be shaped like every id the host mints, so an id still names