Merge remote-tracking branch 'origin/main' into brennanb2025/fix-false-green

This commit is contained in:
Merge Sim
2026-08-31 11:40:52 -07:00
48 changed files with 4042 additions and 891 deletions
+1 -1
View File
@@ -12651,7 +12651,7 @@
"large persisted mismatches route in 50-row pages with coalesced per-mailbox wakes",
"Run pointers pin the existing mixed-version-compatible --run identity",
"direct and Dispatch mail remain durable without unpinned synthetic terminal turns",
"a delayed pointer submit cannot press Enter after the agent becomes working",
"a staged pointer submits once when Codex or Claude becomes working and still withholds Enter for permission",
"an explicit check releases its staged pointer reservation without redrive",
"accepted pointer text is durably deduplicated before delayed Enter and provider Enter refusal",
"a known PTY exit releases staged rows for the replacement process",
+17
View File
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'
import { ORCHESTRATION_COMMAND_SPECS } from './orchestration'
describe('orchestration send command spec', () => {
it('documents valid message types and the question reply path', () => {
const sendSpec = ORCHESTRATION_COMMAND_SPECS.find(
(spec) => spec.path.join(' ') === 'orchestration send'
)
expect(sendSpec?.notes).toEqual(
expect.arrayContaining([
'Valid --type values: status, dispatch, worker_done, merge_ready, escalation, handoff, decision_gate, question, heartbeat.',
'To answer a worker question, use orchestration reply --id <msg_id> --body <text> with the same Orca CLI executable.'
])
)
})
})
+2
View File
@@ -68,6 +68,8 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
'phase'
],
notes: [
'Valid --type values: status, dispatch, worker_done, merge_ready, escalation, handoff, decision_gate, question, heartbeat.',
'To answer a worker question, use orchestration reply --id <msg_id> --body <text> with the same Orca CLI executable.',
'On Windows PowerShell, quote group addresses such as --to "@all" or --to "@worktree:<id>".',
"worker_done and heartbeat are exact-Dispatch signals and cannot target groups; omit --to to use the Dispatch's Run mailbox.",
'worker_done requires --outcome succeeded or --outcome failed.',
+110 -33
View File
@@ -15,8 +15,7 @@ describe('preserveAgentAuthBeforeRestart', () => {
codexRuntimeHome: {
syncForCurrentSelection: vi.fn(() => {
calls.push('codex')
}),
syncActiveWslSelectionsBeforeRestart: vi.fn()
})
},
claudeRuntimeAuth: {
syncForCurrentSelection: vi.fn(async () => {
@@ -33,35 +32,13 @@ describe('preserveAgentAuthBeforeRestart', () => {
expect(calls).toEqual(['codex', 'claude', 'flush'])
})
it('runs WSL Codex preservation through the runtime service', async () => {
const syncForCurrentSelection = vi.fn()
const syncActiveWslSelectionsBeforeRestart = vi.fn()
await preserveAgentAuthBeforeRestart({
codexRuntimeHome: {
syncForCurrentSelection,
syncActiveWslSelectionsBeforeRestart
},
store: {
flushPendingOrThrowAsync: vi.fn()
}
})
expect(syncForCurrentSelection).toHaveBeenCalledTimes(1)
expect(syncForCurrentSelection).toHaveBeenNthCalledWith(1)
expect(syncActiveWslSelectionsBeforeRestart).toHaveBeenCalledTimes(1)
})
it('runs Claude preservation before WSL Codex preservation', async () => {
it('runs Claude preservation after Codex and before the store flush', async () => {
const calls: string[] = []
await preserveAgentAuthBeforeRestart({
codexRuntimeHome: {
syncForCurrentSelection: vi.fn(() => {
calls.push('codex-host')
}),
syncActiveWslSelectionsBeforeRestart: vi.fn(() => {
calls.push('codex-wsl')
})
},
claudeRuntimeAuth: {
@@ -76,29 +53,104 @@ describe('preserveAgentAuthBeforeRestart', () => {
}
})
expect(calls).toEqual(['codex-host', 'claude', 'codex-wsl', 'flush'])
expect(calls).toEqual(['codex-host', 'claude', 'flush'])
})
it('continues after WSL Codex preservation fails', async () => {
it('drains retained WSL Codex auth before flushing the store', async () => {
const calls: string[] = []
await preserveAgentAuthBeforeRestart({
codexRuntimeHome: {
syncForCurrentSelection: vi.fn(() => {
calls.push('codex-host')
}),
syncActiveWslSelectionsBeforeRestart: vi.fn(async () => {
calls.push('codex-wsl')
})
},
store: {
flushPendingOrThrowAsync: vi.fn(async () => {
calls.push('flush')
})
}
})
expect(calls).toEqual(['codex-host', 'codex-wsl', 'flush'])
})
it('does not release restart while the bounded WSL drain is still running', async () => {
vi.useFakeTimers()
let finishWslDrain!: () => void
let settled = false
const flushPendingOrThrowAsync = vi.fn()
const preservation = preserveAgentAuthBeforeRestart({
codexRuntimeHome: {
syncForCurrentSelection: vi.fn(),
syncActiveWslSelectionsBeforeRestart: vi.fn(
() =>
new Promise<void>((resolve) => {
finishWslDrain = resolve
})
)
},
store: { flushPendingOrThrowAsync }
}).then(() => {
settled = true
})
await vi.advanceTimersByTimeAsync(2_000)
await vi.advanceTimersByTimeAsync(1)
expect(flushPendingOrThrowAsync).toHaveBeenCalledTimes(1)
expect(settled).toBe(false)
finishWslDrain()
await preservation
expect(settled).toBe(true)
})
it('continues after the bounded WSL drain fails without logging secrets', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const flushPendingOrThrowAsync = vi.fn()
await preserveAgentAuthBeforeRestart({
codexRuntimeHome: {
syncForCurrentSelection: vi.fn(),
syncActiveWslSelectionsBeforeRestart: vi.fn(() => {
syncActiveWslSelectionsBeforeRestart: vi.fn(async () => {
throw new Error('wsl-token-secret')
})
},
store: {
flushPendingOrThrowAsync
}
store: { flushPendingOrThrowAsync }
})
expect(flushPendingOrThrowAsync).toHaveBeenCalledTimes(1)
expect(warn).toHaveBeenCalledWith(
'[agent-auth-restart] Codex auth preservation failed (Error); continuing restart/update'
)
expect(JSON.stringify(warn.mock.calls)).not.toContain('token-secret')
})
it('does not start Claude after host Codex exhausts the lifecycle budget', async () => {
vi.useFakeTimers()
const startedAt = Date.now()
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const syncClaude = vi.fn(async () => {})
await preserveAgentAuthBeforeRestart({
codexRuntimeHome: {
syncForCurrentSelection: vi.fn(() => {
vi.setSystemTime(startedAt + 2_000)
})
},
claudeRuntimeAuth: { syncForCurrentSelection: syncClaude }
})
expect(syncClaude).not.toHaveBeenCalled()
expect(warn).toHaveBeenCalledWith(
'[agent-auth-restart] Claude auth preservation exceeded 0ms; continuing restart/update'
)
})
it('flushes the store when auth services are missing', async () => {
const flushPendingOrThrowAsync = vi.fn()
@@ -116,8 +168,7 @@ describe('preserveAgentAuthBeforeRestart', () => {
codexRuntimeHome: {
syncForCurrentSelection: vi.fn(() => {
throw new Error('codex-token-secret')
}),
syncActiveWslSelectionsBeforeRestart: vi.fn()
})
},
claudeRuntimeAuth: {
syncForCurrentSelection: vi.fn(async () => {
@@ -170,6 +221,32 @@ describe('preserveAgentAuthBeforeRestart', () => {
expect(calls).toEqual(['claude-start', 'flush', 'claude-finish'])
})
it('shares the original lifecycle timeout between Claude and store preservation', async () => {
vi.useFakeTimers()
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
let settled = false
const preservation = preserveAgentAuthBeforeRestart({
claudeRuntimeAuth: {
syncForCurrentSelection: vi.fn(() => new Promise<void>(() => {}))
},
store: {
flushPendingOrThrowAsync: vi.fn(() => new Promise<void>(() => {}))
}
}).then(() => {
settled = true
})
await vi.advanceTimersByTimeAsync(2_000)
await vi.runOnlyPendingTimersAsync()
await preservation
expect(settled).toBe(true)
expect(warn).toHaveBeenCalledWith(
'[agent-auth-restart] Store persistence exceeded 0ms; continuing restart/update'
)
})
it('bounds a store flush that never settles', async () => {
vi.useFakeTimers()
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
+22 -25
View File
@@ -4,10 +4,8 @@ import type { Store } from './persistence'
const AUTH_PRESERVATION_TIMEOUT_MS = 2_000
type CodexRuntimeAuthSync = Pick<
CodexRuntimeHomeService,
'syncForCurrentSelection' | 'syncActiveWslSelectionsBeforeRestart'
>
type CodexRuntimeAuthSync = Pick<CodexRuntimeHomeService, 'syncForCurrentSelection'> &
Partial<Pick<CodexRuntimeHomeService, 'syncActiveWslSelectionsBeforeRestart'>>
type ClaudeRuntimeAuthSync = Pick<ClaudeRuntimeAuthService, 'syncForCurrentSelection'>
type ShutdownStore = Pick<Store, 'flushPendingOrThrowAsync'>
@@ -28,34 +26,33 @@ export async function preserveAgentAuthBeforeRestart({
store
}: AgentAuthRestartPreservationOptions): Promise<void> {
const startedAt = Date.now()
runCodexPreservationStep(codexRuntimeHome)
// Why: the drain owns guest-process timeouts; a shared 2s cutoff can relaunch before promotion.
const wslCodexPreservation = runWslCodexPreservationStep(codexRuntimeHome)
const remainingMs = Math.max(0, AUTH_PRESERVATION_TIMEOUT_MS - (Date.now() - startedAt))
if (claudeRuntimeAuth && remainingMs > 0) {
const claudeRemainingMs = remainingLifecycleTime(startedAt)
if (claudeRuntimeAuth && claudeRemainingMs > 0) {
await runWithinLifecycleTimeout(
'Claude auth preservation',
() => claudeRuntimeAuth.syncForCurrentSelection(),
remainingMs
claudeRemainingMs
)
} else if (claudeRuntimeAuth) {
logStepTimeout('Claude auth preservation', 0)
}
if (codexRuntimeHome && Date.now() - startedAt < AUTH_PRESERVATION_TIMEOUT_MS) {
runWslCodexPreservationStep(codexRuntimeHome)
} else if (codexRuntimeHome) {
logStepTimeout('Codex auth preservation', 0)
}
const storePreservation = store
? runWithinLifecycleTimeout(
'Store persistence',
() => store.flushPendingOrThrowAsync(),
remainingLifecycleTime(startedAt)
)
: Promise.resolve()
await Promise.all([wslCodexPreservation, storePreservation])
}
if (store) {
const storeRemainingMs = Math.max(0, AUTH_PRESERVATION_TIMEOUT_MS - (Date.now() - startedAt))
await runWithinLifecycleTimeout(
'Store persistence',
() => store.flushPendingOrThrowAsync(),
storeRemainingMs
)
}
function remainingLifecycleTime(startedAt: number): number {
return Math.max(0, AUTH_PRESERVATION_TIMEOUT_MS - (Date.now() - startedAt))
}
function runCodexPreservationStep(codexRuntimeHome: CodexRuntimeAuthSync | null | undefined): void {
@@ -66,11 +63,11 @@ function runCodexPreservationStep(codexRuntimeHome: CodexRuntimeAuthSync | null
}
}
function runWslCodexPreservationStep(
async function runWslCodexPreservationStep(
codexRuntimeHome: CodexRuntimeAuthSync | null | undefined
): void {
): Promise<void> {
try {
codexRuntimeHome?.syncActiveWslSelectionsBeforeRestart()
await codexRuntimeHome?.syncActiveWslSelectionsBeforeRestart?.()
} catch (error) {
logStepFailure('Codex auth preservation', error)
}
@@ -89,7 +86,7 @@ async function runWithinLifecycleTimeout(
})
// Why: this timeout only releases the restart/update path. It does not
// cancel a sync that already started, and Codex sync is synchronous today.
// cancel a sync that already started.
const timeoutResult = new Promise<'timeout'>((resolve) => {
timeout = setTimeout(() => resolve('timeout'), timeoutMs)
})
@@ -0,0 +1,463 @@
import { describe, expect, it } from 'vitest'
import { _internals } from './legacy-wsl-runtime-auth-drain'
import {
NEWER_AUTH,
RETIRED_SESSION,
SOURCE_AUTH,
SOURCE_CREDENTIALS,
TARGET_AUTH,
TORN_CREDENTIALS,
isWindows
} from './legacy-wsl-runtime-auth-drain-script-fixtures'
import { runApplyScript } from './legacy-wsl-runtime-auth-drain-script-harness'
import {
runAbsentLegacyHomeScript,
runRecoveryScript
} from './legacy-wsl-runtime-auth-drain-recovery-script-harness'
describe.skipIf(isWindows)('legacy WSL auth drain apply script', () => {
it('distinguishes an absent legacy home from an authless retained home', () => {
const absent = runAbsentLegacyHomeScript({
createLegacyHome: false,
script: _internals.inspectLegacyAuthScript
})
const retained = runAbsentLegacyHomeScript({
createLegacyHome: true,
script: _internals.inspectLegacyAuthScript
})
expect(absent.status).toBe(22)
expect(retained.status).toBe(21)
})
it('resolves an active-home-only legacy layout as retained', () => {
const outcome = runAbsentLegacyHomeScript({
activeHomeOnly: true,
createLegacyHome: true,
script: _internals.inspectLegacyAuthScript
})
expect(outcome.status).toBe(21)
expect(outcome.markerExists).toBe(false)
})
it('does not finalize while an authless legacy home still holds sessions', () => {
const outcome = runAbsentLegacyHomeScript({
createLegacyHome: true,
script: _internals.finalizeAbsentAuthScript
})
expect(outcome.status).toBe(47)
expect(outcome.markerExists).toBe(false)
})
it('creates the completion-marker parent when the retired tree never existed', () => {
const outcome = runAbsentLegacyHomeScript({
createLegacyHome: false,
markerParentMissing: true,
script: _internals.finalizeAbsentAuthScript
})
expect(outcome.status).toBe(0)
expect(outcome.markerExists).toBe(true)
})
it('promotes the validated source into the account home', () => {
const outcome = runApplyScript()
expect(outcome.targetAuth).toBe(SOURCE_AUTH)
})
it('leaves the legacy home untouched while promoting', () => {
// One-directional: the promote step must never write back to the old home.
expect(runApplyScript().legacyAuth).toBe(SOURCE_AUTH)
})
it('refuses torn bytes and leaves the account home intact', () => {
// Hash call 1 is the source pre-check; rotating right after it means `cp`
// reads bytes freshness never judged. Pre-guard, those reached the target.
const outcome = runApplyScript({ rewriteAfterHashCall: 1 })
expect(outcome.status).toBe(42)
expect(outcome.targetAuth).toBe(TARGET_AUTH)
})
it('refuses a symlinked live source before the destructive path can retire it', () => {
const result = runApplyScript({ deleteSource: true, sourceAuthSymlink: true })
expect(result.status).toBe(46)
expect(result.legacyAuth).toBe(SOURCE_AUTH)
expect(result.markerExists).toBe(false)
expect(result.targetAuth).toBe(TARGET_AUTH)
})
it('refuses MCP credentials that changed after host validation', () => {
const outcome = runApplyScript({
rewriteAfterHashCall: 2,
rewriteBytes: TORN_CREDENTIALS,
rewriteTarget: 'source-credentials',
sourceCredentials: SOURCE_CREDENTIALS
})
expect(outcome.status).toBe(43)
expect(outcome.targetCredentials).toBeNull()
})
it('does not overwrite auth changed after the destination hash check', () => {
const outcome = runApplyScript({
rewriteBytes: NEWER_AUTH,
rewriteAfterHashCall: 4,
rewriteTarget: 'target-auth'
})
expect(outcome.status).toBe(39)
expect(outcome.targetAuth).toBe(NEWER_AUTH)
})
it('keeps the source pending when an unpromoted destination changes before deletion', () => {
// Hash call 3 is the pinned destination check; the quarantine recheck must
// observe this in-place rewrite before removing the source.
const outcome = runApplyScript({
deleteSource: true,
promoteAuth: false,
rewriteAfterHashCall: 3,
rewriteBytes: NEWER_AUTH,
rewriteTarget: 'target-auth'
})
expect(outcome.status).toBe(45)
expect(outcome.legacyAuth).toBe(SOURCE_AUTH)
expect(outcome.targetAuth).toBe(NEWER_AUTH)
expect(outcome.markerExists).toBe(false)
})
it('keeps the source pending when the destination changes after final precommit validation', () => {
// Hash call 9 validates the independent snapshot. A rewrite immediately
// afterward races the atomic cutover and must be detected on the old inode.
const outcome = runApplyScript({
deleteSource: true,
promoteAuth: false,
rewriteAfterHashCall: 9,
rewriteBytes: NEWER_AUTH,
rewriteTarget: 'target-auth'
})
expect(outcome.status).toBe(45)
expect(outcome.legacyAuth).toBe(SOURCE_AUTH)
expect(outcome.targetAuth).toBe(NEWER_AUTH)
expect(outcome.markerExists).toBe(false)
})
it('blocks destination rewrites after the final locked validation', () => {
// Hash call 12 is the installed read-only destination check. The shim's
// attempted in-place rewrite must fail before the source is retired.
const outcome = runApplyScript({
deleteSource: true,
promoteAuth: false,
rewriteAfterHashCall: 12,
rewriteTarget: 'target-auth'
})
expect(outcome.status).toBe(0)
expect(outcome.legacyAuth).toBeNull()
expect(outcome.targetAuth).toBe(TARGET_AUTH)
expect(outcome.markerExists).toBe(true)
})
it('recovers the source and destination mode after abrupt interruption', () => {
const outcome = runApplyScript({ deleteSource: true, killAfterSourceRemoval: true })
expect(outcome.status).not.toBe(0)
expect(outcome.legacyAuth).toBe(SOURCE_AUTH)
expect(outcome.targetAuth).toBe(SOURCE_AUTH)
expect(outcome.targetMode).toBe(0o600)
expect(outcome.markerExists).toBe(false)
})
it('can unlock the destination after interruption during atomic installation', () => {
const outcome = runApplyScript({
deleteSource: true,
killAfterDestinationInstall: true
})
expect(outcome.status).not.toBe(0)
expect(outcome.legacyAuth).toBe(SOURCE_AUTH)
expect(outcome.targetAuth).toBe(SOURCE_AUTH)
expect(outcome.targetMode).toBe(0o600)
expect(outcome.markerExists).toBe(false)
})
it('cleans path metadata when interrupted before destination recovery is linked', () => {
const outcome = runApplyScript({
deleteSource: true,
killBeforeDestinationRecoveryLink: true
})
expect(outcome.status).not.toBe(0)
expect(outcome.legacyAuth).toBe(SOURCE_AUTH)
expect(outcome.destinationRecoveryAuth).toBeNull()
expect(outcome.destinationRecoveryPathExists).toBe(false)
expect(outcome.markerExists).toBe(false)
})
it('restores verified source recovery instead of mutable quarantine after a crash', () => {
const outcome = runApplyScript({
deleteSource: true,
killAfterSourceRemoval: true,
rewriteQuarantineBeforeRecovery: true
})
expect(outcome.status).not.toBe(0)
expect(outcome.legacyAuth).toBe(SOURCE_AUTH)
expect(outcome.sourceRecoveryAuth).toBeNull()
expect(outcome.sourceQuarantineAuth).toBe(NEWER_AUTH)
expect(outcome.markerExists).toBe(false)
})
it('retains verified destination recovery when the target inode changed after a crash', () => {
const outcome = runApplyScript({
deleteSource: true,
killAfterSourceRemoval: true,
replaceTargetBeforeRecovery: true
})
expect(outcome.status).not.toBe(0)
expect(outcome.legacyAuth).toBe(SOURCE_AUTH)
expect(outcome.targetAuth).toBe(NEWER_AUTH)
expect(outcome.destinationRecoveryAuth).toBe(SOURCE_AUTH)
expect(outcome.destinationRecoveryPathExists).toBe(true)
expect(outcome.markerExists).toBe(false)
})
it('preserves a source rewrite that lands before quarantine', () => {
const outcome = runApplyScript({
deleteSource: true,
promoteAuth: false,
rewriteAfterHashCall: 6,
rewriteBytes: NEWER_AUTH,
rewriteTarget: 'source-auth',
sourceSession: RETIRED_SESSION
})
expect(outcome.status).toBe(40)
expect(outcome.legacyAuth).toBe(NEWER_AUTH)
expect(outcome.targetSession).toBeNull()
expect(outcome.markerExists).toBe(false)
})
it('bridges retired sessions while a legacy pane still owns the source', () => {
const outcome = runApplyScript({
deleteSource: false,
promoteAuth: false,
sourceSession: RETIRED_SESSION
})
expect(outcome.status).toBe(0)
expect(outcome.legacyAuth).toBe(SOURCE_AUTH)
expect(outcome.targetSession).toBe(RETIRED_SESSION)
expect(outcome.markerExists).toBe(false)
})
it('rolls back retained-pane links when source ownership changes during the bridge', () => {
const outcome = runApplyScript({
deleteSource: false,
promoteAuth: false,
rewriteBytes: NEWER_AUTH,
rewriteSourceAfterSessionLink: true,
sourceSession: RETIRED_SESSION
})
expect(outcome.status).toBe(40)
expect(outcome.legacyAuth).toBe(NEWER_AUTH)
expect(outcome.targetSession).toBeNull()
expect(outcome.markerExists).toBe(false)
})
it('rolls back retained-pane links when destination ownership changes during the bridge', () => {
const outcome = runApplyScript({
deleteSource: false,
promoteAuth: false,
rewriteBytes: NEWER_AUTH,
rewriteTargetAfterSessionLink: true,
sourceSession: RETIRED_SESSION
})
expect(outcome.status).toBe(45)
expect(outcome.legacyAuth).toBe(SOURCE_AUTH)
expect(outcome.targetAuth).toBe(NEWER_AUTH)
expect(outcome.targetSession).toBeNull()
expect(outcome.markerExists).toBe(false)
})
it('rolls back retired links when ownership changes during the bridge', () => {
const outcome = runApplyScript({
deleteSource: true,
promoteAuth: false,
rewriteBytes: NEWER_AUTH,
rewriteSourceAfterSessionLink: true,
sourceSession: RETIRED_SESSION
})
expect(outcome.status).toBe(40)
expect(outcome.legacyAuth).toBe(NEWER_AUTH)
expect(outcome.sourceRecoveryAuth).toBe(SOURCE_AUTH)
expect(outcome.sourceQuarantineAuth).toBe(SOURCE_AUTH)
expect(outcome.targetSession).toBeNull()
expect(outcome.markerExists).toBe(false)
})
it('rolls back when an atomic rename replaces the destination during the bridge', () => {
const outcome = runApplyScript({
deleteSource: true,
promoteAuth: false,
replaceTargetAfterSessionLink: true,
rewriteBytes: NEWER_AUTH,
sourceSession: RETIRED_SESSION
})
expect(outcome.status).toBe(45)
expect(outcome.legacyAuth).toBe(SOURCE_AUTH)
expect(outcome.targetAuth).toBe(NEWER_AUTH)
expect(outcome.targetSession).toBeNull()
expect(outcome.markerExists).toBe(false)
})
it('retires auth after bridging sessions across guest filesystems', () => {
const outcome = runApplyScript({
crossFilesystemBridge: true,
deleteSource: true,
promoteAuth: false,
sourceSession: RETIRED_SESSION
})
expect(outcome.status).toBe(0)
expect(outcome.legacyAuth).toBeNull()
expect(outcome.targetSession).toBe(RETIRED_SESSION)
expect(outcome.markerExists).toBe(true)
})
it('restores the verified recovery when an open writer mutates the quarantined inode', () => {
const outcome = runApplyScript({
deleteSource: true,
promoteAuth: false,
rewriteBytes: NEWER_AUTH,
rewriteQuarantineAfterSessionLink: true,
sourceSession: RETIRED_SESSION
})
expect(outcome.status).toBe(40)
expect(outcome.legacyAuth).toBe(SOURCE_AUTH)
expect(outcome.sourceRecoveryAuth).toBeNull()
expect(outcome.sourceQuarantineAuth).toBe(NEWER_AUTH)
expect(outcome.targetSession).toBeNull()
expect(outcome.markerExists).toBe(false)
})
it('rolls back a published session link after abrupt interruption', () => {
const outcome = runApplyScript({
deleteSource: true,
killAfterSessionLink: true,
sourceSession: RETIRED_SESSION
})
expect(outcome.status).not.toBe(0)
expect(outcome.legacyAuth).toBe(SOURCE_AUTH)
expect(outcome.targetSession).toBeNull()
expect(outcome.markerExists).toBe(false)
})
it('finishes a durable session-link commit after abrupt interruption', () => {
const outcome = runApplyScript({
deleteSource: true,
killDuringSessionCommit: true,
sourceSession: RETIRED_SESSION
})
expect(outcome.status).not.toBe(0)
expect(outcome.legacyAuth).toBe(SOURCE_AUTH)
expect(outcome.targetSession).toBe(RETIRED_SESSION)
expect(outcome.sessionCommitMarkerExists).toBe(false)
expect(outcome.markerExists).toBe(false)
})
it('finishes durable cleanup when inspection finds a committed marker', () => {
const outcome = runRecoveryScript({
markerPresent: true,
script: _internals.inspectLegacyAuthScript
})
expect(outcome.status).toBe(20)
expect(outcome.destinationMode).toBe(0o600)
expect(outcome.destinationRecoveryExists).toBe(false)
expect(outcome.destinationRecoveryPathExists).toBe(false)
expect(outcome.sourceRecoveryExists).toBe(false)
})
it('finishes durable cleanup when apply finds a committed marker', () => {
const outcome = runRecoveryScript({
markerPresent: true,
script: _internals.applyLegacyAuthScript
})
expect(outcome.status).toBe(0)
expect(outcome.destinationMode).toBe(0o600)
expect(outcome.destinationRecoveryExists).toBe(false)
expect(outcome.destinationRecoveryPathExists).toBe(false)
expect(outcome.sourceRecoveryExists).toBe(false)
})
it('refuses absent-source finalization while a durable recovery copy exists', () => {
const outcome = runRecoveryScript({
markerPresent: false,
script: _internals.finalizeAbsentAuthScript
})
expect(outcome.status).toBe(46)
expect(outcome.sourceAuth).toBe(SOURCE_AUTH)
expect(outcome.destinationMode).toBe(0o600)
expect(outcome.destinationRecoveryExists).toBe(false)
expect(outcome.destinationRecoveryPathExists).toBe(false)
expect(outcome.sourceRecoveryExists).toBe(false)
expect(outcome.markerExists).toBe(false)
})
it('fails closed when destination recovery has no target-path metadata', () => {
const outcome = runRecoveryScript({
markerPresent: false,
pathMetadata: false,
script: _internals.inspectLegacyAuthScript
})
expect(outcome.status).toBe(46)
expect(outcome.destinationRecoveryExists).toBe(true)
expect(outcome.destinationRecoveryPathExists).toBe(false)
})
it('deletes a promoted source only while the destination remains intact', () => {
const changedDestination = runApplyScript({
deleteSource: true,
rewriteAfterHashCall: 7,
rewriteBytes: NEWER_AUTH,
rewriteTarget: 'target-auth'
})
expect(changedDestination.status).toBe(45)
expect(changedDestination.legacyAuth).toBe(SOURCE_AUTH)
expect(changedDestination.markerExists).toBe(false)
const outcome = runApplyScript({ deleteSource: true })
expect(outcome.status).toBe(0)
expect(outcome.legacyAuth).toBeNull()
expect(outcome.targetAuth).toBe(SOURCE_AUTH)
expect(outcome.markerExists).toBe(true)
})
it('refuses to retire the source when the destination is atomically replaced after the inode pin', () => {
// Why this needs its own case: the pinned hard link keeps the ORIGINAL inode, so its hash
// still matches after another writer renames a different file over the destination path.
// Only the `-ef` inode-identity assertions can see that; hash checks cannot. Without them
// the script proceeds and retires the source, leaving the user with bytes nobody validated.
const outcome = runApplyScript({ replaceTargetOnHashOf: '.orca-drain-destination-' })
expect(outcome.status).not.toBe(0)
// The source is the only thing that must survive an unproven destination.
expect(outcome.legacyAuth).toBe(SOURCE_AUTH)
expect(outcome.markerExists).toBe(false)
})
})
@@ -0,0 +1,4 @@
export const MARKER_PRESENT_EXIT = 20
export const SOURCE_AUTH_ABSENT_EXIT = 21
export const LEGACY_HOME_ABSENT_EXIT = 22
export const LEGACY_HOME_STILL_PRESENT_EXIT = 47
@@ -0,0 +1,110 @@
// Why: runs the drain's recovery and absent-legacy-home guest scripts under `sh`.
import { execFileSync } from 'node:child_process'
import {
linkSync,
mkdirSync,
mkdtempSync,
readFileSync,
writeFileSync,
existsSync,
statSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
RETIRED_SESSION,
RETIRED_SESSION_SEGMENTS,
SOURCE_AUTH,
TARGET_AUTH
} from './legacy-wsl-runtime-auth-drain-script-fixtures'
export function runRecoveryScript(options: {
destinationRecovery?: boolean
markerPresent: boolean
pathMetadata?: boolean
script: string
}): {
destinationMode: number
destinationRecoveryExists: boolean
destinationRecoveryPathExists: boolean
markerExists: boolean
sourceAuth: string | null
sourceRecoveryExists: boolean
status: number
} {
const root = mkdtempSync(join(tmpdir(), 'orca-drain-recovery-'))
const legacyHome = join(root, 'legacy')
const targetHome = join(root, 'account')
mkdirSync(legacyHome)
mkdirSync(targetHome)
const markerPath = join(root, 'drain-marker.json')
const sourceRecoveryPath = `${markerPath}.orca-drain-source`
const destinationRecoveryPath = `${markerPath}.orca-drain-destination`
const destinationRecoveryTargetPath = `${markerPath}.orca-drain-destination-path`
const sourceAuthPath = join(legacyHome, 'auth.json')
const destinationAuthPath = join(targetHome, 'auth.json')
writeFileSync(sourceRecoveryPath, SOURCE_AUTH, { mode: 0o400 })
writeFileSync(destinationAuthPath, TARGET_AUTH, { mode: 0o400 })
if (options.destinationRecovery !== false) {
linkSync(destinationAuthPath, destinationRecoveryPath)
}
if (options.pathMetadata !== false) {
writeFileSync(destinationRecoveryTargetPath, `${destinationAuthPath}\0`, { mode: 0o600 })
}
if (options.markerPresent) {
writeFileSync(markerPath, '{"completed":true}\n')
}
let status = 0
try {
execFileSync(
'/bin/sh',
['-c', options.script, 'sh', legacyHome, join(root, 'absent-active-home'), markerPath],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 20_000, windowsHide: true }
)
} catch (error) {
status = (error as { status?: number }).status ?? -1
}
return {
destinationMode: statSync(destinationAuthPath).mode & 0o777,
destinationRecoveryExists: existsSync(destinationRecoveryPath),
destinationRecoveryPathExists: existsSync(destinationRecoveryTargetPath),
markerExists: existsSync(markerPath),
sourceAuth: existsSync(sourceAuthPath) ? readFileSync(sourceAuthPath, 'utf8') : null,
sourceRecoveryExists: existsSync(sourceRecoveryPath),
status
}
}
export function runAbsentLegacyHomeScript(options: {
activeHomeOnly?: boolean
createLegacyHome: boolean
markerParentMissing?: boolean
script: string
}): {
markerExists: boolean
status: number
} {
const root = mkdtempSync(join(tmpdir(), 'orca-drain-absent-home-'))
const legacyHome = join(root, 'legacy')
const activeHome = join(root, 'absent-active-home')
const markerPath = options.markerParentMissing
? join(root, 'marker-parent', 'drain-marker.json')
: join(root, 'drain-marker.json')
if (options.createLegacyHome) {
const sessionHome = options.activeHomeOnly ? activeHome : legacyHome
mkdirSync(join(sessionHome, ...RETIRED_SESSION_SEGMENTS.slice(0, -1)), { recursive: true })
writeFileSync(join(sessionHome, ...RETIRED_SESSION_SEGMENTS), RETIRED_SESSION)
}
let status = 0
try {
execFileSync('/bin/sh', ['-c', options.script, 'sh', legacyHome, activeHome, markerPath], {
encoding: 'utf8',
windowsHide: true,
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 20_000
})
} catch (error) {
status = (error as { status?: number }).status ?? -1
}
return { markerExists: existsSync(markerPath), status }
}
@@ -0,0 +1,171 @@
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { _internals } from './legacy-wsl-runtime-auth-drain'
const SOURCE_AUTH = '{"tokens":{"expires_at":2000}}\n'
const TARGET_AUTH = '{"tokens":{"expires_at":1000}}\n'
const SOURCE_CREDENTIALS = '{"server":{"access_token":"source"}}\n'
const RETIRED_SESSION = '{"session":"retired"}\n'
const LATER_SESSION = '{"session":"later"}\n'
const now = new Date()
const SESSION_SEGMENTS = [
'sessions',
String(now.getFullYear()),
String(now.getMonth() + 1).padStart(2, '0'),
String(now.getDate()).padStart(2, '0'),
'retired.jsonl'
]
function sha256(contents: string): string {
return createHash('sha256').update(contents).digest('hex')
}
function runInspect(root: string, legacyHome: string, markerPath: string): number {
try {
execFileSync(
'/bin/sh',
[
'-c',
_internals.inspectLegacyAuthScript,
'sh',
legacyHome,
join(root, 'absent-active-home'),
markerPath
],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 20_000 }
)
return 0
} catch (error) {
return (error as { status?: number }).status ?? -1
}
}
describe.skipIf(process.platform === 'win32')('legacy WSL auth drain rollback recovery', () => {
it('reopens a completed drain when rollback recreated the retired home', () => {
const root = mkdtempSync(join(tmpdir(), 'orca-drain-reland-'))
const legacyHome = join(root, 'legacy')
const targetHome = join(root, 'account')
const markerPath = join(root, 'drain-marker.json')
const legacyAuthPath = join(legacyHome, 'auth.json')
const targetAuthPath = join(targetHome, 'auth.json')
const targetCredentialsPath = join(targetHome, '.credentials.json')
const sourceSessionPath = join(legacyHome, ...SESSION_SEGMENTS)
const targetSessionPath = join(targetHome, ...SESSION_SEGMENTS)
const laterSourceSessionPath = join(sourceSessionPath, '..', 'later.jsonl')
const laterTargetSessionPath = join(targetSessionPath, '..', 'later.jsonl')
const oldSourceSessionPath = join(legacyHome, 'sessions', '1999', '01', '01', 'old.jsonl')
const oldTargetSessionPath = join(targetHome, 'sessions', '1999', '01', '01', 'old.jsonl')
const rollbackSourceSessionPath = join(sourceSessionPath, '..', 'clock-rollback.jsonl')
const rollbackTargetSessionPath = join(targetSessionPath, '..', 'clock-rollback.jsonl')
const restartSourceSessionPath = join(sourceSessionPath, '..', 'restart-full.jsonl')
const restartTargetSessionPath = join(targetSessionPath, '..', 'restart-full.jsonl')
const blockedSourceSessionPath = join(sourceSessionPath, '..', 'blocked-watermark.jsonl')
const blockedTargetSessionPath = join(targetSessionPath, '..', 'blocked-watermark.jsonl')
mkdirSync(join(sourceSessionPath, '..'), { recursive: true })
mkdirSync(targetHome)
writeFileSync(markerPath, '{"completed":true}\n')
writeFileSync(legacyAuthPath, SOURCE_AUTH)
writeFileSync(join(legacyHome, '.credentials.json'), SOURCE_CREDENTIALS)
writeFileSync(sourceSessionPath, RETIRED_SESSION)
writeFileSync(targetAuthPath, TARGET_AUTH)
expect(runInspect(root, legacyHome, markerPath)).toBe(0)
const apply = (targetHash: string, promote: string, retire: string, bridge: string): void => {
execFileSync(
'/bin/sh',
[
'-c',
_internals.applyLegacyAuthScript,
'sh',
legacyHome,
join(root, 'absent-active-home'),
markerPath,
targetHome,
sha256(SOURCE_AUTH),
targetHash,
promote,
retire,
sha256(SOURCE_CREDENTIALS),
bridge
],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 20_000 }
)
}
apply(sha256(TARGET_AUTH), '1', '0', 'recent')
expect(existsSync(sourceSessionPath)).toBe(true)
expect(readFileSync(targetSessionPath, 'utf8')).toBe(RETIRED_SESSION)
mkdirSync(join(oldSourceSessionPath, '..'), { recursive: true })
writeFileSync(laterSourceSessionPath, LATER_SESSION)
writeFileSync(oldSourceSessionPath, RETIRED_SESSION)
apply(sha256(SOURCE_AUTH), '0', '0', 'recent')
expect(readFileSync(laterTargetSessionPath, 'utf8')).toBe(LATER_SESSION)
expect(existsSync(oldTargetSessionPath)).toBe(false)
const watermarkPath = `${markerPath}.orca-drain-session-watermark`
const linkedWatermarkPath = join(root, 'linked-watermark')
writeFileSync(linkedWatermarkPath, '1999/01/01\n')
rmSync(watermarkPath)
symlinkSync(linkedWatermarkPath, watermarkPath)
apply(sha256(SOURCE_AUTH), '0', '0', 'recent')
expect(readFileSync(oldTargetSessionPath, 'utf8')).toBe(RETIRED_SESSION)
rmSync(watermarkPath)
writeFileSync(watermarkPath, '2999/12/31\n')
writeFileSync(rollbackSourceSessionPath, LATER_SESSION)
apply(sha256(SOURCE_AUTH), '0', '0', 'recent')
expect(readFileSync(rollbackTargetSessionPath, 'utf8')).toBe(LATER_SESSION)
writeFileSync(restartSourceSessionPath, LATER_SESSION)
apply(sha256(SOURCE_AUTH), '0', '0', 'full')
expect(readFileSync(restartTargetSessionPath, 'utf8')).toBe(LATER_SESSION)
rmSync(watermarkPath)
mkdirSync(watermarkPath)
writeFileSync(blockedSourceSessionPath, LATER_SESSION)
expect(() => apply(sha256(SOURCE_AUTH), '0', '0', 'full')).toThrow()
expect(existsSync(blockedTargetSessionPath)).toBe(false)
expect(() => apply(sha256(SOURCE_AUTH), '0', '0', 'recent')).toThrow()
expect(existsSync(blockedTargetSessionPath)).toBe(false)
rmSync(watermarkPath, { recursive: true })
apply(sha256(SOURCE_AUTH), '0', '0', 'recent')
expect(readFileSync(blockedTargetSessionPath, 'utf8')).toBe(LATER_SESSION)
apply(sha256(SOURCE_AUTH), '0', '1', 'full')
expect(existsSync(legacyAuthPath)).toBe(false)
expect(readFileSync(targetAuthPath, 'utf8')).toBe(SOURCE_AUTH)
expect(readFileSync(targetCredentialsPath, 'utf8')).toBe(SOURCE_CREDENTIALS)
expect(readFileSync(targetSessionPath, 'utf8')).toBe(RETIRED_SESSION)
expect(readFileSync(oldTargetSessionPath, 'utf8')).toBe(RETIRED_SESSION)
expect(existsSync(markerPath)).toBe(true)
})
it('keeps completion authoritative when rollback recreated auth as a symlink', () => {
const root = mkdtempSync(join(tmpdir(), 'orca-drain-reland-symlink-'))
const legacyHome = join(root, 'legacy')
const markerPath = join(root, 'drain-marker.json')
const linkedAuthPath = join(root, 'linked-auth.json')
mkdirSync(legacyHome)
writeFileSync(markerPath, '{"completed":true}\n')
writeFileSync(linkedAuthPath, SOURCE_AUTH)
symlinkSync(linkedAuthPath, join(legacyHome, 'auth.json'))
expect(runInspect(root, legacyHome, markerPath)).toBe(46)
expect(existsSync(markerPath)).toBe(true)
expect(readFileSync(linkedAuthPath, 'utf8')).toBe(SOURCE_AUTH)
})
})
@@ -0,0 +1,27 @@
// Why: the auth/credential/session byte fixtures the real-script drain suites assert against.
import { createHash } from 'node:crypto'
export const isWindows = process.platform === 'win32'
export const SOURCE_AUTH = '{"tokens":{"expires_at":2000}}\n'
export const TARGET_AUTH = '{"tokens":{"expires_at":1000}}\n'
export const NEWER_AUTH = '{"tokens":{"expires_at":3000}}\n'
// A different, well-formed auth that another writer atomically renames into place.
export const INTRUDER_AUTH = '{"tokens":{"expires_at":9000}}\n'
// Codex truncates before it writes, so a read landing mid-rotation sees this.
export const TORN_AUTH = '{"tokens":{"exp'
export const SOURCE_CREDENTIALS = '{"server":{"access_token":"source"}}\n'
export const TORN_CREDENTIALS = '{"server":'
export const RETIRED_SESSION = '{"session":"retired"}\n'
export const RETIRED_SESSION_SEGMENTS = ['sessions', '2026', '08', '26', 'retired.jsonl']
export function sha256(contents: string): string {
return createHash('sha256').update(contents).digest('hex')
}
/**
* Runs the real guest script under `sh`, with `sha256sum` shimmed so a chosen
* hash call can rewrite the source underneath the script. That is the only way
* to land Codex's in-place rotation inside the window the script itself opens.
*/
@@ -0,0 +1,275 @@
// Why: runs the real guest apply script under `sh` with shimmed coreutils so tests can inject
// precise interference at chosen points.
import { execFileSync } from 'node:child_process'
import {
chmodSync,
mkdirSync,
mkdtempSync,
readFileSync,
renameSync,
symlinkSync,
writeFileSync,
existsSync,
statSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { installDrainInterferenceShims } from './legacy-wsl-runtime-auth-drain-script-interference-shims'
import {
INTRUDER_AUTH,
NEWER_AUTH,
RETIRED_SESSION_SEGMENTS,
SOURCE_AUTH,
TARGET_AUTH,
TORN_AUTH,
sha256
} from './legacy-wsl-runtime-auth-drain-script-fixtures'
import type {
DrainApplyInterference,
DrainApplyOutcome
} from './legacy-wsl-runtime-auth-drain-script-run-types'
import { _internals } from './legacy-wsl-runtime-auth-drain'
export function runApplyScript(options: DrainApplyInterference = {}): DrainApplyOutcome {
const root = mkdtempSync(join(tmpdir(), 'orca-drain-apply-'))
const legacyHome = join(root, 'legacy')
const targetHome = join(root, 'account')
const binDir = join(root, 'bin')
for (const dir of [legacyHome, targetHome, binDir]) {
mkdirSync(dir, { recursive: true })
}
const legacyAuthPath = join(legacyHome, 'auth.json')
const targetAuthPath = join(targetHome, 'auth.json')
const legacyCredentialsPath = join(legacyHome, '.credentials.json')
const targetCredentialsPath = join(targetHome, '.credentials.json')
const markerPath = join(root, 'drain-marker.json')
writeFileSync(legacyAuthPath, SOURCE_AUTH)
writeFileSync(targetAuthPath, TARGET_AUTH)
if (options.sourceAuthSymlink) {
const sourceAuthTarget = join(root, 'linked-source-auth.json')
renameSync(legacyAuthPath, sourceAuthTarget)
symlinkSync(sourceAuthTarget, legacyAuthPath)
}
if (options.sourceCredentials !== undefined) {
writeFileSync(legacyCredentialsPath, options.sourceCredentials)
}
if (options.sourceSession !== undefined) {
const sessionPath = join(legacyHome, ...RETIRED_SESSION_SEGMENTS)
mkdirSync(join(sessionPath, '..'), { recursive: true })
writeFileSync(sessionPath, options.sourceSession)
}
const counterPath = join(root, 'hash-calls')
writeFileSync(counterPath, '0')
installDrainInterferenceShims(binDir, options)
if (options.killDuringSessionCommit) {
const rmShimPath = join(binDir, 'rm')
writeFileSync(
rmShimPath,
`#!/usr/bin/env node
const { spawnSync } = require('node:child_process')
const fs = require('node:fs')
const args = process.argv.slice(2)
const result = spawnSync('/bin/rm', args, { stdio: 'inherit' })
const target = args.at(-1) ?? ''
if (
result.status === 0 &&
fs.existsSync(process.env.SESSION_COMMIT_MARKER) &&
target.includes('/account/sessions/') &&
target.includes('.orca-bridge-')
) {
const parent = spawnSync('/bin/ps', ['-o', 'ppid=', '-p', String(process.ppid)], {
encoding: 'utf8'
})
process.kill(Number(parent.stdout.trim()), 'SIGKILL')
}
process.exit(result.status ?? 1)
`
)
chmodSync(rmShimPath, 0o755)
}
if (
options.crossFilesystemBridge ||
options.killBeforeDestinationRecoveryLink ||
options.killAfterSessionLink ||
options.replaceTargetAfterSessionLink ||
options.rewriteQuarantineAfterSessionLink ||
options.rewriteSourceAfterSessionLink ||
options.rewriteTargetAfterSessionLink
) {
const lnShimPath = join(binDir, 'ln')
writeFileSync(
lnShimPath,
`#!/usr/bin/env node
const { spawnSync } = require('node:child_process')
const fs = require('node:fs')
const args = process.argv.slice(2)
if (
process.env.KILL_DESTINATION_RECOVERY === '1' &&
args.at(-1)?.endsWith('.orca-drain-destination')
) {
process.kill(process.ppid, 'SIGKILL')
process.exit(1)
}
if (
process.env.CROSS_FILESYSTEM_BRIDGE === '1' &&
args.at(-2)?.includes('.orca-drain-session-stage') &&
args.at(-1)?.includes('.orca-bridge-')
) {
process.exit(1)
}
const result = spawnSync('/bin/ln', args, { stdio: 'inherit' })
if (
result.status === 0 &&
args.at(-1)?.includes('/account/sessions/') &&
args.at(-1)?.endsWith('/retired.jsonl')
) {
if (process.env.KILL_SESSION_LINK === '1') {
const parent = spawnSync('/bin/ps', ['-o', 'ppid=', '-p', String(process.ppid)], {
encoding: 'utf8'
})
process.kill(Number(parent.stdout.trim()), 'SIGKILL')
} else if (process.env.REWRITE_AFTER_SESSION_LINK === '1') {
if (process.env.REPLACE_TARGET === '1') {
const replacement = process.env.REWRITE_SESSION_AUTH + '.replacement'
fs.writeFileSync(replacement, process.env.REWRITE_BYTES)
fs.renameSync(replacement, process.env.REWRITE_SESSION_AUTH)
} else {
if (process.env.REWRITE_QUARANTINE === '1') {
fs.chmodSync(process.env.REWRITE_SESSION_AUTH, 0o600)
}
fs.writeFileSync(process.env.REWRITE_SESSION_AUTH, process.env.REWRITE_BYTES)
}
}
}
process.exit(result.status ?? 1)
`
)
chmodSync(lnShimPath, 0o755)
}
let status = 0
try {
execFileSync(
'/bin/sh',
[
'-c',
_internals.applyLegacyAuthScript,
'sh',
legacyHome,
join(root, 'absent-active-home'),
markerPath,
targetHome,
sha256(SOURCE_AUTH),
sha256(TARGET_AUTH),
options.promoteAuth === false ? '0' : '1',
options.deleteSource ? '1' : '0',
options.sourceCredentials === undefined ? 'missing' : sha256(options.sourceCredentials),
'full'
],
{
encoding: 'utf8',
windowsHide: true,
env: {
...process.env,
HASH_COUNTER: counterPath,
REPLACE_ON_HASH_OF: options.replaceTargetOnHashOf ?? '',
REPLACE_PATH: targetAuthPath,
REPLACE_BYTES: INTRUDER_AUTH,
CROSS_FILESYSTEM_BRIDGE: options.crossFilesystemBridge ? '1' : '0',
KILL_DESTINATION: options.killAfterDestinationInstall ? '1' : '0',
KILL_DESTINATION_RECOVERY: options.killBeforeDestinationRecoveryLink ? '1' : '0',
KILL_SESSION_LINK: options.killAfterSessionLink ? '1' : '0',
KILL_SOURCE: options.killAfterSourceRemoval ? '1' : '0',
PATH: `${binDir}:${process.env.PATH ?? ''}`,
SESSION_COMMIT_MARKER: `${markerPath}.orca-drain-session-commit`,
REWRITE_AFTER: options.rewriteAfterHashCall ? String(options.rewriteAfterHashCall) : '',
REWRITE_AFTER_SESSION_LINK:
options.replaceTargetAfterSessionLink ||
options.rewriteQuarantineAfterSessionLink ||
options.rewriteSourceAfterSessionLink ||
options.rewriteTargetAfterSessionLink
? '1'
: '0',
REWRITE_BYTES: options.rewriteBytes ?? TORN_AUTH,
REWRITE_QUARANTINE: options.rewriteQuarantineAfterSessionLink ? '1' : '0',
REPLACE_TARGET: options.replaceTargetAfterSessionLink ? '1' : '0',
REWRITE_SESSION_AUTH:
options.rewriteTargetAfterSessionLink || options.replaceTargetAfterSessionLink
? targetAuthPath
: options.rewriteQuarantineAfterSessionLink
? `${markerPath}.orca-drain-live-source`
: legacyAuthPath,
REWRITE_TARGET:
options.rewriteTarget === 'source-credentials'
? legacyCredentialsPath
: options.rewriteTarget === 'target-auth'
? targetAuthPath
: legacyAuthPath
},
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 20_000
}
)
} catch (error) {
status = (error as { status?: number }).status ?? -1
}
if (
options.killAfterDestinationInstall ||
options.killBeforeDestinationRecoveryLink ||
options.killAfterSessionLink ||
options.killAfterSourceRemoval ||
options.killDuringSessionCommit
) {
if (options.rewriteQuarantineBeforeRecovery) {
const quarantinePath = `${markerPath}.orca-drain-live-source`
chmodSync(quarantinePath, 0o600)
writeFileSync(quarantinePath, NEWER_AUTH)
}
if (options.replaceTargetBeforeRecovery) {
const replacementPath = `${targetAuthPath}.replacement`
writeFileSync(replacementPath, NEWER_AUTH)
renameSync(replacementPath, targetAuthPath)
}
try {
execFileSync(
'/bin/sh',
[
'-c',
_internals.inspectLegacyAuthScript,
'sh',
legacyHome,
join(root, 'absent-active-home'),
markerPath
],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 20_000, windowsHide: true }
)
} catch {
// Recovery succeeds before inspect continues and emits the pending auth payload.
}
}
return {
legacyAuth: existsSync(legacyAuthPath) ? readFileSync(legacyAuthPath, 'utf8') : null,
markerExists: existsSync(markerPath),
status,
targetAuth: readFileSync(targetAuthPath, 'utf8'),
targetCredentials: existsSync(targetCredentialsPath)
? readFileSync(targetCredentialsPath, 'utf8')
: null,
targetMode: statSync(targetAuthPath).mode & 0o777,
targetSession: existsSync(join(targetHome, ...RETIRED_SESSION_SEGMENTS))
? readFileSync(join(targetHome, ...RETIRED_SESSION_SEGMENTS), 'utf8')
: null,
sourceQuarantineAuth: existsSync(`${markerPath}.orca-drain-live-source`)
? readFileSync(`${markerPath}.orca-drain-live-source`, 'utf8')
: null,
sourceRecoveryAuth: existsSync(`${markerPath}.orca-drain-source`)
? readFileSync(`${markerPath}.orca-drain-source`, 'utf8')
: null,
destinationRecoveryAuth: existsSync(`${markerPath}.orca-drain-destination`)
? readFileSync(`${markerPath}.orca-drain-destination`, 'utf8')
: null,
destinationRecoveryPathExists: existsSync(`${markerPath}.orca-drain-destination-path`),
sessionCommitMarkerExists: existsSync(`${markerPath}.orca-drain-session-commit`)
}
}
@@ -0,0 +1,62 @@
// Why: installs node-backed sha256sum/mv/rm shims on PATH so the real guest script can be
// interfered with at exact points - a hash read, an install rename, a source removal.
import { chmodSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
export function installDrainInterferenceShims(
binDir: string,
options: { killAfterDestinationInstall?: boolean; killAfterSourceRemoval?: boolean }
): void {
const shimPath = join(binDir, 'sha256sum')
writeFileSync(
shimPath,
`#!/usr/bin/env node
const { createHash } = require('node:crypto')
const fs = require('node:fs')
const file = process.argv[process.argv.length - 1]
process.stdout.write(
createHash('sha256').update(fs.readFileSync(file)).digest('hex') + ' ' + file + '\\n'
)
const calls = Number(fs.readFileSync(process.env.HASH_COUNTER, 'utf8')) + 1
fs.writeFileSync(process.env.HASH_COUNTER, String(calls))
if (process.env.REWRITE_AFTER && calls === Number(process.env.REWRITE_AFTER)) {
fs.writeFileSync(process.env.REWRITE_TARGET, process.env.REWRITE_BYTES)
}
// Why: an atomic rename leaves the pinned hard link on the OLD inode, so its hash still
// matches while the path now resolves elsewhere. Only an inode identity check sees that.
if (process.env.REPLACE_ON_HASH_OF && file.includes(process.env.REPLACE_ON_HASH_OF)) {
const staged = process.env.REPLACE_PATH + '.intruder'
fs.writeFileSync(staged, process.env.REPLACE_BYTES)
fs.renameSync(staged, process.env.REPLACE_PATH)
}
`
)
chmodSync(shimPath, 0o755)
if (options.killAfterDestinationInstall || options.killAfterSourceRemoval) {
const mvShimPath = join(binDir, 'mv')
writeFileSync(
mvShimPath,
`#!/usr/bin/env node
const { spawnSync } = require('node:child_process')
const fs = require('node:fs')
const args = process.argv.slice(2)
const result = spawnSync('/bin/mv', args, { stdio: 'inherit' })
const from = args.at(-2) ?? ''
const to = args.at(-1) ?? ''
const sourceInstalled =
process.env.KILL_SOURCE === '1' &&
from.endsWith('/legacy/auth.json') &&
to.endsWith('.orca-drain-live-source')
const destinationInstalled =
process.env.KILL_DESTINATION === '1' &&
from.includes('/account/auth.json.orca-drain-snapshot-') &&
to.endsWith('/account/auth.json')
if (result.status === 0 && (sourceInstalled || destinationInstalled)) {
process.kill(process.ppid, 'SIGKILL')
}
process.exit(result.status ?? 1)
`
)
chmodSync(mvShimPath, 0o755)
}
}
@@ -0,0 +1,40 @@
// Why: the interference knobs and the observable result surface of a real apply-script run,
// kept beside the harness so neither module carries both the contract and the machinery.
export type DrainApplyInterference = {
crossFilesystemBridge?: boolean
deleteSource?: boolean
killAfterDestinationInstall?: boolean
killBeforeDestinationRecoveryLink?: boolean
killAfterSessionLink?: boolean
killAfterSourceRemoval?: boolean
killDuringSessionCommit?: boolean
promoteAuth?: boolean
replaceTargetAfterSessionLink?: boolean
replaceTargetOnHashOf?: string
replaceTargetBeforeRecovery?: boolean
rewriteAfterHashCall?: number
rewriteBytes?: string
rewriteQuarantineAfterSessionLink?: boolean
rewriteQuarantineBeforeRecovery?: boolean
rewriteSourceAfterSessionLink?: boolean
rewriteTargetAfterSessionLink?: boolean
rewriteTarget?: 'source-auth' | 'source-credentials' | 'target-auth'
sourceAuthSymlink?: boolean
sourceSession?: string
sourceCredentials?: string
}
export type DrainApplyOutcome = {
legacyAuth: string | null
markerExists: boolean
status: number
targetAuth: string
targetCredentials: string | null
targetMode: number
targetSession: string | null
sourceQuarantineAuth: string | null
sourceRecoveryAuth: string | null
destinationRecoveryAuth: string | null
destinationRecoveryPathExists: boolean
sessionCommitMarkerExists: boolean
}
@@ -0,0 +1,300 @@
import {
LEGACY_HOME_ABSENT_EXIT,
MARKER_PRESENT_EXIT,
SOURCE_AUTH_ABSENT_EXIT
} from './legacy-wsl-runtime-auth-drain-exit-codes'
import {
DISCARD_DESTINATION_RECOVERY_COMMAND,
RECOVER_DESTINATION_AUTH_COMMAND,
RESOLVE_LEGACY_HOME_SCRIPT,
RETIRED_SESSION_BRIDGE_COMMAND,
RETIRED_RECENT_SESSION_BRIDGE_COMMAND,
ROLLBACK_SESSION_LINKS_FUNCTION
} from './legacy-wsl-runtime-auth-drain-shell-commands'
export * from './legacy-wsl-runtime-auth-drain-exit-codes'
export { FINALIZE_ABSENT_AUTH_SCRIPT } from './legacy-wsl-runtime-auth-finalize-script'
export const INSPECT_LEGACY_AUTH_SCRIPT = `
set -eu
source_recovery_auth="$3.orca-drain-source"
source_quarantine_auth="$3.orca-drain-live-source"
destination_recovery_auth="$3.orca-drain-destination"
destination_recovery_path="$3.orca-drain-destination-path"
session_link_manifest="$3.orca-drain-session-links"
session_commit_marker="$3.orca-drain-session-commit"
session_stage_root="$3.orca-drain-session-stage"
${ROLLBACK_SESSION_LINKS_FUNCTION}
${RESOLVE_LEGACY_HOME_SCRIPT}
source_auth="$legacy_home/auth.json"
if [ -e "$3" ] || [ -L "$3" ]; then
[ -f "$3" ] && [ ! -L "$3" ] || exit 46
commit_session_links || exit 46
if [ -f "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ]; then
chmod 600 "$destination_recovery_auth"
fi
rm -f -- "$source_recovery_auth" "$source_quarantine_auth" "$destination_recovery_auth" "$destination_recovery_path"
if [ ! -e "$source_auth" ] && [ ! -L "$source_auth" ]; then
exit ${MARKER_PRESENT_EXIT}
fi
[ -f "$source_auth" ] && [ ! -L "$source_auth" ] || exit 46
rm -- "$3"
fi
rollback_session_links
if [ "$legacy_home_resolved" = 0 ]; then
exit ${LEGACY_HOME_ABSENT_EXIT}
fi
if [ ! -e "$source_auth" ] && [ ! -L "$source_auth" ]; then
if [ -f "$source_recovery_auth" ] && [ ! -L "$source_recovery_auth" ]; then
mv -- "$source_recovery_auth" "$source_auth"
chmod 600 "$source_auth"
elif [ -f "$source_quarantine_auth" ] && [ ! -L "$source_quarantine_auth" ]; then
mv -- "$source_quarantine_auth" "$source_auth"
chmod 600 "$source_auth"
elif [ -e "$source_recovery_auth" ] || [ -L "$source_recovery_auth" ]; then
exit 46
fi
fi
if [ -e "$destination_recovery_auth" ] || [ -L "$destination_recovery_auth" ]; then
[ -f "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ] || exit 46
[ -f "$destination_recovery_path" ] && [ ! -L "$destination_recovery_path" ] || exit 46
${RECOVER_DESTINATION_AUTH_COMMAND} || exit 46
elif [ -e "$destination_recovery_path" ] || [ -L "$destination_recovery_path" ]; then
[ -f "$destination_recovery_path" ] && [ ! -L "$destination_recovery_path" ] || exit 46
rm -- "$destination_recovery_path"
fi
if [ ! -e "$source_auth" ] && [ ! -L "$source_auth" ]; then
exit ${SOURCE_AUTH_ABSENT_EXIT}
fi
[ -f "$source_auth" ] && [ ! -L "$source_auth" ] || exit 46
encode_file() {
encoded=$(base64 < "$1") || return 1
printf '%s' "$encoded" | tr -d '\n'
}
encode_file "$source_auth"
printf '\n'
source_credentials="$legacy_home/.credentials.json"
if [ -f "$source_credentials" ] && [ ! -L "$source_credentials" ]; then
printf 'present\n'
encode_file "$source_credentials"
printf '\n'
elif [ ! -e "$source_credentials" ] && [ ! -L "$source_credentials" ]; then
printf 'missing\n\n'
else
exit 44
fi
`
export const APPLY_LEGACY_AUTH_SCRIPT = `
set -eu
source_recovery_auth="$3.orca-drain-source"
source_quarantine_auth="$3.orca-drain-live-source"
destination_recovery_auth="$3.orca-drain-destination"
destination_recovery_path="$3.orca-drain-destination-path"
session_link_manifest="$3.orca-drain-session-links"
session_commit_marker="$3.orca-drain-session-commit"
session_stage_root="$3.orca-drain-session-stage"
session_scan_watermark="$3.orca-drain-session-watermark"
${ROLLBACK_SESSION_LINKS_FUNCTION}
if [ -e "$3" ] || [ -L "$3" ]; then
[ -f "$3" ] && [ ! -L "$3" ] || exit 46
commit_session_links || exit 46
if [ -f "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ]; then
chmod 600 "$destination_recovery_auth"
fi
rm -f -- "$source_recovery_auth" "$source_quarantine_auth" "$destination_recovery_auth" "$destination_recovery_path"
exit 0
fi
${RESOLVE_LEGACY_HOME_SCRIPT}
target_home=$(readlink -f -- "$4") || exit 33
[ "$legacy_home" != "$target_home" ] || exit 34
source_auth="$legacy_home/auth.json"
target_auth="$target_home/auth.json"
if [ ! -e "$source_auth" ] && [ ! -L "$source_auth" ]; then exit 35; fi
[ -f "$source_auth" ] && [ ! -L "$source_auth" ] || exit 46
if [ ! -e "$target_auth" ] && [ ! -L "$target_auth" ]; then exit 36; fi
[ -f "$target_auth" ] && [ ! -L "$target_auth" ] || exit 46
hash_file() { sha256sum -- "$1" | cut -d ' ' -f 1; }
[ "$(hash_file "$source_auth")" = "$5" ] || exit 37
[ "$(hash_file "$target_auth")" = "$6" ] || exit 38
if [ -e "$source_quarantine_auth" ] || [ -L "$source_quarantine_auth" ]; then
[ -f "$source_quarantine_auth" ] && [ ! -L "$source_quarantine_auth" ] || exit 46
rm -- "$source_quarantine_auth"
fi
if [ -e "$destination_recovery_auth" ] || [ -L "$destination_recovery_auth" ] || [ -e "$destination_recovery_path" ] || [ -L "$destination_recovery_path" ]; then
[ -f "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ] || exit 46
[ -f "$destination_recovery_path" ] && [ ! -L "$destination_recovery_path" ] || exit 46
${DISCARD_DESTINATION_RECOVERY_COMMAND} || exit 46
fi
umask 077
temporary_auth="$target_auth.orca-drain-$$"
temporary_credentials="$target_home/.credentials.json.orca-drain-$$"
temporary_previous_auth="$target_auth.orca-drain-previous-$$"
temporary_destination_auth="$target_auth.orca-drain-destination-$$"
temporary_source_auth="$source_auth.orca-drain-source-$$"
temporary_destination_snapshot="$target_auth.orca-drain-snapshot-$$"
temporary_destination_path="$3.orca-drain-destination-path-$$"
temporary_source_snapshot="$3.orca-drain-source-$$"
temporary_marker="$3.orca-drain-$$"
temporary_session_scan_watermark="$session_scan_watermark.$$"
drain_marker="$3"
expected_source_hash="$5"
cleanup() {
if [ ! -f "$drain_marker" ]; then
rollback_session_links
else
commit_session_links || :
fi
if [ ! -f "$drain_marker" ] && [ ! -e "$source_auth" ] && [ ! -L "$source_auth" ]; then
if [ -f "$source_recovery_auth" ] && [ ! -L "$source_recovery_auth" ] && [ "$(hash_file "$source_recovery_auth")" = "$expected_source_hash" ]; then
mv -- "$source_recovery_auth" "$source_auth" || :
chmod 600 "$source_auth" || :
elif [ -f "$source_quarantine_auth" ] && [ ! -L "$source_quarantine_auth" ] && [ "$(hash_file "$source_quarantine_auth")" = "$expected_source_hash" ]; then
mv -- "$source_quarantine_auth" "$source_auth" || :
chmod 600 "$source_auth" || :
fi
elif [ ! -f "$drain_marker" ]; then
# A late writer owns the recreated path; retain verified recovery artifacts for retry.
:
elif [ -f "$drain_marker" ]; then
rm -f -- "$source_recovery_auth" "$source_quarantine_auth" "$destination_recovery_path"
fi
if [ -f "$drain_marker" ]; then
if [ -f "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ]; then
chmod 600 "$destination_recovery_auth" || :
fi
rm -f -- "$destination_recovery_auth" "$destination_recovery_path"
elif [ -e "$destination_recovery_auth" ] || [ -L "$destination_recovery_auth" ]; then
if [ -f "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ] && [ -f "$destination_recovery_path" ] && [ ! -L "$destination_recovery_path" ]; then
${RECOVER_DESTINATION_AUTH_COMMAND} || :
fi
elif [ -f "$destination_recovery_path" ] && [ ! -L "$destination_recovery_path" ]; then
rm -f -- "$destination_recovery_path"
fi
if [ -f "$target_auth" ]; then
chmod 600 "$target_auth" || :
fi
rm -f -- "$temporary_auth" "$temporary_credentials" "$temporary_previous_auth" "$temporary_destination_auth" "$temporary_source_auth" "$temporary_destination_snapshot" "$temporary_destination_path" "$temporary_source_snapshot" "$temporary_marker" "$temporary_session_scan_watermark"
}
trap cleanup EXIT HUP INT TERM
if [ "$8" != 1 ]; then
session_scan_start=''; session_scan_day=$(date +%Y/%m/%d) || exit 46
if [ -f "$session_scan_watermark" ] && [ ! -L "$session_scan_watermark" ]; then
[ "\${10}" != recent ] || IFS= read -r session_scan_start < "$session_scan_watermark" || session_scan_start=''
elif [ -e "$session_scan_watermark" ] && [ ! -L "$session_scan_watermark" ]; then exit 46
fi
fi
source_credentials="$legacy_home/.credentials.json"
target_credentials="$target_home/.credentials.json"
if [ -f "$source_credentials" ] && [ ! -e "$target_credentials" ] && [ ! -L "$target_credentials" ]; then
[ "$9" != missing ] || exit 43
[ "$(hash_file "$source_credentials")" = "$9" ] || exit 43
cp -- "$source_credentials" "$temporary_credentials"
chmod 600 "$temporary_credentials"
[ "$(hash_file "$temporary_credentials")" = "$9" ] || exit 43
[ "$(hash_file "$source_credentials")" = "$9" ] || exit 43
mv -n -- "$temporary_credentials" "$target_credentials"
elif [ "$9" = missing ] && [ ! -e "$target_credentials" ] && [ ! -L "$target_credentials" ]; then
[ ! -e "$source_credentials" ] && [ ! -L "$source_credentials" ] || exit 43
fi
if [ "$7" = 1 ]; then
cp -- "$source_auth" "$temporary_auth"
chmod 600 "$temporary_auth"
# Codex rewrites auth.json in place, so this copy is a second read: verify the
# bytes being promoted, not the ones freshness was judged on.
[ "$(hash_file "$temporary_auth")" = "$5" ] || exit 42
[ "$(hash_file "$target_auth")" = "$6" ] || exit 39
# The hard link keeps the destination inode observable without creating a
# missing-path crash window. In-place writers update both names.
ln -- "$target_auth" "$temporary_previous_auth"
[ "$(hash_file "$temporary_previous_auth")" = "$6" ] || exit 39
mv -f -- "$temporary_auth" "$target_auth"
if [ "$(hash_file "$temporary_previous_auth")" != "$6" ]; then
mv -f -- "$temporary_previous_auth" "$target_auth"
exit 39
fi
rm -- "$temporary_previous_auth"
fi
if [ "$8" != 1 ]; then
expected_target_hash="$6"
[ "$7" != 1 ] || expected_target_hash="$5"
# Keep both live auth inodes observable while links are staged, then prove
# the paths still name those identities before publishing the bridge.
ln -- "$source_auth" "$temporary_source_auth"
ln -- "$target_auth" "$temporary_destination_auth"
[ "$(hash_file "$temporary_source_auth")" = "$5" ] || exit 40
[ "$(hash_file "$temporary_destination_auth")" = "$expected_target_hash" ] || exit 45
[ "$source_auth" -ef "$temporary_source_auth" ] || exit 40
[ "$target_auth" -ef "$temporary_destination_auth" ] || exit 45
if [ "\${10}" = full ]; then
${RETIRED_SESSION_BRIDGE_COMMAND}
elif [ "\${10}" = recent ]; then
case "$session_scan_start" in
????/??/??) ${RETIRED_RECENT_SESSION_BRIDGE_COMMAND} ;;
*) ${RETIRED_SESSION_BRIDGE_COMMAND} ;;
esac
else
exit 46
fi
[ "$(hash_file "$temporary_source_auth")" = "$5" ] || exit 40
[ "$(hash_file "$temporary_destination_auth")" = "$expected_target_hash" ] || exit 45
[ "$source_auth" -ef "$temporary_source_auth" ] || exit 40
[ "$target_auth" -ef "$temporary_destination_auth" ] || exit 45
rm -- "$temporary_source_auth" "$temporary_destination_auth"
commit_session_links
printf '%s\n' "$session_scan_day" > "$temporary_session_scan_watermark"
chmod 600 "$temporary_session_scan_watermark"
mv -f -- "$temporary_session_scan_watermark" "$session_scan_watermark"
fi
if [ "$8" = 1 ]; then
expected_target_hash="$6"
[ "$7" != 1 ] || expected_target_hash="$5"
# Pin the live inode and stage independent snapshots before retiring the source.
ln -- "$target_auth" "$temporary_destination_auth"
[ "$(hash_file "$temporary_destination_auth")" = "$expected_target_hash" ] || exit 45
[ "$target_auth" -ef "$temporary_destination_auth" ] || exit 45
cp -- "$target_auth" "$temporary_destination_snapshot"
chmod 400 "$temporary_destination_snapshot"
[ "$(hash_file "$temporary_destination_snapshot")" = "$expected_target_hash" ] || exit 45
cp -- "$source_auth" "$temporary_source_snapshot"
chmod 400 "$temporary_source_snapshot"
[ "$(hash_file "$temporary_source_snapshot")" = "$5" ] || exit 40
[ "$(hash_file "$source_auth")" = "$5" ] || exit 40
mv -f -- "$temporary_source_snapshot" "$source_recovery_auth"
[ "$(hash_file "$source_recovery_auth")" = "$5" ] || exit 40
printf '%s\\0' "$target_auth" > "$temporary_destination_path"
chmod 600 "$temporary_destination_path"
mv -f -- "$temporary_destination_path" "$destination_recovery_path"
ln -- "$temporary_destination_snapshot" "$destination_recovery_auth"
[ "$temporary_destination_snapshot" -ef "$destination_recovery_auth" ] || exit 45
[ "$(hash_file "$destination_recovery_auth")" = "$expected_target_hash" ] || exit 45
[ "$(hash_file "$temporary_destination_auth")" = "$expected_target_hash" ] || exit 45
[ "$target_auth" -ef "$temporary_destination_auth" ] || exit 45
[ "$(hash_file "$temporary_destination_snapshot")" = "$expected_target_hash" ] || exit 45
# The atomic replacement detaches the destination path from any writer that
# already opened the old inode; read-only mode blocks new writers until commit.
mv -f -- "$temporary_destination_snapshot" "$target_auth"
if [ "$(hash_file "$temporary_destination_auth")" != "$expected_target_hash" ]; then
mv -f -- "$temporary_destination_auth" "$target_auth"
exit 45
fi
[ "$(hash_file "$target_auth")" = "$expected_target_hash" ] || exit 45
[ "$target_auth" -ef "$destination_recovery_auth" ] || exit 45
[ "$(hash_file "$destination_recovery_auth")" = "$expected_target_hash" ] || exit 45
[ "$(hash_file "$source_auth")" = "$5" ] || exit 40
mv -- "$source_auth" "$source_quarantine_auth"
chmod 400 "$source_quarantine_auth"
[ "$(hash_file "$source_quarantine_auth")" = "$5" ] || exit 40
${RETIRED_SESSION_BRIDGE_COMMAND}
[ ! -e "$source_auth" ] && [ ! -L "$source_auth" ] || exit 40
[ "$(hash_file "$source_quarantine_auth")" = "$5" ] || exit 40
[ "$(hash_file "$target_auth")" = "$expected_target_hash" ] || exit 45
[ "$target_auth" -ef "$destination_recovery_auth" ] || exit 45
[ "$(hash_file "$destination_recovery_auth")" = "$expected_target_hash" ] || exit 45
commit_session_links
rm -- "$temporary_destination_auth"
printf '%s\n' '{"completed":true}' > "$temporary_marker"
chmod 600 "$temporary_marker"
mv -f -- "$temporary_marker" "$3"
chmod 600 "$destination_recovery_auth"
rm -- "$source_recovery_auth" "$source_quarantine_auth" "$destination_recovery_auth" "$destination_recovery_path"
fi
`
@@ -0,0 +1,90 @@
import { quotePosixShell } from '../../shared/wsl-login-shell-command'
import { WSL_CODEX_SESSION_BRIDGE_SCRIPT } from '../codex/wsl-codex-session-bridge-script'
export const RETIRED_SESSION_BRIDGE_COMMAND = `bash -c ${quotePosixShell(WSL_CODEX_SESSION_BRIDGE_SCRIPT)} bash "$legacy_home/sessions" "$target_home/sessions" "$session_link_manifest" "$session_stage_root"`
export const RETIRED_RECENT_SESSION_BRIDGE_COMMAND = `${RETIRED_SESSION_BRIDGE_COMMAND} recent "$session_scan_start" "$session_scan_day"`
const ROLLBACK_SESSION_LINKS_COMMAND = `bash -c ${quotePosixShell(`while IFS= read -r -d '' staged_file && IFS= read -r -d '' target_file; do
if [ -e "$target_file" ] || [ -L "$target_file" ]; then
[ -f "$staged_file" ] && [ ! -L "$staged_file" ] && [ "$target_file" -ef "$staged_file" ] || exit 1
rm -- "$target_file" || exit 1
fi
if [ -e "$staged_file" ] || [ -L "$staged_file" ]; then
[ -f "$staged_file" ] && [ ! -L "$staged_file" ] || exit 1
rm -- "$staged_file" || exit 1
fi
done < "$1"`)} bash`
const COMMIT_SESSION_LINKS_COMMAND = `bash -c ${quotePosixShell(`while IFS= read -r -d '' staged_file && IFS= read -r -d '' target_file; do
if [ -e "$staged_file" ] || [ -L "$staged_file" ]; then
[ -f "$staged_file" ] && [ ! -L "$staged_file" ] || exit 1
rm -- "$staged_file" || exit 1
fi
done < "$1"`)} bash`
export const RECOVER_DESTINATION_AUTH_COMMAND = `bash -c ${quotePosixShell(`IFS= read -r -d '' target_auth < "$1" || exit 1
case "$target_auth" in /*) ;; *) exit 1 ;; esac
if [ ! -e "$target_auth" ] && [ ! -L "$target_auth" ]; then
mv -- "$2" "$target_auth" || exit 1
chmod 600 "$target_auth" || exit 1
rm -- "$1" || exit 1
elif [ -f "$target_auth" ] && [ ! -L "$target_auth" ] && [ "$target_auth" -ef "$2" ]; then
chmod 600 "$target_auth" || exit 1
rm -- "$2" "$1" || exit 1
else
chmod 600 "$2" || exit 1
fi`)} bash "$destination_recovery_path" "$destination_recovery_auth"`
export const DISCARD_DESTINATION_RECOVERY_COMMAND = `bash -c ${quotePosixShell(`IFS= read -r -d '' recorded_target < "$1" || exit 1
[ "$recorded_target" = "$3" ] || exit 1
chmod 600 "$2" "$3" || exit 1
rm -- "$2" "$1" || exit 1`)} bash "$destination_recovery_path" "$destination_recovery_auth" "$target_auth"`
export const ROLLBACK_SESSION_LINKS_FUNCTION = `
rollback_session_links() {
if [ -e "$session_commit_marker" ] || [ -L "$session_commit_marker" ]; then
[ -f "$session_commit_marker" ] && [ ! -L "$session_commit_marker" ] || return 1
commit_session_links || return 1
return 0
fi
if [ -f "$session_link_manifest" ] && [ ! -L "$session_link_manifest" ]; then
${ROLLBACK_SESSION_LINKS_COMMAND} "$session_link_manifest" || return 1
rm -- "$session_link_manifest" || return 1
elif [ -e "$session_link_manifest" ] || [ -L "$session_link_manifest" ]; then
return 1
fi
rm -rf -- "$session_stage_root" || return 1
}
commit_session_links() {
if [ ! -e "$session_commit_marker" ] && [ ! -L "$session_commit_marker" ]; then
: > "$session_commit_marker" || return 1
fi
[ -f "$session_commit_marker" ] && [ ! -L "$session_commit_marker" ] || return 1
if [ -f "$session_link_manifest" ] && [ ! -L "$session_link_manifest" ]; then
${COMMIT_SESSION_LINKS_COMMAND} "$session_link_manifest" || return 1
rm -- "$session_link_manifest" || return 1
elif [ -e "$session_link_manifest" ] || [ -L "$session_link_manifest" ]; then
return 1
fi
rm -rf -- "$session_stage_root" || return 1
rm -- "$session_commit_marker" || return 1
}
`
export const RESOLVE_LEGACY_HOME_SCRIPT = `
legacy_home="$1"
legacy_home_resolved=0
if [ -e "$1" ] || [ -L "$1" ]; then
legacy_home=$(readlink -f -- "$1") || exit 30
legacy_home_resolved=1
fi
if [ -e "$2" ] || [ -L "$2" ]; then
active_home=$(readlink -f -- "$2") || exit 31
if [ "$legacy_home_resolved" = 1 ]; then
[ "$active_home" = "$legacy_home" ] || exit 32
else
legacy_home="$active_home"
legacy_home_resolved=1
fi
fi
`
@@ -0,0 +1,307 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { runWslProcessMock } = vi.hoisted(() => ({ runWslProcessMock: vi.fn() }))
vi.mock('../wsl/wsl-runner', () => ({
runWslProcess: runWslProcessMock
}))
import {
_internals,
drainLegacyWslRuntimeAuth,
startLegacyWslRuntimeAuthDrain
} from './legacy-wsl-runtime-auth-drain'
import { readWslCodexAuths } from './wsl-codex-auth-batch-reader'
const SOURCE_AUTH = '{"tokens":{"expires_at":2000}}\n'
const STALE_AUTH = '{"tokens":{"expires_at":1000}}\n'
const NEWER_AUTH = '{"tokens":{"expires_at":3000}}\n'
function inspection(auth: string, credentials?: string): string {
return [
Buffer.from(auth).toString('base64'),
credentials === undefined ? 'missing' : 'present',
credentials === undefined ? '' : Buffer.from(credentials).toString('base64')
].join('\n')
}
function result(code: number, stdout = '') {
return {
code,
stdout,
stderr: '',
timedOut: false,
environmentResolved: true
}
}
describe('legacy WSL runtime auth drain', () => {
beforeEach(() => {
runWslProcessMock.mockReset()
_internals.resetDrainQueue()
})
it('promotes fresher auth guest-side while a legacy pane remains', async () => {
runWslProcessMock
.mockResolvedValueOnce(result(0, inspection(SOURCE_AUTH)))
.mockResolvedValueOnce(result(0))
const resolveDestination = vi.fn(() => ({
authContents: STALE_AUTH,
linuxHomePath: '/home/alice/.local/share/orca/codex-accounts/account-1/home'
}))
await drainLegacyWslRuntimeAuth({
distro: 'Ubuntu',
guestHomeLinuxPath: '/home/alice',
legacyPanePresent: true,
resolveDestination
})
expect(resolveDestination).toHaveBeenCalledWith(SOURCE_AUTH)
expect(runWslProcessMock).toHaveBeenCalledTimes(2)
expect(runWslProcessMock.mock.calls[1]?.[0].args.slice(3)).toEqual([
'/home/alice/.local/share/orca/codex-accounts/account-1/home',
expect.any(String),
expect.any(String),
'1',
'0',
'missing',
'full'
])
expect(runWslProcessMock.mock.calls[1]?.[0].script).toContain('readlink -f')
expect(runWslProcessMock.mock.calls[1]?.[0].script).toContain('source_credentials=')
expect(runWslProcessMock.mock.calls[1]?.[0].script).toContain('chmod 600')
expect(runWslProcessMock.mock.calls[1]?.[0].timeoutMs).toBe(30_000)
})
it('bridges sessions without changing auth when freshness cannot be proven', async () => {
runWslProcessMock
.mockResolvedValueOnce(result(0, inspection('{"tokens":{}}\n')))
.mockResolvedValueOnce(result(0))
await drainLegacyWslRuntimeAuth({
distro: 'Ubuntu',
guestHomeLinuxPath: '/home/alice',
legacyPanePresent: false,
resolveDestination: () => ({
authContents: '{"tokens":{}}\n',
linuxHomePath: '/home/alice/.codex'
})
})
expect(runWslProcessMock).toHaveBeenCalledTimes(2)
expect(runWslProcessMock.mock.calls[1]?.[0].args.slice(-4)).toEqual([
'0',
'0',
'missing',
'full'
])
})
it('refuses a source with no unique destination', async () => {
runWslProcessMock.mockResolvedValueOnce(result(0, inspection(SOURCE_AUTH)))
await drainLegacyWslRuntimeAuth({
distro: 'Ubuntu',
guestHomeLinuxPath: '/home/alice',
legacyPanePresent: false,
resolveDestination: () => null
})
expect(runWslProcessMock).toHaveBeenCalledTimes(1)
})
it('retires stale legacy auth only after the last recorded pane exits', async () => {
runWslProcessMock
.mockResolvedValueOnce(result(0, inspection(SOURCE_AUTH)))
.mockResolvedValueOnce(result(0))
await drainLegacyWslRuntimeAuth({
distro: 'Ubuntu',
guestHomeLinuxPath: '/home/alice',
legacyPanePresent: false,
resolveDestination: () => ({
authContents: NEWER_AUTH,
linuxHomePath: '/home/alice/.codex'
})
})
expect(runWslProcessMock.mock.calls[1]?.[0].args.slice(-4)).toEqual([
'0',
'1',
'missing',
'full'
])
expect(runWslProcessMock.mock.calls[1]?.[0].timeoutMs).toBe(30_000)
})
it('recovers a failed guest apply before resolving the drain as pending', async () => {
runWslProcessMock
.mockResolvedValueOnce(result(0, inspection(SOURCE_AUTH)))
.mockResolvedValueOnce(result(45))
.mockResolvedValueOnce(result(0, inspection(SOURCE_AUTH)))
await expect(
drainLegacyWslRuntimeAuth({
distro: 'Ubuntu',
guestHomeLinuxPath: '/home/alice',
legacyPanePresent: false,
resolveDestination: () => ({
authContents: NEWER_AUTH,
linuxHomePath: '/home/alice/.codex'
})
})
).resolves.toBe('pending')
expect(runWslProcessMock).toHaveBeenCalledTimes(3)
expect(runWslProcessMock.mock.calls[2]?.[0]).toEqual(
expect.objectContaining({
script: _internals.inspectLegacyAuthScript,
timeoutMs: 5_000
})
)
})
it('rejects an awaited launch drain when guest recovery cannot finish', async () => {
runWslProcessMock
.mockResolvedValueOnce(result(0, inspection(SOURCE_AUTH)))
.mockResolvedValueOnce(result(45))
.mockResolvedValueOnce(result(46))
await expect(
startLegacyWslRuntimeAuthDrain(
{
distro: 'Ubuntu',
guestHomeLinuxPath: '/home/alice',
legacyPanePresent: false,
resolveDestination: () => ({
authContents: NEWER_AUTH,
linuxHomePath: '/home/alice/.codex'
})
},
{ throwOnFailure: true }
)
).rejects.toThrow('Legacy WSL auth drain recover failed')
})
it('keeps an absent source retryable while the legacy home remains', async () => {
runWslProcessMock.mockResolvedValueOnce(result(21))
await drainLegacyWslRuntimeAuth({
distro: 'Ubuntu',
guestHomeLinuxPath: '/home/alice',
legacyPanePresent: false,
resolveDestination: vi.fn()
})
expect(runWslProcessMock).toHaveBeenCalledTimes(1)
})
it('does nothing after the guest-side completion marker is present', async () => {
runWslProcessMock.mockResolvedValueOnce(result(20))
const resolveDestination = vi.fn()
await drainLegacyWslRuntimeAuth({
distro: 'Ubuntu',
guestHomeLinuxPath: '/home/alice',
legacyPanePresent: false,
resolveDestination
})
expect(resolveDestination).not.toHaveBeenCalled()
expect(runWslProcessMock).toHaveBeenCalledTimes(1)
})
it('marks an absent legacy home complete after every legacy pane exits', async () => {
runWslProcessMock.mockResolvedValueOnce(result(22)).mockResolvedValueOnce(result(0))
await drainLegacyWslRuntimeAuth({
distro: 'Ubuntu',
guestHomeLinuxPath: '/home/alice',
legacyPanePresent: false,
resolveDestination: vi.fn()
})
expect(runWslProcessMock).toHaveBeenCalledTimes(2)
expect(runWslProcessMock.mock.calls[1]?.[0].args).toEqual([
'/home/alice/.local/share/orca/codex-runtime-home/home',
'/home/alice/.local/share/orca/codex-runtime-home/active/wsl/home',
'/home/alice/.local/share/orca/codex-runtime-home/direct-home-auth-drain-v1.json'
])
})
it('coalesces concurrent drain triggers instead of queueing every poll', async () => {
runWslProcessMock.mockImplementation(() => new Promise(() => {}))
const options = {
distro: 'Ubuntu',
guestHomeLinuxPath: '/home/alice',
legacyPanePresent: true,
resolveDestination: () => null
}
startLegacyWslRuntimeAuthDrain(options)
startLegacyWslRuntimeAuthDrain(options)
await Promise.resolve()
expect(runWslProcessMock).toHaveBeenCalledTimes(1)
})
it('bounds sequential pending launches to recent session dates', async () => {
runWslProcessMock.mockImplementation((options: { script: string }) =>
Promise.resolve(
options.script === _internals.inspectLegacyAuthScript
? result(0, inspection(SOURCE_AUTH))
: result(0)
)
)
const options = {
distro: 'Ubuntu',
guestHomeLinuxPath: '/home/alice',
legacyPanePresent: true,
resolveDestination: () => ({
authContents: STALE_AUTH,
linuxHomePath: '/home/alice/.local/share/orca/codex-accounts/account-1/home'
})
}
await startLegacyWslRuntimeAuthDrain(options, { throwOnFailure: true })
await startLegacyWslRuntimeAuthDrain(options, { throwOnFailure: true })
await startLegacyWslRuntimeAuthDrain(
{ ...options, legacyPanePresent: false },
{ throwOnFailure: true }
)
const applyCalls = runWslProcessMock.mock.calls.filter(
([call]) => call.script === _internals.applyLegacyAuthScript
)
expect(applyCalls).toHaveLength(3)
expect(applyCalls.map(([call]) => call.args.at(-1))).toEqual(['full', 'recent', 'full'])
expect(applyCalls.map(([call]) => call.timeoutMs)).toEqual([30_000, 5_000, 30_000])
})
it('reads every candidate home in one bounded guest process', async () => {
runWslProcessMock.mockResolvedValueOnce(
result(
0,
[`present:${Buffer.from(SOURCE_AUTH).toString('base64')}`, 'missing', 'unreadable'].join(
'\n'
)
)
)
await expect(
readWslCodexAuths('Ubuntu', ['/home/alice/.codex-a', '/home/alice/.codex-b', '/bad'])
).resolves.toEqual([
{ kind: 'present', contents: SOURCE_AUTH },
{ kind: 'missing' },
{ kind: 'unreadable' }
])
expect(runWslProcessMock).toHaveBeenCalledTimes(1)
expect(runWslProcessMock).toHaveBeenCalledWith(
expect.objectContaining({
args: ['/home/alice/.codex-a', '/home/alice/.codex-b', '/bad'],
maxOutputBytes: 2 * 1024 * 1024,
timeoutMs: 5_000
})
)
})
})
@@ -0,0 +1,269 @@
import { createHash } from 'node:crypto'
import { posix as pathPosix } from 'node:path'
import { wslCodexRuntimeHomeForGuestHome } from '../pty/codex-home-wsl-env'
import { WSL_SESSION_BRIDGE_TIMEOUT_MS } from '../codex/wsl-codex-session-bridge-script'
import { runWslProcess } from '../wsl/wsl-runner'
import { compareCodexAuthFreshness, codexAuthIsFresher } from './codex-auth-identity'
import {
APPLY_LEGACY_AUTH_SCRIPT,
FINALIZE_ABSENT_AUTH_SCRIPT,
INSPECT_LEGACY_AUTH_SCRIPT,
LEGACY_HOME_ABSENT_EXIT,
LEGACY_HOME_STILL_PRESENT_EXIT,
MARKER_PRESENT_EXIT,
SOURCE_AUTH_ABSENT_EXIT
} from './legacy-wsl-runtime-auth-drain-scripts'
import { decodeWslBase64Payload } from './wsl-codex-auth-batch-reader'
const DRAIN_MARKER_NAME = 'direct-home-auth-drain-v1.json'
export type LegacyWslRuntimeAuthDestination = {
authContents: string
linuxHomePath: string
}
type LegacyWslRuntimeInspection = {
authContents: string
credentials: { kind: 'missing' } | { kind: 'present'; contents: string }
}
type LegacyWslRuntimeAuthDrainOptions = {
distro: string
guestHomeLinuxPath: string
legacyPanePresent: boolean
resolveDestination: (
runtimeAuthContents: string
) => LegacyWslRuntimeAuthDestination | null | Promise<LegacyWslRuntimeAuthDestination | null>
}
const drainQueueByDistro = new Map<string, Promise<void>>()
const completedDistroKeys = new Set<string>()
const pendingSessionBridgeRouteByDistro = new Map<string, string>()
export function startLegacyWslRuntimeAuthDrain(
options: LegacyWslRuntimeAuthDrainOptions,
startOptions: { throwOnFailure?: boolean } = {}
): Promise<void> {
const key = options.distro.trim().toLowerCase()
if (completedDistroKeys.has(key)) {
return Promise.resolve()
}
// Coalesce launch/rate-limit callers while a drain is in flight. Queuing a
// new pass for every poll can otherwise build an unbounded promise chain
// while a legacy pane keeps the migration pending.
const inFlight = drainQueueByDistro.get(key)
if (inFlight) {
return startOptions.throwOnFailure ? inFlight : logDrainFailure(inFlight)
}
const next = drainLegacyWslRuntimeAuth(options).then((status) => {
if (status === 'complete') {
completedDistroKeys.add(key)
}
})
drainQueueByDistro.set(key, next)
const clearQueue = (): void => {
if (drainQueueByDistro.get(key) === next) {
drainQueueByDistro.delete(key)
}
}
void next.then(clearQueue, clearQueue)
return startOptions.throwOnFailure ? next : logDrainFailure(next)
}
function logDrainFailure(task: Promise<void>): Promise<void> {
return task.catch((error) => {
console.warn('[codex-wsl-auth-drain] Failed to drain legacy runtime auth:', error)
})
}
export async function drainLegacyWslRuntimeAuth(
options: LegacyWslRuntimeAuthDrainOptions
): Promise<'complete' | 'pending'> {
const distroKey = options.distro.trim().toLowerCase()
const paths = resolveLegacyRuntimePaths(options.guestHomeLinuxPath)
const inspection = await runWslProcess({
distro: options.distro,
loginPath: 'none',
script: INSPECT_LEGACY_AUTH_SCRIPT,
args: [paths.runtimeHome, paths.activeHome, paths.marker],
timeoutMs: 5_000,
maxOutputBytes: 2 * 1024 * 1024
})
if (inspection.code === MARKER_PRESENT_EXIT) {
return 'complete'
}
if (inspection.code === LEGACY_HOME_ABSENT_EXIT) {
if (!options.legacyPanePresent) {
return finalizeAbsentLegacyAuth(options.distro, paths)
}
return 'pending'
}
if (inspection.code === SOURCE_AUTH_ABSENT_EXIT) {
return 'pending'
}
assertSuccessfulDrainStep('inspect', inspection)
const inspected = parseLegacyRuntimeInspection(inspection.stdout)
if (!inspected) {
return 'pending'
}
const destination = await options.resolveDestination(inspected.authContents)
if (!destination) {
return 'pending'
}
const freshness = compareCodexAuthFreshness(inspected.authContents, destination.authContents)
const promoteAuth =
freshness !== null && codexAuthIsFresher(inspected.authContents, destination.authContents)
const deleteSource = !options.legacyPanePresent && freshness !== null
const sessionBridgeRoute = [
paths.runtimeHome,
destination.linuxHomePath,
options.legacyPanePresent ? 'retained' : 'released'
].join('\0')
const bridgeAllSessions =
deleteSource || pendingSessionBridgeRouteByDistro.get(distroKey) !== sessionBridgeRoute
const result = await runWslProcess({
distro: options.distro,
loginPath: 'none',
script: APPLY_LEGACY_AUTH_SCRIPT,
args: [
paths.runtimeHome,
paths.activeHome,
paths.marker,
destination.linuxHomePath,
sha256(inspected.authContents),
sha256(destination.authContents),
promoteAuth ? '1' : '0',
deleteSource ? '1' : '0',
inspected.credentials.kind === 'present' ? sha256(inspected.credentials.contents) : 'missing',
bridgeAllSessions ? 'full' : 'recent'
],
timeoutMs: bridgeAllSessions ? WSL_SESSION_BRIDGE_TIMEOUT_MS : 5_000,
maxOutputBytes: 16 * 1024
})
try {
assertSuccessfulDrainStep('apply', result)
} catch {
return recoverAfterFailedApply(options.distro, paths)
}
if (!deleteSource) {
pendingSessionBridgeRouteByDistro.set(distroKey, sessionBridgeRoute)
}
return deleteSource ? 'complete' : 'pending'
}
async function recoverAfterFailedApply(
distro: string,
paths: ReturnType<typeof resolveLegacyRuntimePaths>
): Promise<'complete' | 'pending'> {
const recovery = await runWslProcess({
distro,
loginPath: 'none',
script: INSPECT_LEGACY_AUTH_SCRIPT,
args: [paths.runtimeHome, paths.activeHome, paths.marker],
timeoutMs: 5_000,
maxOutputBytes: 2 * 1024 * 1024
})
if (recovery.code === MARKER_PRESENT_EXIT) {
return 'complete'
}
if (
!recovery.timedOut &&
(recovery.code === 0 ||
recovery.code === SOURCE_AUTH_ABSENT_EXIT ||
recovery.code === LEGACY_HOME_ABSENT_EXIT)
) {
return 'pending'
}
assertSuccessfulDrainStep('recover', recovery)
return 'pending'
}
function parseLegacyRuntimeInspection(stdout: string): LegacyWslRuntimeInspection | null {
const [authBase64, credentialsKind, credentialsBase64] = stdout.split('\n')
const authContents = decodeWslBase64Payload(authBase64 ?? '')
if (authContents === null) {
return null
}
if (credentialsKind === 'missing') {
return { authContents, credentials: { kind: 'missing' } }
}
if (credentialsKind !== 'present') {
return null
}
const credentialsContents = decodeWslBase64Payload(credentialsBase64 ?? '')
if (!credentialsContents || !isJsonObject(credentialsContents)) {
return null
}
return { authContents, credentials: { kind: 'present', contents: credentialsContents } }
}
function isJsonObject(contents: string): boolean {
try {
const value = JSON.parse(contents) as unknown
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
} catch {
return false
}
}
function resolveLegacyRuntimePaths(guestHomeLinuxPath: string): {
activeHome: string
marker: string
runtimeHome: string
} {
const runtimeHome = wslCodexRuntimeHomeForGuestHome(guestHomeLinuxPath)
const runtimeRoot = pathPosix.dirname(runtimeHome)
return {
activeHome: pathPosix.join(runtimeRoot, 'active', 'wsl', 'home'),
marker: pathPosix.join(runtimeRoot, DRAIN_MARKER_NAME),
runtimeHome
}
}
async function finalizeAbsentLegacyAuth(
distro: string,
paths: ReturnType<typeof resolveLegacyRuntimePaths>
): Promise<'complete' | 'pending'> {
const result = await runWslProcess({
distro,
loginPath: 'none',
script: FINALIZE_ABSENT_AUTH_SCRIPT,
args: [paths.runtimeHome, paths.activeHome, paths.marker],
timeoutMs: 5_000,
maxOutputBytes: 16 * 1024
})
if (result.code === LEGACY_HOME_STILL_PRESENT_EXIT) {
return 'pending'
}
assertSuccessfulDrainStep('finalize', result)
return 'complete'
}
function assertSuccessfulDrainStep(
step: string,
result: { code: number | null; stderr: string; timedOut: boolean }
): void {
if (result.code === 0 && !result.timedOut) {
return
}
const detail = result.stderr.trim()
throw new Error(
`Legacy WSL auth drain ${step} failed (${result.timedOut ? 'timeout' : `exit ${result.code}`})${detail ? `: ${detail}` : ''}`
)
}
function sha256(contents: string): string {
return createHash('sha256').update(contents).digest('hex')
}
export const _internals = {
applyLegacyAuthScript: APPLY_LEGACY_AUTH_SCRIPT,
finalizeAbsentAuthScript: FINALIZE_ABSENT_AUTH_SCRIPT,
inspectLegacyAuthScript: INSPECT_LEGACY_AUTH_SCRIPT,
resetDrainQueue: (): void => {
drainQueueByDistro.clear()
completedDistroKeys.clear()
pendingSessionBridgeRouteByDistro.clear()
}
}
@@ -0,0 +1,71 @@
import { LEGACY_HOME_STILL_PRESENT_EXIT } from './legacy-wsl-runtime-auth-drain-exit-codes'
import {
RECOVER_DESTINATION_AUTH_COMMAND,
RESOLVE_LEGACY_HOME_SCRIPT,
ROLLBACK_SESSION_LINKS_FUNCTION
} from './legacy-wsl-runtime-auth-drain-shell-commands'
export const FINALIZE_ABSENT_AUTH_SCRIPT = `
set -eu
source_recovery_auth="$3.orca-drain-source"
source_quarantine_auth="$3.orca-drain-live-source"
destination_recovery_auth="$3.orca-drain-destination"
destination_recovery_path="$3.orca-drain-destination-path"
session_link_manifest="$3.orca-drain-session-links"
session_commit_marker="$3.orca-drain-session-commit"
session_stage_root="$3.orca-drain-session-stage"
${ROLLBACK_SESSION_LINKS_FUNCTION}
if [ -e "$3" ] || [ -L "$3" ]; then
[ -f "$3" ] && [ ! -L "$3" ] || exit 46
commit_session_links || exit 46
if [ -f "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ]; then
chmod 600 "$destination_recovery_auth"
fi
rm -f -- "$source_recovery_auth" "$source_quarantine_auth" "$destination_recovery_auth" "$destination_recovery_path"
exit 0
fi
rollback_session_links
${RESOLVE_LEGACY_HOME_SCRIPT}
if [ -f "$source_recovery_auth" ] && [ ! -L "$source_recovery_auth" ]; then
[ ! -e "$legacy_home/auth.json" ] && [ ! -L "$legacy_home/auth.json" ] || exit 41
mv -- "$source_recovery_auth" "$legacy_home/auth.json"
chmod 600 "$legacy_home/auth.json"
if [ -e "$destination_recovery_auth" ] || [ -L "$destination_recovery_auth" ]; then
[ -f "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ] || exit 46
[ -f "$destination_recovery_path" ] && [ ! -L "$destination_recovery_path" ] || exit 46
${RECOVER_DESTINATION_AUTH_COMMAND} || exit 46
elif [ -e "$destination_recovery_path" ] || [ -L "$destination_recovery_path" ]; then
[ -f "$destination_recovery_path" ] && [ ! -L "$destination_recovery_path" ] || exit 46
rm -- "$destination_recovery_path"
fi
exit 46
fi
if [ -f "$source_quarantine_auth" ] && [ ! -L "$source_quarantine_auth" ]; then
[ ! -e "$legacy_home/auth.json" ] && [ ! -L "$legacy_home/auth.json" ] || exit 41
mv -- "$source_quarantine_auth" "$legacy_home/auth.json"
chmod 600 "$legacy_home/auth.json"
if [ -e "$destination_recovery_auth" ] || [ -L "$destination_recovery_auth" ]; then
[ -f "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ] || exit 46
[ -f "$destination_recovery_path" ] && [ ! -L "$destination_recovery_path" ] || exit 46
${RECOVER_DESTINATION_AUTH_COMMAND} || exit 46
elif [ -e "$destination_recovery_path" ] || [ -L "$destination_recovery_path" ]; then
[ -f "$destination_recovery_path" ] && [ ! -L "$destination_recovery_path" ] || exit 46
rm -- "$destination_recovery_path"
fi
exit 46
fi
[ ! -e "$source_recovery_auth" ] && [ ! -L "$source_recovery_auth" ] || exit 46
[ ! -e "$source_quarantine_auth" ] && [ ! -L "$source_quarantine_auth" ] || exit 46
[ ! -e "$destination_recovery_auth" ] && [ ! -L "$destination_recovery_auth" ] || exit 46
[ ! -e "$destination_recovery_path" ] && [ ! -L "$destination_recovery_path" ] || exit 46
[ ! -e "$legacy_home/auth.json" ] && [ ! -L "$legacy_home/auth.json" ] || exit 41
[ "$legacy_home_resolved" = 0 ] || exit ${LEGACY_HOME_STILL_PRESENT_EXIT}
umask 077
marker_parent=\${3%/*}
mkdir -p -- "$marker_parent"
temporary_marker="$3.orca-drain-$$"
trap 'rm -f -- "$temporary_marker"' EXIT HUP INT TERM
printf '%s\n' '{"completed":true}' > "$temporary_marker"
chmod 600 "$temporary_marker"
mv -f -- "$temporary_marker" "$3"
`
+245 -378
View File
@@ -1,5 +1,4 @@
/* eslint-disable max-lines -- Why: keeps Codex's whole runtime-home contract in one place so account-switch semantics don't drift across launch/login/quota paths. */
import { quotePosixShell } from '../../shared/wsl-login-shell-command'
import {
appendFileSync,
copyFileSync,
@@ -18,13 +17,13 @@ import {
unlinkSync
} from 'node:fs'
import { isDefinitiveAbsence } from '../../shared/definitive-filesystem-absence'
import { execFileSync } from 'node:child_process'
import {
dirname,
extname,
isAbsolute,
join,
parse,
posix as pathPosix,
relative,
resolve,
win32 as pathWin32
@@ -33,7 +32,6 @@ import { app } from 'electron'
import type { CodexManagedAccount } from '../../shared/managed-account-types'
import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path'
import type { Store } from '../persistence'
import { WSL_CODEX_RUNTIME_HOME_SEGMENTS } from '../pty/codex-home-wsl-env'
import {
recoverInterruptedGuardedFileOperation,
removeFileAtomicallyIfUnchanged,
@@ -55,16 +53,13 @@ import {
resolveWslCodexSessionSourceHome
} from '../codex/codex-session-source-home'
import { startWslCodexSessionBridgeInBackground } from '../codex/wsl-codex-session-bridge'
import { syncSystemConfigIntoManagedCodexHome } from '../codex/codex-config-mirror'
import { parseWslUncPath, toLinuxPath, toWindowsWslUncPath } from '../../shared/wsl-paths'
import {
prepareSystemConfigForFreshRuntimeMirror,
syncSystemConfigIntoManagedCodexHome
} from '../codex/codex-config-mirror'
import { parseWslUncPath, toLinuxPath } from '../../shared/wsl-paths'
import {
getCodexSelectionLaneKey,
getWslSelectionKey,
getSelectedCodexAccountIdForTarget,
normalizeCodexRuntimeSelection,
setSelectedCodexAccountIdForTarget,
type CodexAccountSelectionTarget
} from './runtime-selection'
import { getDefaultWslDistro, getWslHome } from '../wsl'
@@ -91,10 +86,16 @@ import { CodexCredentialAbsenceGrace } from './codex-credential-absence-grace'
import { syncLegacySharedCodexConfigForRetainedPanes } from './legacy-shared-config-compatibility'
import {
getCodexPaneAccount,
hasRecordedLegacyWslCodexPane,
hasRecordedLegacySharedCodexPane,
type CodexPaneHomeRoute
} from '../codex/codex-pane-account-registry'
import { isShellStartupEnvProbeSupported } from '../pty/shell-startup-env'
import {
startLegacyWslRuntimeAuthDrain,
type LegacyWslRuntimeAuthDestination
} from './legacy-wsl-runtime-auth-drain'
import { readWslCodexAuths, type WslCodexAuthRead } from './wsl-codex-auth-batch-reader'
type CodexSystemDefaultSnapshot = {
authJson: string | null
@@ -130,7 +131,6 @@ type CodexRuntimeLogoutMarkerStatus =
| { kind: 'applies' }
| { kind: 'system-default-changed'; systemDefaultAuthJson: string | null }
type CodexReadBackResult = 'unchanged' | 'persisted' | 'rejected'
type CodexReadBackMatch =
| {
kind: 'matched'
@@ -195,11 +195,6 @@ export class CodexRuntimeHomeService {
private lastSyncedAccountId: string | null = null
// Last auth.json Orca wrote to the runtime home; a later diff signals an out-of-band change (Codex token refresh, or external login to adopt).
private lastWrittenAuthJson: string | null = null
// Why: WSL terminals have per-distro runtime homes; sharing the host baseline can make stale WSL auth look newer than managed storage.
private readonly lastWrittenWslAuthJsonByDistro = new Map<string, string | null>()
private readonly lastSyncedWslAccountIdByDistro = new Map<string, string | null>()
private readonly wslRuntimeHomePathByDistro = new Map<string, string>()
private skipNextReadBackForAccountId: string | null = null
// Why: a managed host account refreshes auth in its own home. Remember that
// provenance so a later deselect never adopts stale shared bytes.
private lastHostAccountUsedSelfContainedHome = false
@@ -209,7 +204,6 @@ export class CodexRuntimeHomeService {
private hostSystemDefaultSessionMigrationPending = false
private pendingHostSystemDefaultSessionMigrationNeedsFullScan = false
private pendingHostSystemDefaultSessionMigrationTarget: string | null = null
constructor(private readonly store: Store) {
this.safeRecoverInterruptedRuntimeAuthOperation()
this.safeMigrateLegacySharedAuth()
@@ -244,11 +238,10 @@ export class CodexRuntimeHomeService {
): string | null {
if (target?.runtime === 'wsl') {
const wslTarget = this.resolveWslDefaultTarget(target)
const syncedRuntimeHomePath = this.syncWslRuntimeForCurrentSelection(wslTarget)
this.syncWslConfigAndGlobalInstructionsForLaunch(wslTarget, syncedRuntimeHomePath)
const runtimeHomePath = syncedRuntimeHomePath ?? this.getWslSystemCodexHomePath(wslTarget)
this.startWslSessionBridgeForLaunch(wslTarget, runtimeHomePath)
return runtimeHomePath
const homePath = this.getWslCodexHomePathForSelection(wslTarget)
this.startLegacyWslAuthDrain(wslTarget)
this.finishWslLaunchPreparation(wslTarget, homePath)
return homePath
}
const selfContainedAccount = this.getSelfContainedManagedHostAccount()
if (selfContainedAccount) {
@@ -281,6 +274,23 @@ export class CodexRuntimeHomeService {
return this.getRuntimeHomePath()
}
async prepareForCodexLaunchAsync(
target?: CodexAccountSelectionTarget,
launchEnv?: NodeJS.ProcessEnv,
options?: { unavailableManagedHomePath?: string }
): Promise<string | null> {
if (target?.runtime !== 'wsl') {
return this.prepareForCodexLaunch(target, launchEnv, options)
}
const wslTarget = this.resolveWslDefaultTarget(target)
const homePath = this.getWslCodexHomePathForSelection(wslTarget)
// Why: the retired home may hold the freshest credential, so the first
// direct-home Codex spawn must wait for its bounded guest transaction.
await this.startLegacyWslAuthDrain(wslTarget, { throwOnFailure: true })
this.finishWslLaunchPreparation(wslTarget, homePath)
return homePath
}
beginHostSystemDefaultSessionMigrationLaunch(
codexHomePath: string | null,
options: { reattached?: boolean; launchEnv?: NodeJS.ProcessEnv } = {}
@@ -375,9 +385,18 @@ export class CodexRuntimeHomeService {
}
private getManagedHostAccountHomesForSessionDiscovery(): string[] {
return this.getManagedAccountHomesForSessionDiscovery().filter(
(home) => parseWslUncPath(home) === null
)
const settings = this.store.getSettings()
const homes: string[] = []
for (const account of settings.codexManagedAccounts) {
if (this.getWslManagedHomePath(account)) {
continue
}
const trustedHome = this.getTrustedSelfContainedManagedHomePath(account)
if (trustedHome) {
homes.push(trustedHome)
}
}
return homes
}
private prepareSelfContainedManagedHomeForLaunch(
@@ -570,15 +589,14 @@ export class CodexRuntimeHomeService {
const systemCodexHomePath =
resolveWslCodexSessionSourceHome(this.store.getSettings(), distro) ??
this.getWslSystemCodexHomePath({ runtime: 'wsl', wslDistro: distro })
if (!systemCodexHomePath || systemCodexHomePath === runtimeHomePath) {
return
if (systemCodexHomePath && systemCodexHomePath !== runtimeHomePath) {
// Why: WSL history must be hardlinked inside the distro; host-side links can't bridge Windows and WSL filesystems in a resume-visible way.
void startWslCodexSessionBridgeInBackground({
distro,
systemCodexHomePath,
managedCodexHomePath: runtimeHomePath
})
}
// Why: WSL history must be hardlinked inside the distro; host-side links can't bridge Windows and WSL filesystems in a resume-visible way.
void startWslCodexSessionBridgeInBackground({
distro,
systemCodexHomePath,
managedCodexHomePath: runtimeHomePath
})
}
getHostCodexHomePathsForSessionDiscovery(): string[] {
@@ -724,26 +742,6 @@ export class CodexRuntimeHomeService {
syncLegacySharedCodexConfigForRetainedPanes()
}
syncActiveWslSelectionsBeforeRestart(): void {
if (process.platform !== 'win32') {
return
}
const settings = this.store.getSettings()
for (const [selectedDistroKey, accountId] of Object.entries(
normalizeCodexRuntimeSelection(settings).wsl
)) {
if (!accountId) {
continue
}
const account = this.getActiveAccount(settings.codexManagedAccounts, accountId)
if (!account || account.managedHomeRuntime !== 'wsl') {
continue
}
this.safeReadBackActiveWslAccountBeforeRestart(account, selectedDistroKey)
}
}
private getWslSystemCodexHomePath(target: CodexAccountSelectionTarget): string | null {
if (process.platform !== 'win32') {
return null
@@ -753,9 +751,23 @@ export class CodexRuntimeHomeService {
return null
}
const home = getWslHome(distro)
if (home && /^[A-Za-z]:[\\/]/.test(home)) {
const linuxHome = toLinuxPath(home).trim()
return linuxHome.startsWith('/')
? toWindowsWslUncPath(pathPosix.join(linuxHome, '.codex'), distro)
: null
}
return home ? this.joinWslPath(home, '.codex') : null
}
private finishWslLaunchPreparation(
target: CodexAccountSelectionTarget,
homePath: string | null
): void {
this.syncWslConfigAndGlobalInstructionsForLaunch(target, homePath)
this.startWslSessionBridgeForLaunch(target, homePath)
}
private syncWslConfigAndGlobalInstructionsForLaunch(
target: CodexAccountSelectionTarget,
runtimeHomePath: string | null
@@ -790,10 +802,9 @@ export class CodexRuntimeHomeService {
prepareForRateLimitFetch(target?: CodexAccountSelectionTarget): CodexRateLimitHomeResolution {
if (target?.runtime === 'wsl') {
const wslTarget = this.resolveWslDefaultTarget(target)
const syncedRuntimeHomePath = this.getPreparedWslRateLimitHomePath(wslTarget)
return {
kind: 'ready',
codexHomePath: syncedRuntimeHomePath ?? this.getWslSystemCodexHomePath(wslTarget)
codexHomePath: this.getPreparedWslRateLimitHomePath(wslTarget)
}
}
const selfContainedAccount = this.getSelfContainedManagedHostAccount()
@@ -833,7 +844,7 @@ export class CodexRuntimeHomeService {
launchEnv?: NodeJS.ProcessEnv
): void {
if (target?.runtime === 'wsl') {
this.syncWslRuntimeForCurrentSelection(target)
this.startLegacyWslAuthDrain(this.resolveWslDefaultTarget(target))
return
}
@@ -880,7 +891,6 @@ export class CodexRuntimeHomeService {
// distro-local runtime home, so the host mirror only drops its baseline.
this.lastSyncedAccountId = null
this.lastWrittenAuthJson = null
this.skipNextReadBackForAccountId = null
return
}
if (normalizeCodexRuntimeSelection(settings).host) {
@@ -922,71 +932,13 @@ export class CodexRuntimeHomeService {
}
}
// Why: re-auth/add-account write fresh managed tokens, so skip the next read-back to avoid clobbering them with stale runtime tokens.
// Why: re-auth/add-account writes fresh host tokens, invalidating the shared mirror baseline.
clearLastWrittenAuthJson(
accountId = normalizeCodexRuntimeSelection(this.store.getSettings()).host
): void {
if (accountId === normalizeCodexRuntimeSelection(this.store.getSettings()).host) {
this.lastWrittenAuthJson = null
}
this.skipNextReadBackForAccountId = accountId
}
private readBackRefreshedTokensFromPath(
runtimeAuthPath: string,
options: {
updateLastWrittenAuthJson: boolean
lastWrittenAuthJson?: string | null
setLastWrittenAuthJson?: (contents: string) => void
expectedAccountId?: string
}
): CodexReadBackResult {
try {
if (!existsSync(runtimeAuthPath)) {
return 'unchanged'
}
const lastWrittenAuthJson =
options.lastWrittenAuthJson === undefined
? this.lastWrittenAuthJson
: options.lastWrittenAuthJson
const runtimeContents = readFileSync(runtimeAuthPath, 'utf-8')
if (lastWrittenAuthJson !== null && runtimeContents === lastWrittenAuthJson) {
return 'unchanged'
}
const match = this.findManagedAccountForRuntimeAuth(
runtimeContents,
options.expectedAccountId
)
if (match.kind !== 'matched') {
if (match.kind === 'ambiguous') {
console.warn('[codex-runtime-home] Refusing ambiguous Codex auth read-back')
}
return 'rejected'
}
// Why: after restart there's no last-written baseline, so identity alone can't prove runtime auth is newer than managed storage.
if (
lastWrittenAuthJson === null &&
!this.runtimeAuthIsFresher(runtimeContents, match.managedAuthContents)
) {
return 'rejected'
}
writeFileAtomically(match.managedAuthPath, runtimeContents, { mode: 0o600 })
if (options.updateLastWrittenAuthJson) {
if (options.setLastWrittenAuthJson) {
options.setLastWrittenAuthJson(runtimeContents)
} else {
this.lastWrittenAuthJson = runtimeContents
}
}
return 'persisted'
} catch (error) {
// Why: read-back is best-effort; a transient fs error must not block the forward sync — worst case is one more stale-token cycle.
console.warn('[codex-runtime-home] Failed to read back refreshed tokens:', error)
return 'rejected'
}
}
// Why: which ~/.codex bytes the mirror was seeded from, and whether the system
@@ -1041,233 +993,182 @@ export class CodexRuntimeHomeService {
}
private getWslManagedHomePath(account: CodexManagedAccount | null): string | null {
if (!account) {
return null
}
if (account.managedHomeRuntime === 'wsl' && parseWslUncPath(account.managedHomePath)) {
return account.managedHomePath
}
return parseWslUncPath(account.managedHomePath) ? account.managedHomePath : null
return this.getWslManagedHomeIdentity(account) ? (account?.managedHomePath ?? null) : null
}
private getPreparedWslRateLimitHomePath(target: CodexAccountSelectionTarget): string | null {
const distro = target.wslDistro?.trim()
if (distro) {
const settings = this.store.getSettings()
const selectedAccountId = getSelectedCodexAccountIdForTarget(settings, target)
if (selectedAccountId === null) {
// Why: the system-default account changes outside Orca, so read its real home directly to avoid a stale cached runtime copy.
return this.getWslSystemCodexHomePath(target)
}
const cachedRuntimeHomePath = this.wslRuntimeHomePathByDistro.get(distro)
if (
cachedRuntimeHomePath &&
this.lastSyncedWslAccountIdByDistro.has(distro) &&
this.lastSyncedWslAccountIdByDistro.get(distro) === selectedAccountId
) {
// Why: RateLimitService resolves provenance twice per poll; stay path-only so it doesn't block main on UNC reads and a wsl.exe probe.
return cachedRuntimeHomePath
}
}
return this.syncWslRuntimeForCurrentSelection(target)
return this.getWslCodexHomePathForSelection(target)
}
private syncWslRuntimeForCurrentSelection(target: CodexAccountSelectionTarget): string | null {
if (process.platform !== 'win32') {
return null
}
const wslTarget = this.resolveWslDefaultTarget(target)
private getWslCodexHomePathForSelection(target: CodexAccountSelectionTarget): string | null {
const settings = this.store.getSettings()
const activeAccount = this.getActiveAccount(
const account = this.getActiveAccount(
settings.codexManagedAccounts,
getSelectedCodexAccountIdForTarget(settings, wslTarget)
getSelectedCodexAccountIdForTarget(settings, target)
)
const distro = wslTarget.wslDistro?.trim() || activeAccount?.wslDistro || getDefaultWslDistro()
if (!distro) {
return null
}
const runtimeHomePath = this.getWslRuntimeHomePath(distro)
if (!runtimeHomePath) {
return null
}
this.wslRuntimeHomePathByDistro.set(distro, runtimeHomePath)
mkdirSync(runtimeHomePath, { recursive: true })
this.safeMigrateLegacyWslActiveHomePointer(distro, runtimeHomePath)
this.seedWslRuntimeHome(runtimeHomePath, activeAccount, distro)
const runtimeAuthPath = join(runtimeHomePath, 'auth.json')
const previousWslAccountId = this.lastSyncedWslAccountIdByDistro.get(distro) ?? null
if (previousWslAccountId) {
if (this.skipNextReadBackForAccountId === previousWslAccountId) {
this.skipNextReadBackForAccountId = null
} else {
const previousWslAccount = this.getActiveAccount(
settings.codexManagedAccounts,
previousWslAccountId
)
if (previousWslAccount) {
this.readBackRefreshedTokensFromPath(runtimeAuthPath, {
updateLastWrittenAuthJson: true,
lastWrittenAuthJson: this.lastWrittenWslAuthJsonByDistro.get(distro) ?? null,
setLastWrittenAuthJson: (contents) => {
this.lastWrittenWslAuthJsonByDistro.set(distro, contents)
},
expectedAccountId: previousWslAccount.id
})
}
if (account) {
const targetDistro = this.resolveWslDefaultTarget(target).wslDistro?.trim()
const accountHome = this.getWslLaunchCodexHomePath(account, targetDistro)
if (accountHome) {
return accountHome
}
}
const activeAuthPath = activeAccount ? join(activeAccount.managedHomePath, 'auth.json') : null
if (activeAccount && activeAuthPath && existsSync(activeAuthPath)) {
const activeAuth = readFileSync(activeAuthPath, 'utf-8')
this.writeRuntimeAuthAtPath(runtimeAuthPath, activeAuth)
this.lastWrittenWslAuthJsonByDistro.set(distro, activeAuth)
this.lastSyncedWslAccountIdByDistro.set(distro, activeAccount.id)
return runtimeHomePath
}
if (activeAccount && activeAuthPath) {
console.warn(
'[codex-runtime-home] Active WSL managed account is missing auth.json, restoring system default'
)
this.store.updateSettings({
activeCodexManagedAccountId: settings.activeCodexManagedAccountId,
activeCodexManagedAccountIdsByRuntime: setSelectedCodexAccountIdForTarget(
normalizeCodexRuntimeSelection(settings),
null,
wslTarget
)
})
}
const systemAuthPath = this.getWslSystemCodexAuthPath({ runtime: 'wsl', wslDistro: distro })
if (systemAuthPath && existsSync(systemAuthPath)) {
const systemAuth = readFileSync(systemAuthPath, 'utf-8')
const mirroredSystemDefaultAuth = this.lastWrittenWslAuthJsonByDistro.get(distro) ?? null
const runtimeAuth = existsSync(runtimeAuthPath)
? readFileSync(runtimeAuthPath, 'utf-8')
: null
if (
runtimeAuth !== null &&
runtimeAuth !== systemAuth &&
this.runtimeAuthMatchesSystemDefaultIdentity(runtimeAuth, systemAuth) &&
((mirroredSystemDefaultAuth !== null && systemAuth === mirroredSystemDefaultAuth) ||
(mirroredSystemDefaultAuth === null &&
this.runtimeAuthIsFresher(runtimeAuth, systemAuth)))
) {
// Why: WSL baselines are lost on restart, so a same-identity fresher runtime auth is a token refresh; copy it back before mirroring ~/.codex.
this.writeRuntimeAuthAtPath(systemAuthPath, runtimeAuth)
this.lastWrittenWslAuthJsonByDistro.set(distro, runtimeAuth)
this.lastSyncedWslAccountIdByDistro.set(distro, null)
return runtimeHomePath
}
this.writeRuntimeAuthAtPath(runtimeAuthPath, systemAuth)
this.lastWrittenWslAuthJsonByDistro.set(distro, systemAuth)
this.lastSyncedWslAccountIdByDistro.set(distro, null)
return runtimeHomePath
}
rmSync(runtimeAuthPath, { force: true })
this.lastWrittenWslAuthJsonByDistro.set(distro, null)
this.lastSyncedWslAccountIdByDistro.set(distro, null)
return runtimeHomePath
return this.getWslSystemCodexHomePath(target)
}
private getWslRuntimeHomePath(distro: string): string | null {
const home = getWslHome(distro)
return home ? this.joinWslPath(home, ...WSL_CODEX_RUNTIME_HOME_SEGMENTS) : null
}
private safeReadBackActiveWslAccountBeforeRestart(
private getWslLaunchCodexHomePath(
account: CodexManagedAccount,
selectedDistroKey: string
): void {
try {
this.readBackActiveWslAccountBeforeRestart(account, selectedDistroKey)
} catch (error) {
console.warn('[codex-runtime-home] Failed to preserve WSL Codex auth before restart:', error)
targetDistro: string | undefined
): string | null {
const wslHome = this.getWslManagedHomeIdentity(account)
if (!wslHome) {
return null
}
const accountDistro = wslHome.distro
if (targetDistro && accountDistro.toLowerCase() !== targetDistro.toLowerCase()) {
return null
}
if (/^[A-Za-z]:[\\/]/.test(account.managedHomePath)) {
return toWindowsWslUncPath(wslHome.linuxHomePath, accountDistro)
}
return account.managedHomePath || toWindowsWslUncPath(wslHome.linuxHomePath, accountDistro)
}
private readBackActiveWslAccountBeforeRestart(
account: CodexManagedAccount,
selectedDistroKey: string
): void {
const distro =
selectedDistroKey === getWslSelectionKey(null)
? account.wslDistro?.trim()
: selectedDistroKey.trim() || account.wslDistro?.trim()
private getWslManagedHomeIdentity(
account: CodexManagedAccount | null
): { distro: string; linuxHomePath: string } | null {
if (!account) {
return null
}
const distro = account.wslDistro?.trim()
const linuxHomePath = account.wslLinuxHomePath?.trim()
if (account.managedHomeRuntime === 'wsl' && distro && linuxHomePath?.startsWith('/')) {
return { distro, linuxHomePath }
}
const legacyHome = parseWslUncPath(account.managedHomePath)
return legacyHome ? { distro: legacyHome.distro, linuxHomePath: legacyHome.linuxPath } : null
}
private startLegacyWslAuthDrain(
target: CodexAccountSelectionTarget,
options: { throwOnFailure?: boolean } = {}
): Promise<void> {
if (process.platform !== 'win32') {
return Promise.resolve()
}
const distro = target.wslDistro?.trim() || getDefaultWslDistro()
if (!distro) {
return
return Promise.resolve()
}
const runtimeHomePath = this.wslRuntimeHomePathByDistro.get(distro)
if (!runtimeHomePath) {
return
const guestHome = getWslHome(distro)
const guestHomeLinuxPath = guestHome ? toLinuxPath(guestHome).trim() : ''
if (!guestHomeLinuxPath.startsWith('/')) {
return Promise.resolve()
}
this.readBackRefreshedTokensFromPath(join(runtimeHomePath, 'auth.json'), {
updateLastWrittenAuthJson: true,
lastWrittenAuthJson: this.lastWrittenWslAuthJsonByDistro.get(distro) ?? null,
setLastWrittenAuthJson: (contents) => {
this.lastWrittenWslAuthJsonByDistro.set(distro, contents)
},
expectedAccountId: account.id
})
}
private safeMigrateLegacyWslActiveHomePointer(distro: string, runtimeHomePath: string): void {
let legacyPanePresent = true
try {
this.migrateLegacyWslActiveHomePointer(distro, runtimeHomePath)
legacyPanePresent = hasRecordedLegacyWslCodexPane(getCodexSelectionLaneKey(target))
} catch (error) {
console.warn('[codex-runtime-home] Failed to migrate legacy WSL active Codex home:', error)
// Why: unknown pane liveness must preserve the source, but promotion can
// still keep the direct home from launching stale auth.
console.warn('[codex-wsl-auth-drain] Pane registry unavailable; preserving source:', error)
}
}
private migrateLegacyWslActiveHomePointer(distro: string, runtimeHomePath: string): void {
const runtimeWsl = parseWslUncPath(runtimeHomePath)
if (!runtimeWsl?.linuxPath.endsWith('/codex-runtime-home/home')) {
return
}
const activeLinuxPath = runtimeWsl.linuxPath.replace(
/\/codex-runtime-home\/home$/,
'/codex-runtime-home/active/wsl/home'
)
const nextLinuxPath = `${activeLinuxPath}.next-${process.pid}-${Date.now()}`
const activeLinuxParentPath = this.dirnameLinuxPath(activeLinuxPath)
// Why: login-shell cleanup turns `exit 0` into status 1, so fall through.
execFileSync(
'wsl.exe',
[
'-d',
return startLegacyWslRuntimeAuthDrain(
{
distro,
'--exec',
'bash',
'-lc',
[
'set -e',
`if [ ! -e ${quotePosixShell(activeLinuxPath)} ] && [ ! -L ${quotePosixShell(activeLinuxPath)} ]; then :`,
`elif [ -e ${quotePosixShell(activeLinuxPath)} ] && [ ! -L ${quotePosixShell(activeLinuxPath)} ]; then :`,
'else',
`mkdir -p ${quotePosixShell(activeLinuxParentPath)}`,
`rm -rf -- ${quotePosixShell(nextLinuxPath)}`,
`ln -s -- ${quotePosixShell(runtimeWsl.linuxPath)} ${quotePosixShell(nextLinuxPath)}`,
`mv -Tf -- ${quotePosixShell(nextLinuxPath)} ${quotePosixShell(activeLinuxPath)}`,
'fi'
].join('\n')
],
// wsl.exe is console-subsystem: without this a GUI-launched Orca flashes
// a conhost and steals foreground for up to the timeout (#10488).
{ stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000, windowsHide: true }
guestHomeLinuxPath,
legacyPanePresent,
resolveDestination: (runtimeAuthContents) =>
this.resolveLegacyWslAuthDestination(distro, runtimeAuthContents)
},
options
)
}
private dirnameLinuxPath(value: string): string {
const index = value.lastIndexOf('/')
return index > 0 ? value.slice(0, index) : '/'
/** Preserve refreshed auth from retained legacy WSL panes before restart. */
async syncActiveWslSelectionsBeforeRestart(): Promise<void> {
if (process.platform !== 'win32') {
return
}
const settings = this.store.getSettings()
const drains: Promise<void>[] = []
for (const [selectedDistroKey, accountId] of Object.entries(
normalizeCodexRuntimeSelection(settings).wsl
)) {
if (!accountId) {
continue
}
const account = this.getActiveAccount(settings.codexManagedAccounts, accountId)
if (!account || account.managedHomeRuntime !== 'wsl') {
continue
}
const distro =
selectedDistroKey === getWslSelectionKey(null)
? account.wslDistro?.trim() || null
: selectedDistroKey.trim() || null
if (distro) {
drains.push(this.startLegacyWslAuthDrain({ runtime: 'wsl', wslDistro: distro }))
}
}
await Promise.all(drains)
}
private async resolveLegacyWslAuthDestination(
distro: string,
runtimeAuthContents: string
): Promise<LegacyWslRuntimeAuthDestination | null> {
const accountHomes = this.store.getSettings().codexManagedAccounts.flatMap((account) => {
const wslHome = this.getWslManagedHomeIdentity(account)
return wslHome?.distro.toLowerCase() === distro.toLowerCase()
? [{ account, linuxPath: wslHome.linuxHomePath }]
: []
})
const accounts = accountHomes.map(({ account }) => account)
const systemHome = this.getWslSystemCodexHomePath({ runtime: 'wsl', wslDistro: distro })
const parsedSystemHome = systemHome ? parseWslUncPath(systemHome) : null
let reads: WslCodexAuthRead[]
try {
reads = await readWslCodexAuths(distro, [
...accountHomes.map(({ linuxPath }) => linuxPath),
...(parsedSystemHome ? [parsedSystemHome.linuxPath] : [])
])
} catch {
reads = accountHomes.map(() => ({ kind: 'unreadable' }))
if (parsedSystemHome) {
reads.push({ kind: 'unreadable' })
}
}
const authReads = new Map<string, WslCodexAuthRead>(
accountHomes.map(({ account }, index) => [account.id, reads[index] ?? { kind: 'unreadable' }])
)
const match = this.findManagedAccountForRuntimeAuth(runtimeAuthContents, undefined, {
accounts,
authReads
})
if (match.kind === 'ambiguous') {
return null
}
if (match.kind === 'matched') {
const accountHome = accountHomes.find(({ account }) => account.id === match.account.id)
if (!accountHome) {
return null
}
return {
authContents: match.managedAuthContents,
linuxHomePath: accountHome.linuxPath
}
}
if (!systemHome || !parsedSystemHome) {
return null
}
const systemAuth = reads[accountHomes.length] ?? { kind: 'unreadable' }
if (systemAuth.kind !== 'present') {
return null
}
return this.runtimeAuthMatchesSystemDefaultIdentity(runtimeAuthContents, systemAuth.contents)
? { authContents: systemAuth.contents, linuxHomePath: parsedSystemHome.linuxPath }
: null
}
private joinWslPath(basePath: string, ...segments: string[]): string {
@@ -1286,40 +1187,13 @@ export class CodexRuntimeHomeService {
return defaultDistro ? { runtime: 'wsl', wslDistro: defaultDistro } : target
}
private getWslSystemCodexAuthPath(target: CodexAccountSelectionTarget): string | null {
const home = this.getWslSystemCodexHomePath(target)
return home ? this.joinWslPath(home, 'auth.json') : null
}
private seedWslRuntimeHome(
runtimeHomePath: string,
activeAccount: CodexManagedAccount | null,
distro: string
): void {
const runtimeConfigPath = join(runtimeHomePath, 'config.toml')
if (existsSync(runtimeConfigPath)) {
return
}
const candidateHomes = [
activeAccount?.managedHomePath,
this.getWslSystemCodexHomePath({ runtime: 'wsl', wslDistro: distro })
].filter((value): value is string => Boolean(value))
for (const homePath of candidateHomes) {
const configPath = join(homePath, 'config.toml')
if (existsSync(configPath)) {
writeFileAtomically(
runtimeConfigPath,
prepareWslRuntimeSeedConfig(readFileSync(configPath, 'utf-8'), homePath)
)
return
}
}
}
private findManagedAccountForRuntimeAuth(
runtimeAuthContents: string,
expectedAccountId?: string
expectedAccountId?: string,
options?: {
accounts: readonly CodexManagedAccount[]
authReads: ReadonlyMap<string, WslCodexAuthRead>
}
): CodexReadBackMatch {
const matches: {
account: CodexManagedAccount
@@ -1327,18 +1201,17 @@ export class CodexRuntimeHomeService {
managedAuthContents: string
}[] = []
let unreadableHomeCouldOwnRuntimeAuth = false
for (const account of this.store.getSettings().codexManagedAccounts) {
for (const account of options?.accounts ?? this.store.getSettings().codexManagedAccounts) {
if (expectedAccountId && account.id !== expectedAccountId) {
continue
}
const managedAuthPath = join(account.managedHomePath, 'auth.json')
if (!existsSync(managedAuthPath)) {
let managedAuthContents: string
const suppliedRead = options?.authReads.get(account.id)
if (suppliedRead?.kind === 'missing') {
continue
}
let managedAuthContents: string
try {
managedAuthContents = readFileSync(managedAuthPath, 'utf-8')
} catch {
if (suppliedRead?.kind === 'unreadable') {
// Why: an unreadable home can never be compared, but letting the read
// throw abandons the scan for every other account — dropping a refresh
// the runtime home holds for one of them. Only its record can rule it
@@ -1351,6 +1224,24 @@ export class CodexRuntimeHomeService {
}
continue
}
if (suppliedRead?.kind === 'present') {
managedAuthContents = suppliedRead.contents
} else {
if (!existsSync(managedAuthPath)) {
continue
}
try {
managedAuthContents = readFileSync(managedAuthPath, 'utf-8')
} catch {
if (
!expectedAccountId &&
codexAuthCouldBelongToManagedAccount(runtimeAuthContents, account)
) {
unreadableHomeCouldOwnRuntimeAuth = true
}
continue
}
}
if (codexAuthMatchesManagedAccount(runtimeAuthContents, account, managedAuthContents)) {
matches.push({ account, managedAuthPath, managedAuthContents })
}
@@ -1372,10 +1263,6 @@ export class CodexRuntimeHomeService {
return codexAuthMatchesSystemDefaultIdentity(runtimeAuthContents, systemDefaultAuthContents)
}
private runtimeAuthIsFresher(runtimeAuthContents: string, managedAuthContents: string): boolean {
return codexAuthIsFresher(runtimeAuthContents, managedAuthContents)
}
private safeMigrateLegacySharedAuth(): void {
const settings = this.store.getSettings()
try {
@@ -2082,15 +1969,6 @@ export class CodexRuntimeHomeService {
return true
}
private writeRuntimeAuthAtPath(authPath: string, contents: string): void {
if (this.fileContentsEqual(authPath, contents)) {
this.ensureOwnerOnlyMode(authPath)
return
}
mkdirSync(dirname(authPath), { recursive: true })
writeFileAtomically(authPath, contents, { mode: 0o600 })
}
/**
* `true`/`false` only when the bytes were actually read; `null` when the file
* could not be read at all. The old `catch { return false }` reported "these
@@ -2397,14 +2275,3 @@ export class CodexRuntimeHomeService {
rmSync(this.getSystemDefaultSnapshotPath(), { force: true })
}
}
// Why: Codex reads this config inside WSL, so relative path settings must anchor to the Linux-side home (verbatim copy breaks load, os error 2).
export function prepareWslRuntimeSeedConfig(
configContents: string,
sourceHomePath: string
): string {
return prepareSystemConfigForFreshRuntimeMirror(
configContents,
parseWslUncPath(sourceHomePath)?.linuxPath ?? sourceHomePath
)
}
@@ -1,6 +1,11 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import type * as CodexConfigMirror from '../codex/codex-config-mirror'
import type * as CodexHomePaths from '../codex/codex-home-paths'
import type * as CodexPaneAccountRegistry from '../codex/codex-pane-account-registry'
import type * as LegacyWslRuntimeAuthDrain from './legacy-wsl-runtime-auth-drain'
import type * as WslCodexAuthBatchReader from './wsl-codex-auth-batch-reader'
import { createSettings } from './runtime-home-settings-test-fixtures'
import {
createCodexAuthJson,
@@ -87,18 +92,16 @@ describe('CodexRuntimeHomeService', () => {
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe('{"account":"host-system"}\n')
expect(service.prepareForCodexLaunch()).toBe(getRuntimeCodexHomePath())
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
wslRuntimeHomePath
)
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(
'{"account":"wsl"}\n'
wslManagedHomePath
)
expect(existsSync(join(wslRuntimeHomePath, 'auth.json'))).toBe(false)
expect(service.prepareForRateLimitFetch()).toEqual({
kind: 'ready',
codexHomePath: getRuntimeCodexHomePath()
})
expect(service.prepareForRateLimitFetch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toEqual({
kind: 'ready',
codexHomePath: wslRuntimeHomePath
codexHomePath: wslManagedHomePath
})
} finally {
if (originalPlatform) {
@@ -107,7 +110,7 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('clears a selected WSL managed account when auth.json is missing', async () => {
it('keeps a selected WSL managed home when auth.json is temporarily missing', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
@@ -161,13 +164,11 @@ describe('CodexRuntimeHomeService', () => {
)
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
wslRuntimeHomePath
managedHomePath
)
expect(store.updateSettings).toHaveBeenCalledWith({
activeCodexManagedAccountId: null,
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: null } }
})
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(systemAuth)
expect(store.updateSettings).not.toHaveBeenCalled()
expect(existsSync(join(wslRuntimeHomePath, 'auth.json'))).toBe(false)
expect(readFileSync(join(systemCodexHomePath, 'auth.json'), 'utf-8')).toBe(systemAuth)
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
@@ -175,7 +176,7 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('seeds the WSL runtime config with rewritten paths and no system hook trust', async () => {
it('launches WSL system default against its existing config without a runtime seed', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
@@ -214,21 +215,18 @@ describe('CodexRuntimeHomeService', () => {
)
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
wslRuntimeHomePath
systemCodexHomePath
)
const runtimeConfigPath = join(wslRuntimeHomePath, 'config.toml')
const runtimeConfig = readFileSync(runtimeConfigPath, 'utf-8')
expect(runtimeConfig).toContain(
`model_instructions_file = '${join(systemCodexHomePath, 'instructions.md')}'`
)
expect(runtimeConfig).toContain('[projects."/home/alice/repo"]')
expect(runtimeConfig).not.toContain('[hooks.state.')
expect(existsSync(runtimeConfigPath)).toBe(false)
const systemConfigPath = join(systemCodexHomePath, 'config.toml')
const systemConfig = readFileSync(systemConfigPath, 'utf-8')
expect(systemConfig).toContain('model_instructions_file = "instructions.md"')
expect(systemConfig).toContain('[hooks.state.')
// Why: WSL runtime configs are seeded once; Codex writes trust into them
// afterwards, so a relaunch must not clobber the seeded file.
writeFileSync(runtimeConfigPath, `${runtimeConfig}\n[projects."/tmp/x"]\n`, 'utf-8')
writeFileSync(systemConfigPath, `${systemConfig}\n[projects."/tmp/x"]\n`, 'utf-8')
service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })
expect(readFileSync(runtimeConfigPath, 'utf-8')).toContain('[projects."/tmp/x"]')
expect(readFileSync(systemConfigPath, 'utf-8')).toContain('[projects."/tmp/x"]')
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
@@ -236,26 +234,7 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('anchors WSL seed rewrites to the Linux-side home parsed from the UNC source', async () => {
const { prepareWslRuntimeSeedConfig } = await import('./runtime-home-service')
// Why: real UNC sources cannot back live fs operations in tests, so pin
// the UNC -> Linux-side anchor translation on the extracted seed function.
expect(
prepareWslRuntimeSeedConfig(
'model_instructions_file = "instructions.md"\n',
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex'
)
).toContain("model_instructions_file = '/home/alice/.codex/instructions.md'")
expect(
prepareWslRuntimeSeedConfig(
'model_instructions_file = "instructions.md"\n',
'\\\\wsl$\\Ubuntu\\home\\alice\\.codex'
)
).toContain("model_instructions_file = '/home/alice/.codex/instructions.md'")
})
it('switches WSL accounts by rewriting one stable WSL runtime home', async () => {
it('switches WSL accounts by selecting each account home directly', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
@@ -317,16 +296,17 @@ describe('CodexRuntimeHomeService', () => {
'home'
)
expect(service.prepareForCodexLaunch(target)).toBe(wslRuntimeHomePath)
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(firstAuth)
expect(service.prepareForCodexLaunch(target)).toBe(firstManagedHomePath)
expect(existsSync(join(wslRuntimeHomePath, 'auth.json'))).toBe(false)
store.updateSettings({
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: 'account-2' } }
})
service.syncForCurrentSelection(target)
expect(service.prepareForCodexLaunch(target)).toBe(wslRuntimeHomePath)
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(secondAuth)
expect(service.prepareForCodexLaunch(target)).toBe(secondManagedHomePath)
expect(readFileSync(join(firstManagedHomePath, 'auth.json'), 'utf-8')).toBe(firstAuth)
expect(readFileSync(join(secondManagedHomePath, 'auth.json'), 'utf-8')).toBe(secondAuth)
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
@@ -334,7 +314,125 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('does not use host auth baseline to accept stale WSL runtime auth', async () => {
it('waits for the legacy drain before completing direct-home launch preparation', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => wslHome
}))
let finishDrain: (() => void) | undefined
const startLegacyWslRuntimeAuthDrain = vi.fn(
(_options: unknown, _startOptions?: { throwOnFailure?: boolean }) =>
new Promise<void>((resolve) => {
finishDrain = resolve
})
)
vi.doMock('./legacy-wsl-runtime-auth-drain', () => ({ startLegacyWslRuntimeAuthDrain }))
const startWslCodexSessionBridgeInBackground = vi.fn(() => Promise.resolve())
vi.doMock('../codex/wsl-codex-session-bridge', () => ({
startWslCodexSessionBridgeInBackground
}))
vi.doMock('../codex/codex-home-paths', async (importOriginal) => ({
...(await importOriginal<typeof CodexHomePaths>()),
syncCodexGlobalInstructionsIntoManagedHome: vi.fn()
}))
vi.doMock('../codex/codex-config-mirror', async (importOriginal) => ({
...(await importOriginal<typeof CodexConfigMirror>()),
syncSystemConfigIntoManagedCodexHome: vi.fn()
}))
const managedHomePath = createManagedAuth(
testState.userDataDir,
'account-1',
createCodexAuthJson('wsl@example.com', 'acct-wsl', 'managed-token')
)
const store = createStore(
createSettings({
codexManagedAccounts: [
{
id: 'account-1',
email: 'wsl@example.com',
managedHomePath,
managedHomeRuntime: 'wsl',
wslDistro: 'Ubuntu',
wslLinuxHomePath: '/home/alice/.local/share/orca/codex-accounts/account-1/home',
providerAccountId: 'acct-wsl',
workspaceLabel: null,
workspaceAccountId: 'acct-wsl',
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
}
],
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: 'account-1' } }
})
)
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const launch = service.prepareForCodexLaunchAsync({ runtime: 'wsl', wslDistro: 'Ubuntu' })
await Promise.resolve()
expect(startLegacyWslRuntimeAuthDrain).toHaveBeenCalledTimes(1)
expect(startLegacyWslRuntimeAuthDrain.mock.calls[0]?.[1]).toEqual({ throwOnFailure: true })
expect(startWslCodexSessionBridgeInBackground).not.toHaveBeenCalled()
finishDrain?.()
await expect(launch).resolves.toBe(managedHomePath)
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledTimes(1)
} finally {
vi.doUnmock('../codex/codex-config-mirror')
vi.doUnmock('../codex/codex-home-paths')
vi.doUnmock('../codex/wsl-codex-session-bridge')
vi.doUnmock('./legacy-wsl-runtime-auth-drain')
vi.doUnmock('../wsl')
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
}
})
it('preserves legacy auth while draining when pane attribution is unavailable', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => wslHome
}))
vi.doMock('../codex/codex-pane-account-registry', async (importOriginal) => ({
...(await importOriginal<typeof CodexPaneAccountRegistry>()),
hasRecordedLegacyWslCodexPane: () => {
throw new Error('registry unavailable')
}
}))
const startLegacyWslRuntimeAuthDrain = vi.fn(() => Promise.resolve())
vi.doMock('./legacy-wsl-runtime-auth-drain', () => ({ startLegacyWslRuntimeAuthDrain }))
const store = createStore(createSettings())
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
await service.prepareForCodexLaunchAsync({ runtime: 'wsl', wslDistro: 'Ubuntu' })
expect(startLegacyWslRuntimeAuthDrain).toHaveBeenCalledWith(
expect.objectContaining({ legacyPanePresent: true }),
{ throwOnFailure: true }
)
} finally {
vi.doUnmock('./legacy-wsl-runtime-auth-drain')
vi.doUnmock('../codex/codex-pane-account-registry')
vi.doUnmock('../wsl')
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
}
})
it('ignores stale retired runtime auth when launching a managed WSL account', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
@@ -342,7 +440,6 @@ describe('CodexRuntimeHomeService', () => {
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => wslHome
}))
const hostAuth = createCodexAuthJson('host@example.com', 'acct-host', 'host-token')
const wslManagedAuth = createCodexAuthJson(
'wsl@example.com',
'acct-wsl',
@@ -355,7 +452,6 @@ describe('CodexRuntimeHomeService', () => {
'runtime-stale',
1_000
)
const hostManagedHomePath = createManagedAuth(testState.userDataDir, 'host-account', hostAuth)
const wslManagedHomePath = createManagedAuth(
testState.userDataDir,
'wsl-account',
@@ -374,17 +470,6 @@ describe('CodexRuntimeHomeService', () => {
const store = createStore(
createSettings({
codexManagedAccounts: [
{
id: 'host-account',
email: 'host@example.com',
managedHomePath: hostManagedHomePath,
providerAccountId: 'acct-host',
workspaceLabel: null,
workspaceAccountId: 'acct-host',
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
},
{
id: 'wsl-account',
email: 'wsl@example.com',
@@ -395,14 +480,13 @@ describe('CodexRuntimeHomeService', () => {
providerAccountId: 'acct-wsl',
workspaceLabel: null,
workspaceAccountId: 'acct-wsl',
createdAt: 2,
updatedAt: 2,
lastAuthenticatedAt: 2
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
}
],
activeCodexManagedAccountId: 'host-account',
activeCodexManagedAccountIdsByRuntime: {
host: 'host-account',
host: null,
wsl: { Ubuntu: 'wsl-account' }
}
})
@@ -413,10 +497,10 @@ describe('CodexRuntimeHomeService', () => {
const service = new CodexRuntimeHomeService(store as never)
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
wslRuntimeHomePath
wslManagedHomePath
)
expect(readFileSync(join(wslManagedHomePath, 'auth.json'), 'utf-8')).toBe(wslManagedAuth)
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(wslManagedAuth)
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(staleWslRuntimeAuth)
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
@@ -424,104 +508,74 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('does not clobber fresh WSL tokens after clearLastWrittenAuthJson', async () => {
it('launches and drains a mounted-drive WSL account through its distro path', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => wslHome
}))
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
const originalAuth = createCodexAuthJson('wsl@example.com', 'acct-wsl', 'original', 1_000)
const staleRuntimeAuth = createCodexAuthJson('wsl@example.com', 'acct-wsl', 'stale', 1_500)
const reauthedAuth = createCodexAuthJson('wsl@example.com', 'acct-wsl', 'reauthed', 2_000)
const managedHomePath = createManagedAuth(testState.userDataDir, 'account-1', originalAuth)
const managedAuthPath = join(managedHomePath, 'auth.json')
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
)
const runtimeAuthPath = join(wslRuntimeHomePath, 'auth.json')
const store = createStore(
createSettings({
codexManagedAccounts: [
{
id: 'account-1',
email: 'wsl@example.com',
managedHomePath,
managedHomeRuntime: 'wsl',
wslDistro: 'Ubuntu',
wslLinuxHomePath: '/home/alice/.local/share/orca/codex-accounts/account-1/home',
providerAccountId: 'acct-wsl',
workspaceLabel: null,
workspaceAccountId: 'acct-wsl',
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
}
],
activeCodexManagedAccountIdsByRuntime: {
host: null,
wsl: { Ubuntu: 'account-1' }
}
})
)
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
expect(service.prepareForCodexLaunch(target)).toBe(wslRuntimeHomePath)
writeFileSync(runtimeAuthPath, staleRuntimeAuth, 'utf-8')
writeFileSync(managedAuthPath, reauthedAuth, 'utf-8')
service.clearLastWrittenAuthJson('account-1')
service.syncForCurrentSelection(target)
expect(readFileSync(managedAuthPath, 'utf-8')).toBe(reauthedAuth)
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(reauthedAuth)
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
}
})
it('reads active WSL token refreshes back before restart using the selected distro', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => wslHome
}))
const managedAuth = createCodexAuthJson('wsl@example.com', 'acct-wsl', 'managed', 1_000)
const refreshedAuth = createCodexAuthJson(
'wsl@example.com',
'acct-wsl',
'runtime-refreshed',
const managedAuth = createCodexAuthJson(
'drive@example.com',
'acct-drive',
'drive-refresh',
2_000
)
const managedHomePath = createManagedAuth(testState.userDataDir, 'account-1', managedAuth)
const managedAuthPath = join(managedHomePath, 'auth.json')
const linuxHomePath = '/mnt/c/Users/alice/orca/codex-accounts/drive-account/home'
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => 'C:\\Users\\alice'
}))
vi.doMock('./wsl-codex-auth-batch-reader', async (importOriginal) => ({
...(await importOriginal<typeof WslCodexAuthBatchReader>()),
readWslCodexAuths: vi.fn(async (_distro: string, homes: string[]) =>
homes.map((home) =>
home === linuxHomePath
? { kind: 'present' as const, contents: managedAuth }
: { kind: 'missing' as const }
)
)
}))
let drainGuestHome: string | null = null
let drainDestination: { authContents: string; linuxHomePath: string } | null = null
const drainTasks: Promise<void>[] = []
vi.doMock('./legacy-wsl-runtime-auth-drain', async (importOriginal) => ({
...(await importOriginal<typeof LegacyWslRuntimeAuthDrain>()),
startLegacyWslRuntimeAuthDrain: (
options: Parameters<typeof LegacyWslRuntimeAuthDrain.startLegacyWslRuntimeAuthDrain>[0]
) => {
drainGuestHome = options.guestHomeLinuxPath
const task = Promise.resolve(options.resolveDestination(managedAuth)).then(
(destination) => {
drainDestination = destination
}
)
drainTasks.push(task)
return task
}
}))
const startWslCodexSessionBridgeInBackground = vi.fn(() => Promise.resolve())
vi.doMock('../codex/wsl-codex-session-bridge', () => ({
startWslCodexSessionBridgeInBackground,
syncWslCodexSessionsIntoManagedHome: vi.fn(() => Promise.resolve())
}))
vi.doMock('../codex/codex-home-paths', async (importOriginal) => ({
...(await importOriginal<typeof CodexHomePaths>()),
syncCodexGlobalInstructionsIntoManagedHome: vi.fn()
}))
vi.doMock('../codex/codex-config-mirror', async (importOriginal) => ({
...(await importOriginal<typeof CodexConfigMirror>()),
syncSystemConfigIntoManagedCodexHome: vi.fn()
}))
const store = createStore(
createSettings({
codexManagedAccounts: [
{
id: 'account-1',
email: 'wsl@example.com',
managedHomePath,
id: 'drive-account',
email: 'drive@example.com',
managedHomePath: 'C:\\Users\\alice\\orca\\codex-accounts\\drive-account\\home',
managedHomeRuntime: 'wsl',
wslDistro: null,
wslLinuxHomePath: '/home/alice/.local/share/orca/codex-accounts/account-1/home',
providerAccountId: 'acct-wsl',
wslDistro: 'Ubuntu',
wslLinuxHomePath: linuxHomePath,
providerAccountId: 'acct-drive',
workspaceLabel: null,
workspaceAccountId: 'acct-wsl',
workspaceAccountId: 'acct-drive',
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
@@ -529,7 +583,7 @@ describe('CodexRuntimeHomeService', () => {
],
activeCodexManagedAccountIdsByRuntime: {
host: null,
wsl: { Ubuntu: 'account-1' }
wsl: { Ubuntu: 'drive-account' }
}
})
)
@@ -537,25 +591,106 @@ describe('CodexRuntimeHomeService', () => {
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
'\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\alice\\orca\\codex-accounts\\drive-account\\home'
)
const runtimeAuthPath = join(wslRuntimeHomePath, 'auth.json')
await Promise.all(drainTasks)
expect(service.prepareForCodexLaunch(target)).toBe(wslRuntimeHomePath)
writeFileSync(runtimeAuthPath, refreshedAuth, 'utf-8')
service.syncActiveWslSelectionsBeforeRestart()
expect(readFileSync(managedAuthPath, 'utf-8')).toBe(refreshedAuth)
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(refreshedAuth)
expect(drainGuestHome).toBe('/mnt/c/Users/alice')
expect(drainDestination).toEqual({ authContents: managedAuth, linuxHomePath })
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledWith({
distro: 'Ubuntu',
systemCodexHomePath: '\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\alice\\.codex',
managedCodexHomePath:
'\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\alice\\orca\\codex-accounts\\drive-account\\home'
})
} finally {
vi.doUnmock('../codex/codex-config-mirror')
vi.doUnmock('../codex/codex-home-paths')
vi.doUnmock('../codex/wsl-codex-session-bridge')
vi.doUnmock('./legacy-wsl-runtime-auth-drain')
vi.doUnmock('./wsl-codex-auth-batch-reader')
vi.doUnmock('../wsl')
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
}
})
it('resolves a mounted-drive WSL system home as the legacy drain destination', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const systemAuth = createCodexAuthJson(
'system@example.com',
'acct-system',
'system-refresh',
2_000
)
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => 'C:\\Users\\alice'
}))
vi.doMock('./wsl-codex-auth-batch-reader', async (importOriginal) => ({
...(await importOriginal<typeof WslCodexAuthBatchReader>()),
readWslCodexAuths: vi.fn(async (_distro: string, homes: string[]) =>
homes.map((home) =>
home === '/mnt/c/Users/alice/.codex'
? { kind: 'present' as const, contents: systemAuth }
: { kind: 'missing' as const }
)
)
}))
let drainDestination: { authContents: string; linuxHomePath: string } | null = null
const drainTasks: Promise<void>[] = []
vi.doMock('./legacy-wsl-runtime-auth-drain', async (importOriginal) => ({
...(await importOriginal<typeof LegacyWslRuntimeAuthDrain>()),
startLegacyWslRuntimeAuthDrain: (
options: Parameters<typeof LegacyWslRuntimeAuthDrain.startLegacyWslRuntimeAuthDrain>[0]
) => {
const task = Promise.resolve(options.resolveDestination(systemAuth)).then((destination) => {
drainDestination = destination
})
drainTasks.push(task)
return task
}
}))
vi.doMock('../codex/wsl-codex-session-bridge', () => ({
startWslCodexSessionBridgeInBackground: vi.fn(() => Promise.resolve())
}))
vi.doMock('../codex/codex-home-paths', async (importOriginal) => ({
...(await importOriginal<typeof CodexHomePaths>()),
syncCodexGlobalInstructionsIntoManagedHome: vi.fn()
}))
vi.doMock('../codex/codex-config-mirror', async (importOriginal) => ({
...(await importOriginal<typeof CodexConfigMirror>()),
syncSystemConfigIntoManagedCodexHome: vi.fn()
}))
const store = createStore(
createSettings({
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: null } }
})
)
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
'\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\alice\\.codex'
)
await Promise.all(drainTasks)
expect(drainDestination).toEqual({
authContents: systemAuth,
linuxHomePath: '/mnt/c/Users/alice/.codex'
})
} finally {
vi.doUnmock('../codex/codex-config-mirror')
vi.doUnmock('../codex/codex-home-paths')
vi.doUnmock('../codex/wsl-codex-session-bridge')
vi.doUnmock('./legacy-wsl-runtime-auth-drain')
vi.doUnmock('./wsl-codex-auth-batch-reader')
vi.doUnmock('../wsl')
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
@@ -1,9 +1,15 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import type * as WslPaths from '../../shared/wsl-paths'
import type * as CodexConfigMirror from '../codex/codex-config-mirror'
import type * as CodexHomePaths from '../codex/codex-home-paths'
import type * as LegacyWslRuntimeAuthDrain from './legacy-wsl-runtime-auth-drain'
import type * as WslCodexAuthBatchReader from './wsl-codex-auth-batch-reader'
import { createSettings } from './runtime-home-settings-test-fixtures'
import {
createCodexAuthJson,
createManagedAuth,
createStore,
setupRuntimeHomeTest,
@@ -34,62 +40,7 @@ describe('CodexRuntimeHomeService', () => {
teardownRuntimeHomeTest()
})
it('builds a valid WSL legacy active-home migration shell command', async () => {
const execFileSyncMock = vi.fn()
vi.doMock('node:child_process', () => ({ execFileSync: execFileSyncMock }))
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(
createStore(createSettings()) as never
) as unknown as {
migrateLegacyWslActiveHomePointer(distro: string, runtimeHomePath: string): void
}
service.migrateLegacyWslActiveHomePointer(
'Ubuntu',
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
)
expect(execFileSyncMock).toHaveBeenCalledTimes(1)
const firstCall = execFileSyncMock.mock.calls[0]
expect(firstCall).toBeDefined()
const [command, args] = firstCall as [string, string[]]
expect(command).toBe('wsl.exe')
expect(args.slice(0, 5)).toEqual(['-d', 'Ubuntu', '--exec', 'bash', '-lc'])
expect(args).toHaveLength(6)
const shellCommand = args[5]
expect(shellCommand).toContain(
"if [ ! -e '/home/alice/.local/share/orca/codex-runtime-home/active/wsl/home' ] && [ ! -L '/home/alice/.local/share/orca/codex-runtime-home/active/wsl/home' ]; then :"
)
expect(shellCommand).toContain(
"elif [ -e '/home/alice/.local/share/orca/codex-runtime-home/active/wsl/home' ] && [ ! -L '/home/alice/.local/share/orca/codex-runtime-home/active/wsl/home' ]; then :"
)
expect(shellCommand).toContain(
"mkdir -p '/home/alice/.local/share/orca/codex-runtime-home/active/wsl'"
)
expect(shellCommand).toContain(
"ln -s -- '/home/alice/.local/share/orca/codex-runtime-home/home' '/home/alice/.local/share/orca/codex-runtime-home/active/wsl/home.next-"
)
expect(shellCommand).toContain(
"mv -Tf -- '/home/alice/.local/share/orca/codex-runtime-home/active/wsl/home.next-"
)
expect(shellCommand).toContain(
"' '/home/alice/.local/share/orca/codex-runtime-home/active/wsl/home'"
)
expect(shellCommand).not.toContain('[! -L')
expect(shellCommand).not.toContain('mv -Tf--')
expect(shellCommand).not.toContain('$1')
expect(shellCommand).not.toContain('$2')
expect(shellCommand).not.toContain('$3')
expect(shellCommand).not.toContain('exit 0')
} finally {
vi.doUnmock('node:child_process')
}
})
it('starts WSL session bridging after materializing the WSL launch home', async () => {
it('skips WSL session bridging when system default already uses its direct home', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const startWslCodexSessionBridgeInBackground = vi.fn(() => Promise.resolve())
@@ -124,17 +75,13 @@ describe('CodexRuntimeHomeService', () => {
)
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
wslRuntimeHomePath
wslSystemHomePath
)
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledTimes(1)
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledWith({
distro: 'Ubuntu',
systemCodexHomePath: wslSystemHomePath,
managedCodexHomePath: wslRuntimeHomePath
})
const runtimeAgentsPath = join(wslRuntimeHomePath, 'AGENTS.md')
expect(readFileSync(runtimeAgentsPath, 'utf-8')).toBe('# WSL instructions\n')
expect(lstatSync(runtimeAgentsPath).isSymbolicLink()).toBe(false)
expect(startWslCodexSessionBridgeInBackground).not.toHaveBeenCalled()
expect(readFileSync(join(wslSystemHomePath, 'AGENTS.md'), 'utf-8')).toBe(
'# WSL instructions\n'
)
expect(existsSync(join(wslRuntimeHomePath, 'AGENTS.md'))).toBe(false)
} finally {
vi.doUnmock('../codex/wsl-codex-session-bridge')
vi.doUnmock('../wsl')
@@ -144,7 +91,7 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('promotes WSL in-Codex setting changes on the next Codex launch', async () => {
it('keeps WSL in-Codex setting changes in the direct system home', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
vi.doMock('../codex/wsl-codex-session-bridge', () => ({
@@ -177,32 +124,20 @@ describe('CodexRuntimeHomeService', () => {
'home'
)
// First launch seeds the runtime config and records the per-distro baseline.
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
wslRuntimeHomePath
join(wslHome, '.codex')
)
const baselinePath = join(wslRuntimeHomePath, '.orca-config-settings-baseline.json')
expect(existsSync(baselinePath)).toBe(true)
expect(existsSync(baselinePath)).toBe(false)
// A direct WSL Codex edit wins and is mirrored into Orca's runtime before
// the baseline advances, so later in-Orca changes remain promotable.
const runtimeConfigPath = join(wslRuntimeHomePath, 'config.toml')
writeFileSync(wslSystemConfigPath, 'model = "outside-edit"\n', 'utf-8')
service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })
expect(readFileSync(runtimeConfigPath, 'utf-8')).toBe('model = "outside-edit"\n')
expect(readFileSync(baselinePath, 'utf-8')).toContain('"model": "\\"outside-edit\\""')
// Codex now persists a /model change inside Orca's reconciled runtime.
writeFileSync(
runtimeConfigPath,
readFileSync(runtimeConfigPath, 'utf-8').replace('model = "outside-edit"', 'model = "o4"'),
'utf-8'
)
expect(readFileSync(wslSystemConfigPath, 'utf-8')).toBe('model = "outside-edit"\n')
writeFileSync(wslSystemConfigPath, 'model = "o4"\n', 'utf-8')
service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })
expect(readFileSync(wslSystemConfigPath, 'utf-8')).toBe('model = "o4"\n')
// Baseline advances so the promoted value is not re-promoted forever.
expect(readFileSync(baselinePath, 'utf-8')).toContain('"model": "\\"o4\\""')
expect(existsSync(baselinePath)).toBe(false)
} finally {
vi.doUnmock('../codex/wsl-codex-session-bridge')
vi.doUnmock('../wsl')
@@ -236,22 +171,13 @@ describe('CodexRuntimeHomeService', () => {
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
)
service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledTimes(1)
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledWith({
distro: 'Ubuntu',
systemCodexHomePath: '/home/me/.config/codex',
managedCodexHomePath: wslRuntimeHomePath
managedCodexHomePath: join(wslHome, '.codex')
})
} finally {
vi.doUnmock('../codex/wsl-codex-session-bridge')
@@ -262,7 +188,7 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('starts WSL session bridging for the distro used by the materialized runtime home', async () => {
it('starts WSL session bridging for the selected direct account home', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const startWslCodexSessionBridgeInBackground = vi.fn(() => Promise.resolve())
@@ -270,14 +196,6 @@ describe('CodexRuntimeHomeService', () => {
startWslCodexSessionBridgeInBackground
}))
const wslHome = join(testState.userDataDir, 'debian-wsl-home')
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
)
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => null,
getWslHome: (distro: string) => (distro === 'Debian' ? wslHome : null)
@@ -287,10 +205,10 @@ describe('CodexRuntimeHomeService', () => {
return {
...actual,
parseWslUncPath: (candidate: string) =>
candidate === wslRuntimeHomePath
candidate.includes('codex-accounts/debian-account/home')
? {
distro: 'Debian',
linuxPath: '/home/alice/.local/share/orca/codex-runtime-home/home'
linuxPath: '/home/alice/.local/share/orca/codex-accounts/debian-account/home'
}
: null
}
@@ -328,12 +246,12 @@ describe('CodexRuntimeHomeService', () => {
const service = new CodexRuntimeHomeService(store as never)
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: null })).toBe(
wslRuntimeHomePath
managedHomePath
)
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledWith({
distro: 'Debian',
systemCodexHomePath: join(wslHome, '.codex'),
managedCodexHomePath: wslRuntimeHomePath
managedCodexHomePath: managedHomePath
})
} finally {
vi.doUnmock('../codex/wsl-codex-session-bridge')
@@ -344,4 +262,251 @@ describe('CodexRuntimeHomeService', () => {
}
}
})
it('does not rescan retired sessions after the launch drain bridges them', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const startWslCodexSessionBridgeInBackground = vi.fn(() => Promise.resolve())
vi.doMock('../codex/wsl-codex-session-bridge', () => ({
startWslCodexSessionBridgeInBackground
}))
const wslHome = join(testState.userDataDir, 'ubuntu-home')
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => wslHome
}))
const managedHomePath = join(wslHome, '.local', 'share', 'orca', 'codex-accounts', 'a', 'home')
const retiredBridgeRuns = vi.fn()
vi.doMock('./legacy-wsl-runtime-auth-drain', async (importOriginal) => ({
...(await importOriginal<typeof LegacyWslRuntimeAuthDrain>()),
startLegacyWslRuntimeAuthDrain: async (options: {
onDestinationAuthorized?: (destination: {
authContents: string
linuxHomePath: string
}) => void
}) => {
retiredBridgeRuns()
options.onDestinationAuthorized?.({
authContents: '{"account":"a"}\n',
linuxHomePath: managedHomePath
})
}
}))
const store = createStore(
createSettings({
codexManagedAccounts: [
{
id: 'a',
email: 'a@example.com',
managedHomePath,
managedHomeRuntime: 'wsl',
wslDistro: 'Ubuntu',
wslLinuxHomePath: managedHomePath,
providerAccountId: 'acct-a',
workspaceLabel: null,
workspaceAccountId: 'acct-a',
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
}
],
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: 'a' } }
})
)
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
await service.prepareForCodexLaunchAsync({ runtime: 'wsl', wslDistro: 'Ubuntu' })
expect(retiredBridgeRuns).toHaveBeenCalledTimes(1)
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledExactlyOnceWith({
distro: 'Ubuntu',
systemCodexHomePath: join(wslHome, '.codex'),
managedCodexHomePath: managedHomePath
})
} finally {
vi.doUnmock('./legacy-wsl-runtime-auth-drain')
vi.doUnmock('../codex/wsl-codex-session-bridge')
vi.doUnmock('../wsl')
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
}
})
it.skipIf(process.platform === 'win32')(
'links retired WSL sessions only into the auth-matched direct home',
async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const guestHome = join(testState.userDataDir, 'ubuntu-home')
const wslHome = `\\\\wsl.localhost\\Ubuntu${guestHome.replaceAll('/', '\\')}`
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => wslHome
}))
const ownerAuth = createCodexAuthJson(
'owner@example.com',
'acct-owner',
'owner-refresh',
2_000
)
const selectedAuth = createCodexAuthJson(
'selected@example.com',
'acct-selected',
'selected-refresh',
2_000
)
const ownerHome = createManagedAuth(testState.userDataDir, 'owner', ownerAuth)
const selectedHome = createManagedAuth(testState.userDataDir, 'selected', selectedAuth)
vi.doMock('./wsl-codex-auth-batch-reader', async (importOriginal) => ({
...(await importOriginal<typeof WslCodexAuthBatchReader>()),
readWslCodexAuths: vi.fn(async (_distro: string, homes: string[]) =>
homes.map((home) => {
if (home === ownerHome) {
return { kind: 'present' as const, contents: ownerAuth }
}
if (home === selectedHome) {
return { kind: 'present' as const, contents: selectedAuth }
}
return { kind: 'missing' as const }
})
)
}))
const drainTasks: Promise<void>[] = []
vi.doMock('../wsl/wsl-runner', () => ({
runWslProcess: vi.fn(
async (options: { args?: string[]; script: string; shell?: 'bash' }) => {
try {
const stdout = execFileSync(options.shell === 'bash' ? '/bin/bash' : '/bin/sh', [
'-c',
options.script,
options.shell ?? 'sh',
...(options.args ?? [])
]).toString()
return {
code: 0,
stdout,
stderr: '',
timedOut: false,
environmentResolved: true
}
} catch (error) {
return {
code: (error as { status?: number }).status ?? 1,
stdout: (error as { stdout?: Buffer }).stdout?.toString() ?? '',
stderr: (error as { stderr?: Buffer }).stderr?.toString() ?? '',
timedOut: false,
environmentResolved: true
}
}
}
)
}))
vi.doMock('./legacy-wsl-runtime-auth-drain', async (importOriginal) => {
const actual = await importOriginal<typeof LegacyWslRuntimeAuthDrain>()
return {
...actual,
startLegacyWslRuntimeAuthDrain: (
options: Parameters<typeof actual.startLegacyWslRuntimeAuthDrain>[0]
) => {
const task = actual.startLegacyWslRuntimeAuthDrain(options)
drainTasks.push(task)
return task
}
}
})
vi.doMock('../codex/codex-home-paths', async (importOriginal) => ({
...(await importOriginal<typeof CodexHomePaths>()),
syncCodexGlobalInstructionsIntoManagedHome: vi.fn()
}))
vi.doMock('../codex/codex-config-mirror', async (importOriginal) => ({
...(await importOriginal<typeof CodexConfigMirror>()),
syncSystemConfigIntoManagedCodexHome: vi.fn()
}))
const retiredHome = join(guestHome, '.local', 'share', 'orca', 'codex-runtime-home', 'home')
const relativeSessionPath = join('sessions', '2026', '08', '26', 'retired.jsonl')
const retiredSessionPath = join(retiredHome, relativeSessionPath)
mkdirSync(join(retiredSessionPath, '..'), { recursive: true })
writeFileSync(join(retiredHome, 'auth.json'), ownerAuth, 'utf-8')
writeFileSync(retiredSessionPath, '{"session":"retired"}\n', 'utf-8')
const blockedTargetDirectory = join(ownerHome, 'sessions', '2026')
mkdirSync(join(blockedTargetDirectory, '..'), { recursive: true })
writeFileSync(blockedTargetDirectory, 'not-a-directory\n', 'utf-8')
const store = createStore(
createSettings({
codexManagedAccounts: [
{
id: 'owner',
email: 'owner@example.com',
managedHomePath: ownerHome,
managedHomeRuntime: 'wsl',
wslDistro: 'Ubuntu',
wslLinuxHomePath: ownerHome,
providerAccountId: 'acct-owner',
workspaceLabel: null,
workspaceAccountId: 'acct-owner',
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
},
{
id: 'selected',
email: 'selected@example.com',
managedHomePath: selectedHome,
managedHomeRuntime: 'wsl',
wslDistro: 'Ubuntu',
wslLinuxHomePath: selectedHome,
providerAccountId: 'acct-selected',
workspaceLabel: null,
workspaceAccountId: 'acct-selected',
createdAt: 2,
updatedAt: 2,
lastAuthenticatedAt: 2
}
],
activeCodexManagedAccountIdsByRuntime: {
host: null,
wsl: { Ubuntu: 'selected' }
}
})
)
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })
await Promise.all(drainTasks)
const linkedSessionPath = join(ownerHome, relativeSessionPath)
expect(existsSync(linkedSessionPath)).toBe(false)
rmSync(blockedTargetDirectory)
service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })
await Promise.all(drainTasks)
expect(readFileSync(linkedSessionPath, 'utf-8')).toBe('{"session":"retired"}\n')
expect(statSync(linkedSessionPath).ino).toBe(statSync(retiredSessionPath).ino)
expect(existsSync(join(selectedHome, relativeSessionPath))).toBe(false)
rmSync(linkedSessionPath)
rmSync(retiredHome, { recursive: true })
service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })
await Promise.all(drainTasks)
expect(existsSync(linkedSessionPath)).toBe(false)
} finally {
vi.doUnmock('../codex/wsl-codex-session-bridge')
vi.doUnmock('../codex/codex-config-mirror')
vi.doUnmock('../codex/codex-home-paths')
vi.doUnmock('./legacy-wsl-runtime-auth-drain')
vi.doUnmock('./wsl-codex-auth-batch-reader')
vi.doUnmock('../wsl/wsl-runner')
vi.doUnmock('../wsl')
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
}
}
)
})
@@ -53,15 +53,6 @@ describe('CodexRuntimeHomeService', () => {
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const syncWslRuntime = vi.spyOn(
service as unknown as {
syncWslRuntimeForCurrentSelection: (target: {
runtime: 'wsl'
wslDistro?: string | null
}) => string | null
},
'syncWslRuntimeForCurrentSelection'
)
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
const expectedHome = join(wslHome, '.codex')
@@ -73,7 +64,6 @@ describe('CodexRuntimeHomeService', () => {
kind: 'ready',
codexHomePath: expectedHome
})
expect(syncWslRuntime).not.toHaveBeenCalled()
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
@@ -93,15 +83,6 @@ describe('CodexRuntimeHomeService', () => {
const debianAuth = createCodexAuthJson('debian@example.com', 'acct-debian', 'debian-token')
const ubuntuHomePath = createManagedAuth(testState.userDataDir, 'ubuntu-account', ubuntuAuth)
const debianHomePath = createManagedAuth(testState.userDataDir, 'debian-account', debianAuth)
const runtimeAuthPath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home',
'auth.json'
)
const store = createStore(
createSettings({
codexManagedAccounts: [
@@ -147,9 +128,9 @@ describe('CodexRuntimeHomeService', () => {
expect(service.prepareForRateLimitFetch({ runtime: 'wsl', wslDistro: null })).toEqual({
kind: 'ready',
codexHomePath: join(wslHome, '.local', 'share', 'orca', 'codex-runtime-home', 'home')
codexHomePath: ubuntuHomePath
})
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(ubuntuAuth)
expect(readFileSync(join(ubuntuHomePath, 'auth.json'), 'utf-8')).toBe(ubuntuAuth)
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
@@ -228,7 +209,7 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('reads WSL system-default token refreshes back to WSL system auth', async () => {
it('keeps WSL system-default token refreshes in its direct home', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
@@ -257,21 +238,11 @@ describe('CodexRuntimeHomeService', () => {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
)
expect(service.prepareForCodexLaunch(target)).toBe(systemCodexHomePath)
writeFileSync(join(systemCodexHomePath, 'auth.json'), refreshedAuth, 'utf-8')
expect(service.prepareForCodexLaunch(target)).toBe(wslRuntimeHomePath)
writeFileSync(join(wslRuntimeHomePath, 'auth.json'), refreshedAuth, 'utf-8')
expect(service.prepareForCodexLaunch(target)).toBe(wslRuntimeHomePath)
expect(service.prepareForCodexLaunch(target)).toBe(systemCodexHomePath)
expect(readFileSync(join(systemCodexHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
@@ -279,7 +250,7 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('preserves WSL system-default token refreshes after app restart', async () => {
it('does not overwrite direct WSL system auth from the retired runtime on restart', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const wslHome = join(testState.userDataDir, 'wsl-home')
@@ -319,8 +290,8 @@ describe('CodexRuntimeHomeService', () => {
const service = new CodexRuntimeHomeService(store as never)
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
expect(service.prepareForCodexLaunch(target)).toBe(wslRuntimeHomePath)
expect(readFileSync(join(systemCodexHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
expect(service.prepareForCodexLaunch(target)).toBe(systemCodexHomePath)
expect(readFileSync(join(systemCodexHomePath, 'auth.json'), 'utf-8')).toBe(systemAuth)
expect(readFileSync(join(wslRuntimeHomePath, 'auth.json'), 'utf-8')).toBe(refreshedAuth)
} finally {
if (originalPlatform) {
@@ -362,7 +333,7 @@ describe('CodexRuntimeHomeService', () => {
expect(syncConfig).toHaveBeenCalledWith({
runtimeHomePath: join(testState.userDataDir, 'runtime-home'),
systemHomePath: 'C:\\Users\\alice/.codex',
systemHomePath: '\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\alice\\.codex',
systemConfigDir: '/mnt/c/Users/alice/.codex'
})
} finally {
@@ -0,0 +1,70 @@
import { runWslProcess } from '../wsl/wsl-runner'
export type WslCodexAuthRead =
| { kind: 'missing' | 'unreadable' }
| { kind: 'present'; contents: string }
export async function readWslCodexAuths(
distro: string,
linuxHomePaths: readonly string[]
): Promise<WslCodexAuthRead[]> {
if (linuxHomePaths.length === 0) {
return []
}
const result = await runWslProcess({
distro,
loginPath: 'none',
script: READ_AUTHS_SCRIPT,
args: linuxHomePaths,
timeoutMs: 5_000,
maxOutputBytes: 2 * 1024 * 1024
})
if (result.code !== 0 || result.timedOut) {
return linuxHomePaths.map(() => ({ kind: 'unreadable' }))
}
const rows = result.stdout.split('\n')
return linuxHomePaths.map((_, index) => parseAuthReadRow(rows[index] ?? ''))
}
export function decodeWslBase64Payload(encoded: string): string | null {
try {
const decoded = Buffer.from(encoded, 'base64')
const canonical = decoded.toString('base64').replace(/=+$/, '')
return canonical === encoded.replace(/=+$/, '') ? decoded.toString('utf8') : null
} catch {
return null
}
}
function parseAuthReadRow(row: string): WslCodexAuthRead {
if (row === 'missing' || row === 'unreadable') {
return { kind: row }
}
if (!row.startsWith('present:')) {
return { kind: 'unreadable' }
}
const contents = decodeWslBase64Payload(row.slice('present:'.length))
return contents === null ? { kind: 'unreadable' } : { kind: 'present', contents }
}
const READ_AUTHS_SCRIPT = `
set -eu
for home_path in "$@"; do
auth_path="$home_path/auth.json"
if [ ! -f "$auth_path" ]; then
if [ ! -e "$auth_path" ] && [ ! -L "$auth_path" ]; then
printf 'missing\n'
else
printf 'unreadable\n'
fi
continue
fi
if encoded=$(base64 < "$auth_path"); then
printf 'present:'
printf '%s' "$encoded" | tr -d '\n'
printf '\n'
else
printf 'unreadable\n'
fi
done
`
@@ -154,6 +154,25 @@ export class CodexHookService {
return install
}
async prepareRuntimeHomeForLaunch(
runtimeHomePath: string | null | undefined,
target: CodexWslRuntimeHookTarget | undefined,
hooksEnabled: boolean
): Promise<AgentHookInstallStatus> {
if (hooksEnabled) {
// Why: a managed account's launch home is its self-contained CODEX_HOME,
// so hooks/trust must install there rather than the shared mirror.
return (
(await this.installForRuntimeHomeSerialized(runtimeHomePath, target)) ??
(await this.install(runtimeHomePath ?? undefined))
)
}
return (
this.refreshRuntimeUserHooksForRuntimeHome(runtimeHomePath, target) ??
(await this.refreshRuntimeUserHooks(runtimeHomePath ?? undefined))
)
}
refreshRuntimeUserHooksForRuntimeHome(
runtimeHomePath: string | null | undefined,
target?: CodexWslRuntimeHookTarget
@@ -26,4 +26,6 @@ export type CodexPaneAccountRecord = {
export type CodexPaneAccountRegistryFile = {
version: 2
panes: Record<string, CodexPaneAccountRecord>
/** Set after record loss until daemon inventory proves no unattributed pane remains. */
legacyWslAttributionUnknown?: true
}
+75 -3
View File
@@ -29,6 +29,7 @@ export type {
*/
let cachedRegistry: CodexPaneAccountRegistryFile | null = null
let cachedRegistryIsAuthoritative = true
function getRegistryPath(): string {
return join(getOrcaUserDataPath(), 'codex-pane-accounts.json')
@@ -58,7 +59,12 @@ function readRegistryOrNull(): CodexPaneAccountRegistryFile | null {
// Why: a corrupt registry still degrades to empty and IS cached — rebuilding
// unparseable state is the intent, and re-reading it every call would only
// repeat the parse failure.
cachedRegistry = parseRegistry(parseRegistryJson(rawRegistry))
const parsedRegistry = parseRegistryJson(rawRegistry)
cachedRegistryIsAuthoritative = isAuthoritativeRegistry(parsedRegistry)
cachedRegistry = parseRegistry(parsedRegistry)
if (!cachedRegistryIsAuthoritative) {
cachedRegistry.legacyWslAttributionUnknown = true
}
return cachedRegistry
}
@@ -85,12 +91,30 @@ function readRegistry(): CodexPaneAccountRegistryFile {
function readRegistryOrThrow(): CodexPaneAccountRegistryFile {
mutations.flush()
const registry = readRegistryOrNull()
if (!registry) {
if (!registry || !cachedRegistryIsAuthoritative) {
throw new Error('Codex pane account registry could not be read')
}
return registry
}
function isAuthoritativeRegistry(parsed: unknown): boolean {
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return false
}
const panes = (parsed as Partial<CodexPaneAccountRegistryFile>).panes
const version = (parsed as Partial<CodexPaneAccountRegistryFile>).version
const legacyWslAttributionUnknown = (parsed as Partial<CodexPaneAccountRegistryFile>)
.legacyWslAttributionUnknown
return (
version === 2 &&
Boolean(panes) &&
typeof panes === 'object' &&
!Array.isArray(panes) &&
Object.values(panes).every(isPaneAccountRecord) &&
(legacyWslAttributionUnknown === undefined || legacyWslAttributionUnknown === true)
)
}
function parseRegistry(parsed: unknown): CodexPaneAccountRegistryFile {
const empty: CodexPaneAccountRegistryFile = { version: 2, panes: {} }
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
@@ -100,6 +124,9 @@ function parseRegistry(parsed: unknown): CodexPaneAccountRegistryFile {
if (!panes || typeof panes !== 'object' || Array.isArray(panes)) {
return empty
}
if ((parsed as Partial<CodexPaneAccountRegistryFile>).legacyWslAttributionUnknown === true) {
empty.legacyWslAttributionUnknown = true
}
for (const [ptyId, record] of Object.entries(panes)) {
if (isPaneAccountRecord(record)) {
empty.panes[ptyId] = {
@@ -147,7 +174,8 @@ function isPaneAccountRecord(value: unknown): value is CodexPaneAccountRecord {
}
const record = value as Partial<CodexPaneAccountRecord>
return (
typeof record.selectionKey === 'string' &&
(record.selectionKey === 'host' ||
(typeof record.selectionKey === 'string' && /^wsl:.+/.test(record.selectionKey))) &&
(record.accountId === null || typeof record.accountId === 'string')
)
}
@@ -172,6 +200,7 @@ function writeRegistry(registry: CodexPaneAccountRegistryFile): boolean {
mode: 0o600
})
renameSync(temporaryPath, registryPath)
cachedRegistryIsAuthoritative = true
return true
} catch (error) {
// Why: this record only powers a restart hint; losing it must never break a
@@ -266,6 +295,41 @@ export function hasRecordedLegacySharedCodexPane(): boolean {
)
}
/** True when a retained WSL pane may still read the retired per-distro runtime home. */
export function hasRecordedLegacyWslCodexPane(selectionKey: string): boolean {
const registry = readRegistryOrThrow()
return (
Boolean(registry.legacyWslAttributionUnknown) ||
Object.values(registry.panes).some(
(record) =>
(wslSelectionKeysMatch(record.selectionKey, selectionKey) ||
record.selectionKey === 'wsl:__default__') &&
(record.homeRoute === undefined || record.homeRoute === 'wsl-home')
)
)
}
function wslSelectionKeysMatch(left: string, right: string): boolean {
return (
left.startsWith('wsl:') &&
right.startsWith('wsl:') &&
left.slice('wsl:'.length).toLowerCase() === right.slice('wsl:'.length).toLowerCase()
)
}
/** True when startup should reconcile a retained legacy WSL record with daemon inventory. */
export function hasAnyRecordedLegacyWslCodexPane(): boolean {
const registry = readRegistry()
return (
Boolean(registry.legacyWslAttributionUnknown) ||
Object.values(registry.panes).some(
(record) =>
record.selectionKey.startsWith('wsl:') &&
(record.homeRoute === undefined || record.homeRoute === 'wsl-home')
)
)
}
/** True when startup may need to repair hooks for a retained managed host pane. */
export function hasRecordedManagedHostCodexPane(): boolean {
return Object.values(readRegistry().panes).some(
@@ -290,6 +354,13 @@ export function reconcileCodexPaneAccountsWithLivePtys(livePtyIds: readonly stri
}
const livePtyIdSet = new Set(livePtyIds)
let changed = false
if (
registry.legacyWslAttributionUnknown &&
livePtyIds.every((ptyId) => ptyId in registry.panes)
) {
delete registry.legacyWslAttributionUnknown
changed = true
}
for (const ptyId of Object.keys(registry.panes)) {
if (!livePtyIdSet.has(ptyId)) {
delete registry.panes[ptyId]
@@ -302,6 +373,7 @@ export function reconcileCodexPaneAccountsWithLivePtys(livePtyIds: readonly stri
export const _internals = {
resetCache: (): void => {
cachedRegistry = null
cachedRegistryIsAuthoritative = true
mutations.reset()
}
}
@@ -202,7 +202,35 @@ describe('resolveCodexPaneLaunchAccount', () => {
).toEqual({
selectionKey: 'wsl:Ubuntu',
accountId: 'wsl-account',
homeRoute: 'wsl-home'
homeRoute: 'account-home'
})
})
it('attributes a mounted-drive WSL launch through its distro UNC spelling', () => {
const account = managedAccount({
id: 'drive-account',
managedHomePath: 'C:\\Users\\u\\orca\\codex-accounts\\drive-account\\home',
managedHomeRuntime: 'wsl',
wslDistro: 'Ubuntu',
wslLinuxHomePath: '/mnt/c/Users/u/orca/codex-accounts/drive-account/home'
})
const args = {
launchCodexHomePath:
'\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\u\\orca\\codex-accounts\\drive-account\\home',
systemCodexHomePath: SYSTEM_HOME,
settings: settings({ wsl: { Ubuntu: 'drive-account' }, accounts: [account] }),
target: { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
}
expect(resolveCodexPaneLaunchAccount({ ...args, pinnedByResume: false })).toEqual({
selectionKey: 'wsl:Ubuntu',
accountId: 'drive-account',
homeRoute: 'account-home'
})
expect(resolveCodexPaneLaunchAccount({ ...args, pinnedByResume: true })).toEqual({
selectionKey: 'wsl:Ubuntu',
accountId: 'drive-account',
homeRoute: 'account-home'
})
})
+51 -18
View File
@@ -1,5 +1,6 @@
import type { GlobalSettings } from '../../shared/global-settings-types'
import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path'
import { parseWslUncPath } from '../../shared/wsl-paths'
import {
getCodexSelectionLaneKey,
getCodexSelectionTargetForAccount,
@@ -80,9 +81,6 @@ function resolveCodexPaneHomeRoute(args: {
settings: CodexPaneLaunchAccountSettings
target: CodexAccountSelectionTarget
}): CodexPaneHomeRoute {
if (args.target.runtime === 'wsl') {
return 'wsl-home'
}
if (
!args.launchCodexHomePath ||
normalizeRuntimePathForComparison(args.launchCodexHomePath) ===
@@ -90,13 +88,19 @@ function resolveCodexPaneHomeRoute(args: {
) {
return 'real-home'
}
const launchHome = normalizeRuntimePathForComparison(args.launchCodexHomePath)
const accountOwnsHome = args.settings.codexManagedAccounts?.some(
(account) =>
getCodexSelectionTargetForAccount(account).runtime === 'host' &&
normalizeRuntimePathForComparison(account.managedHomePath) === launchHome
const launchHomePath = args.launchCodexHomePath
const accountOwnsHome = args.settings.codexManagedAccounts?.some((account) =>
accountOwnsCodexHome(account, args.target, launchHomePath)
)
return accountOwnsHome ? 'account-home' : 'shared-home'
if (accountOwnsHome) {
return 'account-home'
}
if (args.target.runtime === 'wsl') {
return parseWslUncPath(args.launchCodexHomePath)?.linuxPath.endsWith('/.codex')
? 'real-home'
: 'wsl-home'
}
return 'shared-home'
}
/** undefined when no account owns the home; null means the system-default account. */
@@ -110,17 +114,15 @@ function resolveCodexHomeOwnerAccountId(args: {
if (!args.launchCodexHomePath) {
return null
}
const launchHome = normalizeRuntimePathForComparison(args.launchCodexHomePath)
if (launchHome === normalizeRuntimePathForComparison(args.systemCodexHomePath)) {
if (
normalizeRuntimePathForComparison(args.launchCodexHomePath) ===
normalizeRuntimePathForComparison(args.systemCodexHomePath)
) {
return null
}
const laneKey = getCodexSelectionLaneKey(args.target)
const owner = args.settings.codexManagedAccounts?.find(
(account) =>
// Why: a WSL pane resolves its account from its own per-distro lane, so a
// host account's home must never answer for it (and vice versa).
getCodexSelectionLaneKey(getCodexSelectionTargetForAccount(account)) === laneKey &&
normalizeRuntimePathForComparison(account.managedHomePath) === launchHome
const launchHomePath = args.launchCodexHomePath
const owner = args.settings.codexManagedAccounts?.find((account) =>
accountOwnsCodexHome(account, args.target, launchHomePath)
)
// Why: an unowned home cannot be named, and naming the account a pane is stuck
// on is the prompt's whole job — so decline rather than guess. A wrong notice
@@ -130,3 +132,34 @@ function resolveCodexHomeOwnerAccountId(args: {
// still unnameable, so that cohort stays unreported.
return owner ? owner.id : undefined
}
function accountOwnsCodexHome(
account: NonNullable<CodexPaneLaunchAccountSettings['codexManagedAccounts']>[number],
target: CodexAccountSelectionTarget,
launchHomePath: string
): boolean {
// Why: a WSL pane resolves its account from its own per-distro lane, so a
// host account's home must never answer for it (and vice versa).
if (
getCodexSelectionLaneKey(getCodexSelectionTargetForAccount(account)) !==
getCodexSelectionLaneKey(target)
) {
return false
}
if (
normalizeRuntimePathForComparison(account.managedHomePath) ===
normalizeRuntimePathForComparison(launchHomePath)
) {
return true
}
const launchWslHome = target.runtime === 'wsl' ? parseWslUncPath(launchHomePath) : null
const accountDistro = account.wslDistro?.trim()
const accountLinuxHome = account.wslLinuxHomePath?.trim()
return Boolean(
launchWslHome &&
accountDistro &&
accountLinuxHome &&
launchWslHome.distro.toLowerCase() === accountDistro.toLowerCase() &&
launchWslHome.linuxPath === accountLinuxHome
)
}
@@ -7,7 +7,9 @@ import {
_internals,
forgetCodexPaneAccount,
getCodexPaneAccount,
hasAnyRecordedLegacyWslCodexPane,
hasRecordedLegacySharedCodexPane,
hasRecordedLegacyWslCodexPane,
hasRecordedManagedHostCodexPane,
isCodexPaneHomeRouteProvenAwayFromSharedHome,
reconcileCodexPaneAccountsWithLivePtys,
@@ -138,6 +140,53 @@ describe('codex pane account registry', () => {
expect(hasRecordedLegacySharedCodexPane()).toBe(true)
})
it('identifies only legacy runtime-home panes on the requested WSL lane', () => {
recordCodexPaneAccount('pty-legacy', {
selectionKey: 'wsl:Ubuntu',
accountId: 'account-old',
homeRoute: 'wsl-home'
})
recordCodexPaneAccount('pty-direct', {
selectionKey: 'wsl:Ubuntu',
accountId: 'account-new',
homeRoute: 'account-home'
})
recordCodexPaneAccount('pty-other-distro', {
selectionKey: 'wsl:Debian',
accountId: 'account-debian',
homeRoute: 'wsl-home'
})
recordCodexPaneAccount('pty-default', {
selectionKey: 'wsl:__default__',
accountId: null,
homeRoute: 'wsl-home'
})
expect(hasRecordedLegacyWslCodexPane('wsl:Ubuntu')).toBe(true)
expect(hasRecordedLegacyWslCodexPane('wsl:ubuntu')).toBe(true)
forgetCodexPaneAccount('pty-legacy')
expect(hasRecordedLegacyWslCodexPane('wsl:Ubuntu')).toBe(true)
forgetCodexPaneAccount('pty-default')
expect(hasRecordedLegacyWslCodexPane('wsl:Ubuntu')).toBe(false)
expect(hasRecordedLegacyWslCodexPane('wsl:Debian')).toBe(true)
})
it('requests daemon reconciliation for WSL-only legacy records', () => {
recordCodexPaneAccount('pty-direct', {
selectionKey: 'wsl:Ubuntu',
accountId: 'account-new',
homeRoute: 'account-home'
})
expect(hasAnyRecordedLegacyWslCodexPane()).toBe(false)
recordCodexPaneAccount('pty-legacy', {
selectionKey: 'wsl:Ubuntu',
accountId: 'account-old',
homeRoute: 'wsl-home'
})
expect(hasAnyRecordedLegacyWslCodexPane()).toBe(true)
})
it('requests startup inventory only for managed host panes', () => {
recordCodexPaneAccount('pty-real', {
selectionKey: 'host',
@@ -240,6 +289,44 @@ describe('codex pane account registry', () => {
expect(getCodexPaneAccount('pty-1')).toEqual({ selectionKey: 'host', accountId: 'account-a' })
})
it.each([
['unparseable JSON', '{ not json'],
['a malformed pane record', '{"version":2,"panes":{"pty-1":{"selectionKey":7}}}'],
[
'an invalid lane key',
'{"version":2,"panes":{"pty-1":{"selectionKey":"Ubuntu","accountId":null}}}'
],
['an unknown registry version', '{"version":999,"panes":{}}']
])('refuses to authorize a destructive WSL drain from %s', (_label, contents) => {
writeFileSync(join(userDataPath, 'codex-pane-accounts.json'), contents)
_internals.resetCache()
expect(() => hasRecordedLegacyWslCodexPane('wsl:Ubuntu')).toThrow('registry could not be read')
})
it('does not authorize retirement after a new pane repairs a corrupt registry', () => {
writeFileSync(join(userDataPath, 'codex-pane-accounts.json'), '{ not json')
_internals.resetCache()
expect(() => hasRecordedLegacyWslCodexPane('wsl:Ubuntu')).toThrow()
recordCodexPaneAccount('pty-direct', {
selectionKey: 'wsl:Ubuntu',
accountId: 'account-new',
homeRoute: 'account-home'
})
_internals.resetCache()
expect(hasRecordedLegacyWslCodexPane('wsl:Ubuntu')).toBe(true)
reconcileCodexPaneAccountsWithLivePtys(['pty-direct', 'pty-legacy-unknown'])
_internals.resetCache()
expect(hasRecordedLegacyWslCodexPane('wsl:Ubuntu')).toBe(true)
reconcileCodexPaneAccountsWithLivePtys(['pty-direct'])
_internals.resetCache()
expect(hasRecordedLegacyWslCodexPane('wsl:Ubuntu')).toBe(false)
})
it('drops a malformed record without discarding its valid siblings', () => {
writeFileSync(
join(userDataPath, 'codex-pane-accounts.json'),
@@ -83,6 +83,39 @@ function expectedManagedCommand(scriptPath: string): string {
}
describe('Codex WSL runtime hook install', () => {
it('coalesces launch installs for one home without blocking independent homes', async () => {
const service = new CodexHookService()
const releases: (() => void)[] = []
const started: string[] = []
vi.spyOn(service, 'installForRuntimeHome').mockImplementation(async (runtimeHomePath) => {
if (!runtimeHomePath) {
throw new Error('expected a runtime home')
}
started.push(runtimeHomePath)
await new Promise<void>((resolve) => releases.push(resolve))
return {
agent: 'codex',
state: 'installed',
configPath: `${runtimeHomePath}\\hooks.json`,
managedHooksPresent: true,
detail: null
}
})
const firstHome = '\\\\wsl$\\Ubuntu\\home\\Alice\\.codex'
const alias = firstHome.replace('\\\\wsl$', '\\\\wsl.localhost')
const independent = firstHome.replace('\\Alice\\', '\\Bob\\')
const target = { runtime: 'wsl' as const, wslDistro: 'Ubuntu' }
const first = service.prepareRuntimeHomeForLaunch(firstHome, target, true)
const second = service.prepareRuntimeHomeForLaunch(alias, target, true)
const third = service.prepareRuntimeHomeForLaunch(independent, target, true)
await vi.waitFor(() => expect(started).toEqual([firstHome, independent]))
releases.splice(0).forEach((release) => release())
await Promise.all([first, second, third])
expect(started).toEqual([firstHome, independent])
})
it('coalesces aliases of one runtime home without blocking independent homes', async () => {
const service = new CodexHookService()
const releases: (() => void)[] = []
@@ -0,0 +1,122 @@
import { quotePosixShell } from '../../shared/wsl-login-shell-command'
export const WSL_SESSION_BRIDGE_TIMEOUT_MS = 30_000
const WSL_CODEX_SESSION_BRIDGE_BODY = [
'set -u',
'rollback_manifest=${3-}',
'rollback_stage_root=${4-}',
'scan_scope=${5-full}',
'scan_start=${6-}',
'scan_end=${7-}',
'if [ -n "$rollback_manifest" ]; then',
' [ -n "$rollback_stage_root" ] || exit 1',
' mkdir -p -- "$rollback_stage_root" || exit 1',
' : > "$rollback_manifest" || exit 1',
'fi',
'scanned_files=0',
'linked_files=0',
'bridge_failed=0',
'if [ ! -d "$source_sessions_root" ]; then',
` printf '{"scannedFiles":0,"linkedFiles":0}\\n'`,
' exit 0',
'fi',
'file_list=$(mktemp) || exit 1',
'day_list="$file_list.days"',
'trap \'rm -f -- "$file_list" "$day_list"\' EXIT HUP INT TERM',
'case "$scan_scope" in',
` full) find "$source_sessions_root" -type f -name '*.jsonl' -print0 > "$file_list" || exit 1 ;;`,
' recent)',
' case "$scan_start" in ????/??/??) ;; *) exit 1 ;; esac',
' case "$scan_end" in ????/??/??) ;; *) exit 1 ;; esac',
' if [[ "$scan_start" > "$scan_end" ]]; then',
` find "$source_sessions_root" -type f -name '*.jsonl' -print0 > "$file_list" || exit 1`,
' else',
' : > "$file_list" || exit 1',
' find "$source_sessions_root" -mindepth 3 -maxdepth 3 -type d -print0 > "$day_list" || exit 1',
" while IFS= read -r -d '' session_day_root; do",
' session_day=${session_day_root#"$source_sessions_root"/}',
' case "$session_day" in ????/??/??) ;; *) continue ;; esac',
' [[ "$session_day" < "$scan_start" ]] && continue',
' find "$session_day_root" -maxdepth 1 -type f -name \'*.jsonl\' -print0 >> "$file_list" || exit 1',
' done < "$day_list"',
' fi',
' ;;',
' *) exit 1 ;;',
'esac',
"while IFS= read -r -d '' source_file; do",
' scanned_files=$((scanned_files + 1))',
' relative_path=${source_file#"$source_sessions_root"/}',
' target_file="$managed_sessions_root/$relative_path"',
' if [ -e "$target_file" ] || [ -L "$target_file" ]; then',
' continue',
' fi',
' target_dir=${target_file%/*}',
' if ! mkdir -p -- "$target_dir"; then',
' bridge_failed=1',
' continue',
' fi',
' link_source="$source_file"',
' if [ -n "$rollback_manifest" ]; then',
' staged_file="$rollback_stage_root/$relative_path"',
' staged_dir=${staged_file%/*}',
' if ! mkdir -p -- "$staged_dir" || ! ln -- "$source_file" "$staged_file"; then',
' bridge_failed=1',
' continue',
' fi',
' target_stage="$target_file.orca-bridge-$$"',
' if [ -e "$target_stage" ] || [ -L "$target_stage" ]; then',
' bridge_failed=1',
' continue',
' fi',
' if ! ln -- "$staged_file" "$target_stage"; then',
' if ! cp -- "$staged_file" "$target_stage" || ! cmp -s -- "$staged_file" "$target_stage"; then',
' rm -f -- "$target_stage"',
' bridge_failed=1',
' continue',
' fi',
' fi',
' if ! printf \'%s\\0%s\\0\' "$target_stage" "$target_file" >> "$rollback_manifest"; then',
' rm -f -- "$target_stage"',
' bridge_failed=1',
' continue',
' fi',
' link_source="$target_stage"',
' fi',
// Codex resume ignores symlinked JSONL, so create links inside the distro.
' if ln -- "$link_source" "$target_file"; then',
' linked_files=$((linked_files + 1))',
' elif [ ! -e "$target_file" ] && [ ! -L "$target_file" ]; then',
' if [ -n "$rollback_manifest" ]; then',
' bridge_failed=1',
' else',
' target_stage="$target_file.orca-bridge-$$"',
' if [ ! -e "$target_stage" ] && [ ! -L "$target_stage" ] && cp -- "$source_file" "$target_stage" && cmp -s -- "$source_file" "$target_stage" && ln -- "$target_stage" "$target_file"; then',
' linked_files=$((linked_files + 1))',
' else',
' bridge_failed=1',
' fi',
' rm -f -- "$target_stage"',
' fi',
' fi',
'done < "$file_list"',
'[ "$bridge_failed" = 0 ] || exit 1',
`printf '{"scannedFiles":%s,"linkedFiles":%s}\\n' "$scanned_files" "$linked_files"`
].join('\n')
export const WSL_CODEX_SESSION_BRIDGE_SCRIPT = [
'source_sessions_root="$1"',
'managed_sessions_root="$2"',
WSL_CODEX_SESSION_BRIDGE_BODY
].join('\n')
export function buildWslCodexSessionBridgeShellCommand(paths: {
managedSessionsRoot: string
systemSessionsRoot: string
}): string {
return [
`source_sessions_root=${quotePosixShell(paths.systemSessionsRoot)}`,
`managed_sessions_root=${quotePosixShell(paths.managedSessionsRoot)}`,
WSL_CODEX_SESSION_BRIDGE_BODY
].join('\n')
}
+109 -2
View File
@@ -1,4 +1,8 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { execFileSync } from 'node:child_process'
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
const { runWslProcessMock } = vi.hoisted(() => ({
runWslProcessMock: vi.fn()
@@ -60,10 +64,10 @@ describe('syncWslCodexSessionsIntoManagedHome', () => {
"managed_sessions_root='/home/alice/.local/share/orca/codex-runtime-home/home/sessions'"
)
expect(shellCommand).toContain(`find "$source_sessions_root" -type f -name '*.jsonl' -print0`)
expect(shellCommand).toContain('ln -- "$source_file" "$target_file"')
expect(shellCommand).toContain('ln -- "$link_source" "$target_file"')
expect(shellCommand).toContain('if [ -e "$target_file" ] || [ -L "$target_file" ]; then')
expect(shellCommand).not.toContain('ln -s')
expect(shellCommand).not.toContain('cp ')
expect(shellCommand).toContain('cp --')
expect(shellCommand).not.toContain('sqlite')
})
@@ -180,4 +184,107 @@ describe('buildWslCodexSessionBridgeShellCommand', () => {
expect(shellCommand).toContain('$source_file')
expect(shellCommand).toContain('$((scanned_files + 1))')
})
it.skipIf(process.platform === 'win32')(
'publishes a verified guest-side copy across filesystems',
() => {
const root = mkdtempSync(join(tmpdir(), 'orca-wsl-session-bridge-cross-fs-'))
const sourceSessionsRoot = join(root, 'legacy', 'sessions')
const managedSessionsRoot = join(root, 'managed', 'sessions')
const binDir = join(root, 'bin')
const relativePath = join('2026', '08', '26', 'retired.jsonl')
const sourcePath = join(sourceSessionsRoot, relativePath)
const targetPath = join(managedSessionsRoot, relativePath)
mkdirSync(join(sourcePath, '..'), { recursive: true })
mkdirSync(binDir)
writeFileSync(sourcePath, '{"session":"retired"}\n', 'utf-8')
const lnShimPath = join(binDir, 'ln')
writeFileSync(
lnShimPath,
`#!/bin/sh
if [ "$2" = "$BRIDGE_SOURCE" ] && [ "$3" = "$BRIDGE_TARGET" ]; then
exit 1
fi
exec /bin/ln "$@"
`
)
chmodSync(lnShimPath, 0o755)
const shellCommand = buildWslCodexSessionBridgeShellCommand({
systemSessionsRoot: sourceSessionsRoot,
managedSessionsRoot
})
try {
execFileSync('/bin/bash', ['-c', shellCommand], {
env: {
...process.env,
BRIDGE_SOURCE: sourcePath,
BRIDGE_TARGET: targetPath,
PATH: `${binDir}:${process.env.PATH ?? ''}`
}
})
expect(readFileSync(targetPath, 'utf-8')).toBe('{"session":"retired"}\n')
} finally {
rmSync(root, { recursive: true, force: true })
}
}
)
it.skipIf(process.platform === 'win32')(
'leaves an existing copied session to its current writer',
() => {
const root = mkdtempSync(join(tmpdir(), 'orca-wsl-session-bridge-existing-'))
const sourceSessionsRoot = join(root, 'legacy', 'sessions')
const managedSessionsRoot = join(root, 'managed', 'sessions')
const relativePath = join('2026', '08', '26', 'retired.jsonl')
const sourcePath = join(sourceSessionsRoot, relativePath)
const targetPath = join(managedSessionsRoot, relativePath)
mkdirSync(join(sourcePath, '..'), { recursive: true })
mkdirSync(join(targetPath, '..'), { recursive: true })
writeFileSync(sourcePath, '{"session":"retired"}\n{"event":"legacy"}\n', 'utf-8')
writeFileSync(targetPath, '{"session":"retired"}\n', 'utf-8')
const shellCommand = buildWslCodexSessionBridgeShellCommand({
systemSessionsRoot: sourceSessionsRoot,
managedSessionsRoot
})
try {
execFileSync('/bin/bash', ['-c', shellCommand])
expect(readFileSync(targetPath, 'utf-8')).toBe('{"session":"retired"}\n')
} finally {
rmSync(root, { recursive: true, force: true })
}
}
)
it.skipIf(process.platform === 'win32')(
'fails in the guest shell when a missing session cannot be linked, then retries cleanly',
() => {
const root = mkdtempSync(join(tmpdir(), 'orca-wsl-session-bridge-'))
const sourceSessionsRoot = join(root, 'legacy', 'sessions')
const managedSessionsRoot = join(root, 'managed', 'sessions')
const relativePath = join('2026', '08', '26', 'retired.jsonl')
const sourcePath = join(sourceSessionsRoot, relativePath)
const blockingPath = join(managedSessionsRoot, '2026')
mkdirSync(join(sourcePath, '..'), { recursive: true })
mkdirSync(managedSessionsRoot, { recursive: true })
writeFileSync(sourcePath, '{"session":"retired"}\n', 'utf-8')
writeFileSync(blockingPath, 'not-a-directory\n', 'utf-8')
const shellCommand = buildWslCodexSessionBridgeShellCommand({
systemSessionsRoot: sourceSessionsRoot,
managedSessionsRoot
})
try {
expect(() => execFileSync('/bin/bash', ['-c', shellCommand])).toThrow()
rmSync(blockingPath)
execFileSync('/bin/bash', ['-c', shellCommand])
expect(readFileSync(join(managedSessionsRoot, relativePath), 'utf-8')).toBe(
'{"session":"retired"}\n'
)
} finally {
rmSync(root, { recursive: true, force: true })
}
}
)
})
+6 -36
View File
@@ -1,7 +1,12 @@
import { posix as pathPosix } from 'node:path'
import { quotePosixShell as quoteBashString } from '../../shared/wsl-login-shell-command'
import { parseWslUncPath } from '../../shared/wsl-paths'
import { runWslProcess } from '../wsl/wsl-runner'
import {
buildWslCodexSessionBridgeShellCommand,
WSL_SESSION_BRIDGE_TIMEOUT_MS
} from './wsl-codex-session-bridge-script'
export { buildWslCodexSessionBridgeShellCommand } from './wsl-codex-session-bridge-script'
export type WslCodexSessionBridgeTarget = {
distro: string
@@ -21,8 +26,6 @@ export type WslCodexSessionBridgeSummary = {
const emptySummary: WslCodexSessionBridgeSummary = { scannedFiles: 0, linkedFiles: 0 }
const backgroundWslSessionBridgeTasks = new Map<string, Promise<void>>()
const WSL_SESSION_BRIDGE_TIMEOUT_MS = 30_000
export function startWslCodexSessionBridgeInBackground(
target: WslCodexSessionBridgeTarget
): Promise<void> {
@@ -86,39 +89,6 @@ export function resolveWslCodexSessionBridgeLinuxPaths(
}
}
export function buildWslCodexSessionBridgeShellCommand(
paths: WslCodexSessionBridgeLinuxPaths
): string {
const shellCommand = [
'set -u',
`source_sessions_root=${quoteBashString(paths.systemSessionsRoot)}`,
`managed_sessions_root=${quoteBashString(paths.managedSessionsRoot)}`,
'scanned_files=0',
'linked_files=0',
'if [ ! -d "$source_sessions_root" ]; then',
` printf '{"scannedFiles":0,"linkedFiles":0}\\n'`,
' exit 0',
'fi',
"while IFS= read -r -d '' source_file; do",
' scanned_files=$((scanned_files + 1))',
' relative_path=${source_file#"$source_sessions_root"/}',
' target_file="$managed_sessions_root/$relative_path"',
' if [ -e "$target_file" ] || [ -L "$target_file" ]; then',
' continue',
' fi',
' target_dir=${target_file%/*}',
' mkdir -p -- "$target_dir" || continue',
// Why: Codex resume ignores symlinked JSONL, so WSL links must be
// Linux hardlinks created inside the distro filesystem.
' if ln -- "$source_file" "$target_file"; then',
' linked_files=$((linked_files + 1))',
' fi',
`done < <(find "$source_sessions_root" -type f -name '*.jsonl' -print0 2>/dev/null)`,
`printf '{"scannedFiles":%s,"linkedFiles":%s}\\n' "$scanned_files" "$linked_files"`
].join('\n')
return shellCommand
}
function getWslSessionBridgeTaskKey(target: WslCodexSessionBridgeTarget): string {
return [target.distro, target.systemCodexHomePath, target.managedCodexHomePath].join('\0')
}
+21 -19
View File
@@ -69,6 +69,7 @@ import {
import {
type CodexPaneHomeRoute,
getCodexPaneAccount,
hasAnyRecordedLegacyWslCodexPane,
hasRecordedManagedHostCodexPane,
isCodexPaneHomeRouteProvenAwayFromSharedHome,
reconcileCodexPaneAccountsWithLivePtys
@@ -1116,7 +1117,8 @@ function startTerminalRuntimeStartupServices(): WindowsDesktopStartupServices {
macosLoginSessionWatch: process.platform === 'darwin' && !isServeMode
})
// Why: a retained shell keeps its launch-time Codex home even when the current routing lane changes.
if (codexRuntimeHome && hasRecordedManagedHostCodexPane()) {
const hasRetainedManagedHostPane = hasRecordedManagedHostCodexPane()
if (codexRuntimeHome && (hasRetainedManagedHostPane || hasAnyRecordedLegacyWslCodexPane())) {
const livePtyIds = await listLiveDaemonPtyIds()
if (livePtyIds) {
reconcileCodexPaneAccountsWithLivePtys(livePtyIds)
@@ -1124,15 +1126,17 @@ function startTerminalRuntimeStartupServices(): WindowsDesktopStartupServices {
// Why (#16441): each retained home can run a codex app-server grant
// session. Awaiting them here delayed the first window by N sessions;
// a retained shell cannot invoke Codex before this provider serves.
void reconcileRetainedCodexHookHomes({
hookService: codexHookService,
hooksEnabled:
isAgentStatusHooksEnabled(settings) &&
settings?.disabledTuiAgents.includes('codex') !== true,
runtimeHomePaths: codexRuntimeHome.getRetainedHostCodexHookHomePaths(livePtyIds)
}).catch((error: unknown) => {
console.warn('[codex-hook-service] retained Codex home reconcile failed:', error)
})
if (hasRetainedManagedHostPane) {
void reconcileRetainedCodexHookHomes({
hookService: codexHookService,
hooksEnabled:
isAgentStatusHooksEnabled(settings) &&
settings?.disabledTuiAgents.includes('codex') !== true,
runtimeHomePaths: codexRuntimeHome.getRetainedHostCodexHookHomePaths(livePtyIds)
}).catch((error: unknown) => {
console.warn('[codex-hook-service] retained Codex home reconcile failed:', error)
})
}
}
}
// Why: retained shells can invoke Codex immediately after the startup gate.
@@ -1230,7 +1234,7 @@ async function prepareCodexRuntimeHomeForLaunch(
// Why: a ManagedCodexHomeTemporarilyUnavailableError must escape uncaught —
// the fallbacks below all key off `null`, which means "system default", so
// swallowing the refusal would launch the wrong account (#STA-4422).
let runtimeHomePath = codexRuntimeHome!.prepareForCodexLaunch(target, launchEnv, {
let runtimeHomePath = await codexRuntimeHome!.prepareForCodexLaunchAsync(target, launchEnv, {
unavailableManagedHomePath: launchContext?.unavailableManagedHomePath
})
if (runtimeHomePath === null && !realHomeHooksPrepared) {
@@ -1239,7 +1243,7 @@ async function prepareCodexRuntimeHomeForLaunch(
// re-resolve if the capability gate rejects it.
realHomeHooksPrepared = await ensureRealHomeHooksIfSelected()
if (realHomeHooksPrepared) {
runtimeHomePath = codexRuntimeHome!.prepareForCodexLaunch(target, launchEnv, {
runtimeHomePath = await codexRuntimeHome!.prepareForCodexLaunchAsync(target, launchEnv, {
unavailableManagedHomePath: launchContext?.unavailableManagedHomePath
})
}
@@ -1259,13 +1263,11 @@ async function prepareCodexRuntimeHomeForLaunch(
const hooksEnabled = isAgentStatusHooksEnabled(store?.getSettings())
try {
// Why: honor the persisted off switch so post-startup launches can't reinstall removed hooks.
const status = hooksEnabled
? ((await codexHookService.installForRuntimeHome(runtimeHomePath, hookTarget)) ??
// Why: a managed account's launch home is its own self-contained
// CODEX_HOME, so hooks/trust must install there, not the shared mirror.
(await codexHookService.install(runtimeHomePath ?? undefined)))
: (codexHookService.refreshRuntimeUserHooksForRuntimeHome(runtimeHomePath, hookTarget) ??
(await codexHookService.refreshRuntimeUserHooks(runtimeHomePath ?? undefined)))
const status = await codexHookService.prepareRuntimeHomeForLaunch(
runtimeHomePath,
hookTarget,
hooksEnabled
)
if (status.state === 'error') {
console.warn(
`[codex-hook-service] failed to ${
+1 -3
View File
@@ -1,6 +1,4 @@
/** Guest-relative layout of Orca's managed WSL CODEX_HOME. Must stay in sync
* with getWslRuntimeHomePath (codex-accounts/runtime-home-service.ts), which
* builds the UNC twin of this path. */
/** Guest-relative layout of Orca's retired WSL CODEX_HOME, retained for migration reads. */
export const WSL_CODEX_RUNTIME_HOME_SEGMENTS = [
'.local',
'share',
@@ -471,25 +471,54 @@ describe('orchestration notification mailbox consistency', () => {
db.close()
})
it('does not submit or duplicate a pointer after the agent becomes working', async () => {
vi.useFakeTimers()
const db = createDatabase('orca-mailbox-working-before-enter-')
const harness = createRuntime(db)
const run = createBoundRun(db, 'Working-before-Enter Run')
const message = insertDirectRunMessage(db, run.id, 'Actionable status')
it.each([
['Codex', '\x1b]0;Codex working\x07', '\x1b]0;Codex done\x07'],
['Claude', '\x1b]0;\u280b Claude working\x07', '\x1b]0;\u2733 Claude Code\x07']
])(
'submits a staged pointer after %s becomes working without duplicating it',
async (_provider, workingTitle, idleTitle) => {
vi.useFakeTimers()
const db = createDatabase('orca-mailbox-working-before-enter-')
const harness = createRuntime(db)
const run = createBoundRun(db, 'Working-before-Enter Run')
const message = insertDirectRunMessage(db, run.id, 'Actionable status')
await driveToLiveIdle(harness.runtime)
expect(pointerCount(harness.write)).toBe(1)
harness.runtime.onPtyData(PTY_ID, '\x1b]0;Codex working\x07', 3)
await vi.advanceTimersByTimeAsync(500)
await driveToLiveIdle(harness.runtime)
expect(pointerCount(harness.write)).toBe(1)
harness.runtime.onPtyData(PTY_ID, workingTitle, 3)
await vi.advanceTimersByTimeAsync(500)
expect(harness.write.mock.calls.filter(([, payload]) => payload === '\r')).toHaveLength(0)
expect(db.getMessageById(message.id)?.delivered_at).toEqual(expect.any(String))
harness.runtime.onPtyData(PTY_ID, '\x1b]0;Codex done\x07', 4)
await Promise.resolve()
expect(pointerCount(harness.write)).toBe(1)
db.close()
})
expect(harness.write.mock.calls.filter(([, payload]) => payload === '\r')).toHaveLength(1)
expect(db.getMessageById(message.id)?.delivered_at).toEqual(expect.any(String))
harness.runtime.onPtyData(PTY_ID, idleTitle, 4)
await Promise.resolve()
expect(pointerCount(harness.write)).toBe(1)
db.close()
}
)
it.each([
['Codex', '\x1b]0;Codex permission\x07'],
['Claude', '\x1b]0;Claude waiting for permission\x07']
])(
'does not submit a staged pointer after %s enters a permission state',
async (_provider, permissionTitle) => {
vi.useFakeTimers()
const db = createDatabase('orca-mailbox-permission-before-enter-')
const harness = createRuntime(db)
const run = createBoundRun(db, 'Permission-before-Enter Run')
const message = insertDirectRunMessage(db, run.id, 'Actionable status')
await driveToLiveIdle(harness.runtime)
expect(pointerCount(harness.write)).toBe(1)
harness.runtime.onPtyData(PTY_ID, permissionTitle, 3)
await vi.advanceTimersByTimeAsync(500)
expect(harness.write.mock.calls.filter(([, payload]) => payload === '\r')).toHaveLength(0)
expect(db.getMessageById(message.id)?.delivered_at).toEqual(expect.any(String))
db.close()
}
)
it('releases staged pointer state when an explicit check owns the batch', async () => {
vi.useFakeTimers()
@@ -54,8 +54,9 @@ export function submitOrchestrationMailboxPointer<TWaiter extends OrchestrationM
} else if (deps.mailboxOwner.resolve(currentLeaf) !== input.mailboxHandle) {
clearAndRedrive = true
} else if (
currentLeaf.lastAgentStatus === 'idle' &&
currentLeaf.lastAgentStatusObservedLive
currentLeaf.lastAgentStatusObservedLive &&
// Once staged, working is queue-safe; idle-only strands Orca-owned text in the composer.
(currentLeaf.lastAgentStatus === 'idle' || currentLeaf.lastAgentStatus === 'working')
) {
if (
shouldReleaseOrchestrationPointer(
@@ -0,0 +1,37 @@
import { afterEach, describe, expect, it } from 'vitest'
import type { RpcRequest } from '../core'
import { ORCHESTRATION_METHODS } from './orchestration'
import { RpcDispatcher } from '../dispatcher'
import { createOrchestrationRpcHarness } from './orchestration-rpc-test-harness'
import { ORCHESTRATION_CONTRACT_VERSION } from '../../../../shared/protocol-version'
describe('orchestration.send invalid message type', () => {
const h = createOrchestrationRpcHarness()
afterEach(() => {
h.cleanup()
})
it('explains valid message types and the question reply path', async () => {
const { runtime, ctx } = h.setup()
const dispatcher = new RpcDispatcher({ runtime, methods: ORCHESTRATION_METHODS })
const request: RpcRequest = {
id: 'req_1',
authToken: 'token',
method: 'orchestration.send',
params: { to: 'b', subject: 'hi', type: 'answer' },
orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION
}
const response = await dispatcher.dispatch(request, ctx)
expect(response).toMatchObject({
ok: false,
error: {
code: 'invalid_argument',
message:
'Invalid --type. Expected one of: status, dispatch, worker_done, merge_ready, escalation, handoff, decision_gate, question, heartbeat. To answer a worker question, use the same Orca CLI executable with orchestration reply --id <msg_id> --body <text>.'
}
})
})
})
+8 -11
View File
@@ -76,6 +76,11 @@ async function routeAllMailboxPages(
type DispatchMutationMessageType = 'worker_done' | 'heartbeat' | 'escalation' | 'decision_gate'
const SEND_MESSAGE_TYPE_ERROR = [
`Invalid --type. Expected one of: ${MESSAGE_TYPES.join(', ')}.`,
'To answer a worker question, use the same Orca CLI executable with orchestration reply --id <msg_id> --body <text>.'
].join(' ')
function isDispatchMutationMessageType(
type: string | undefined
): type is DispatchMutationMessageType {
@@ -132,17 +137,9 @@ const SendParams = z
from: OptionalString,
body: OptionalString,
type: z
.enum([
'status',
'dispatch',
'worker_done',
'merge_ready',
'escalation',
'handoff',
'decision_gate',
'question',
'heartbeat'
])
.enum(MESSAGE_TYPES, {
error: SEND_MESSAGE_TYPE_ERROR
})
.optional(),
priority: z.enum(['normal', 'high', 'urgent']).optional(),
threadId: OptionalString,
+67 -3
View File
@@ -53,7 +53,28 @@ describe('AppImage CLI redirect', () => {
).toBeNull()
})
it('routes no-sandbox serve launches through the CLI', () => {
it('keeps direct serve launches in Electron when Chromium switches are present', () => {
expect(
getAppImageCliArgs(
[
'AppRun',
'--no-sandbox',
'--disable-features=FedCm,DirectSockets',
'serve',
'--port',
'6768'
],
{ APPIMAGE: '/opt/orca' },
{
platform: 'linux',
isPackaged: true,
commandNames
}
)
).toBeNull()
})
it('keeps clean serve launches on the CLI path for validation', () => {
expect(
getAppImageCliArgs(
['AppRun', '--no-sandbox', 'serve', '--port', '6768'],
@@ -67,6 +88,34 @@ describe('AppImage CLI redirect', () => {
).toEqual(['serve', '--port', '6768'])
})
it('handles a space-separated Chromium switch value', () => {
expect(
getAppImageCliArgs(
['AppRun', '--disable-features', 'FedCm', 'serve'],
{ APPIMAGE: '/opt/orca' },
{
platform: 'linux',
isPackaged: true,
commandNames
}
)
).toBeNull()
})
it('does not broaden the Electron-owned exception to switches after serve', () => {
expect(
getAppImageCliArgs(
['AppRun', 'serve', '--disable-features=FedCm'],
{ APPIMAGE: '/opt/orca' },
{
platform: 'linux',
isPackaged: true,
commandNames
}
)
).toEqual(['serve', '--disable-features=FedCm'])
})
it('removes no-sandbox before forwarding CLI help', () => {
expect(
getAppImageCliArgs(
@@ -81,6 +130,20 @@ describe('AppImage CLI redirect', () => {
).toEqual(['serve', '--help'])
})
it('still redirects serve help even when Chromium switches are present', () => {
expect(
getAppImageCliArgs(
['AppRun', '--disable-features=FedCm', 'serve', '--help'],
{ APPIMAGE: '/opt/orca' },
{
platform: 'linux',
isPackaged: true,
commandNames
}
)
).toEqual(['--disable-features=FedCm', 'serve', '--help'])
})
it('spawns the unpacked CLI entrypoint with Electron node mode', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-appimage-cli-redirect-'))
const cliEntryPath = join(root, 'app.asar.unpacked', 'out', 'cli', 'index.js')
@@ -118,14 +181,14 @@ describe('AppImage CLI redirect', () => {
expect(spawnOptions?.env).not.toHaveProperty('NODE_REPL_EXTERNAL_MODULE')
})
it('forwards an explicit no-sandbox choice to the serve child', async () => {
it('keeps a clean no-sandbox serve launch on the CLI path', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-appimage-cli-redirect-'))
const cliEntryPath = join(root, 'app.asar.unpacked', 'out', 'cli', 'index.js')
await mkdir(join(root, 'app.asar.unpacked', 'out', 'cli'), { recursive: true })
await writeFile(cliEntryPath, '', 'utf8')
const spawn = vi.fn((..._args: unknown[]) => ({ status: 0 }))
maybeRedirectAppImageCliLaunch({
const result = maybeRedirectAppImageCliLaunch({
argv: ['orca-linux.AppImage', '--no-sandbox', 'serve'],
env: { APPIMAGE: '/opt/orca/orca-linux.AppImage' },
platform: 'linux',
@@ -136,6 +199,7 @@ describe('AppImage CLI redirect', () => {
spawn: spawn as never
})
expect(result).toEqual({ redirected: true, status: 0 })
expect(spawn).toHaveBeenCalledWith(
'/opt/orca/orca-ide',
[cliEntryPath, 'serve'],
+30 -6
View File
@@ -24,7 +24,8 @@ type RedirectOptions = {
const HELP_FLAGS = new Set(['--help', '-h', 'help'])
const APPIMAGE_DESKTOP_FLAGS = new Set(['--no-sandbox'])
const CLI_FLAGS_WITH_VALUES = new Set(['--environment', '--pairing-code'])
const ELECTRON_LAUNCH_SWITCHES = new Set(['--disable-features'])
const CLI_FLAGS_WITH_VALUES = new Set(['--environment', '--pairing-code', '--disable-features'])
// Why: the main tsconfig cannot import the CLI project, but AppImage direct
// launches need a conservative allow-list before bypassing the GUI startup.
const APPIMAGE_CLI_COMMAND_NAMES = [
@@ -157,21 +158,44 @@ export function getAppImageCliArgs(
const commandNames = new Set(options.commandNames)
const firstPositional = findFirstCommandCandidate(cliArgs)
return firstPositional && commandNames.has(firstPositional) ? cliArgs : null
if (!firstPositional || !commandNames.has(firstPositional)) {
return null
}
// Keep serve in Electron only when an Electron launch switch is present.
// Forwarding it to the strict Node-mode CLI parser makes an otherwise valid
// serve launch fail, while clean serve invocations retain CLI validation.
if (
firstPositional === 'serve' &&
cliArgs
.slice(0, findFirstCommandCandidateIndex(cliArgs))
.some((arg) => ELECTRON_LAUNCH_SWITCHES.has(flagName(arg)))
) {
return null
}
return cliArgs
}
function findFirstCommandCandidate(args: string[]): string | null {
const index = findFirstCommandCandidateIndex(args)
return index === -1 ? null : args[index]!
}
function findFirstCommandCandidateIndex(args: string[]): number {
for (let index = 0; index < args.length; index += 1) {
const arg = args[index]
if (!arg.startsWith('-')) {
return arg
return index
}
const flagName = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : arg
if (CLI_FLAGS_WITH_VALUES.has(flagName) && !arg.includes('=')) {
if (CLI_FLAGS_WITH_VALUES.has(flagName(arg)) && !arg.includes('=')) {
index += 1
}
}
return null
return -1
}
function flagName(arg: string): string {
const equalsIndex = arg.indexOf('=')
return equalsIndex === -1 ? arg : arg.slice(0, equalsIndex)
}
function buildElectronRunAsNodeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
@@ -4,10 +4,9 @@ import { describe, expect, it } from 'vitest'
import { getAppImageCliArgs } from './appimage-cli-redirect'
import { argvRequestsServeMode, normalizeServeModeArgv } from './serve-mode-argv'
// Why: index.ts must run both CLI redirects before rewriting argv. Rewriting
// first replaces the `serve` positional with `--serve`, so the redirect's
// command-name lookup finds a port number instead of a command and bails —
// silently dropping AppImage serve launches out of the CLI path (#12677).
// Why: index.ts runs CLI redirects before rewriting argv. Direct AppImage serve
// stays in Electron so launch switches do not cross into the strict Node-mode
// CLI parser; other CLI commands still depend on redirect ordering (#12677).
const REDIRECT_OPTIONS = {
platform: 'linux' as const,
@@ -24,7 +23,7 @@ function rewriteAsIndexDoes(argv: string[]): string[] {
describe('serve argv rewrite vs AppImage CLI redirect ordering', () => {
const launchArgv = ['/opt/orca/orca-ide', '--no-sandbox', 'serve', '--port', '7777', '--json']
it('hands the launch argv to the CLI when the redirect runs first', () => {
it('keeps clean serve validation on the CLI path', () => {
expect(getAppImageCliArgs(launchArgv, MOUNTED_APPIMAGE_ENV, REDIRECT_OPTIONS)).toEqual([
'serve',
'--port',
@@ -33,6 +32,11 @@ describe('serve argv rewrite vs AppImage CLI redirect ordering', () => {
])
})
it('keeps an injected Chromium switch in Electron before argv rewriting', () => {
const injected = [...launchArgv.slice(0, 2), '--disable-features=FedCm', ...launchArgv.slice(2)]
expect(getAppImageCliArgs(injected, MOUNTED_APPIMAGE_ENV, REDIRECT_OPTIONS)).toBeNull()
})
it('loses the redirect if the rewrite runs first', () => {
const rewritten = rewriteAsIndexDoes(launchArgv)
expect(rewritten).toContain('--serve')
+18
View File
@@ -107,6 +107,24 @@ describe('serve-mode-argv', () => {
])
})
it('keeps Electron-injected Chromium switches while normalizing direct serve', () => {
expect(
normalizeServeModeArgv([
'/opt/orca/orca-ide',
'--disable-features=FedCm,DirectSockets',
'serve',
'--port',
'6768'
])
).toEqual([
'/opt/orca/orca-ide',
'--disable-features=FedCm,DirectSockets',
'--serve',
'--serve-port',
'6768'
])
})
it('leaves already-normalized argv unchanged', () => {
// Why every value flag: the CLI's own `orca serve` spawns the app with exactly this shape
// (serveOrcaApp), and the rewrite now runs over it too — a bad mapping would drop the port here.
+2 -1
View File
@@ -24,13 +24,14 @@ const CLI_TO_SERVE_VALUE_FLAG = new Map([
/**
* Flags that consume the next argv token as a value (CLI-form + Electron passthrough).
* Residual class: a flag outside this list whose space-separated value is literally `serve` would
* read as the subcommand. Chromium switches are `--flag=value` only, so no real launch does that.
* read as the subcommand. Include switches that may arrive in either argv shape.
*/
const VALUE_TAKING_FLAGS = new Set([
...CLI_TO_SERVE_VALUE_FLAG.keys(),
'--serve-port',
'--serve-pairing-address',
'--serve-project-root',
'--disable-features',
'--user-data-dir',
'--environment',
'--pairing-code'
@@ -17,7 +17,6 @@
main/agent-hooks/wsl-hook-relay-launch.ts
main/browser/wsl-browser-network-relay-launch.ts
main/claude-accounts/claude-command-process.ts
main/codex-accounts/runtime-home-service.ts
main/codex-accounts/service.ts
main/codex/codex-state-db-backfill-recovery.ts
main/codex/codex-trust-grant-host.ts
@@ -46,7 +46,9 @@ src/main/browser/browser-cookie-import.ts
src/main/browser/browser-route-egress-electron-launch.ts
src/main/browser/browser-route-persisted-worker-electron-process.ts
src/main/claude-accounts/keychain.ts
src/main/codex-accounts/runtime-home-service.ts
src/main/codex-accounts/legacy-wsl-runtime-auth-drain-recovery-script-harness.ts
src/main/codex-accounts/legacy-wsl-runtime-auth-drain-script-harness.ts
src/main/codex-accounts/legacy-wsl-runtime-auth-drain-script-interference-shims.ts
src/main/codex-accounts/service.ts
src/main/codex/codex-app-server-client.ts
src/main/codex/codex-app-server-posix-supervisor.ts
+8 -1
View File
@@ -6,7 +6,8 @@ import {
isWslUncPath,
parseWslUncPath,
resolveWslRepoWorktreeBasePath,
toWindowsWslPath
toWindowsWslPath,
toWindowsWslUncPath
} from './wsl-paths'
describe('wsl path helpers', () => {
@@ -35,6 +36,12 @@ describe('wsl path helpers', () => {
])('converts %s without folding case-sensitive Linux paths', (linuxPath, expected) => {
expect(toWindowsWslPath(linuxPath, 'Ubuntu')).toBe(expected)
})
it('keeps mounted-drive paths on the distro UNC view when requested', () => {
expect(toWindowsWslUncPath('/mnt/c/Users/jin', 'Ubuntu')).toBe(
'\\\\wsl.localhost\\Ubuntu\\mnt\\c\\Users\\jin'
)
})
})
describe('resolveWslRepoWorktreeBasePath', () => {
+7 -2
View File
@@ -55,7 +55,12 @@ export function toWindowsWslPath(linuxPath: string, distro: string): string {
return `${mntMatch[1].toUpperCase()}:${rest || '\\'}`
}
return `\\\\wsl.localhost\\${distro}${linuxPath.replace(/\//g, '\\')}`
return toWindowsWslUncPath(linuxPath, distro)
}
/** Keep a Linux path addressable through its distro, including drvfs mounts. */
export function toWindowsWslUncPath(linuxPath: string, distro: string): string {
return `\\\\wsl.localhost\\${distro}${linuxPath === '/' ? '\\' : linuxPath.replace(/\//g, '\\')}`
}
/**
@@ -86,7 +91,7 @@ export function resolveWslRepoWorktreeBasePath(repoPath: string, basePath: strin
return basePath
}
const collapsed = collapsePosixDotSegments(basePath)
return `\\\\wsl.localhost\\${repoWsl.distro}${collapsed === '/' ? '\\' : collapsed.replace(/\//g, '\\')}`
return toWindowsWslUncPath(collapsed, repoWsl.distro)
}
function collapsePosixDotSegments(absolutePosixPath: string): string {