fix(codex): stop a cold WSL distro from reading as "this home is not yours"

The WSL ownership probe collapsed every failure into a trust verdict. Under
`set -euo pipefail` an absent home, a marker owned by another account, a
`readlink` failure, and `wsl.exe` failing to start a cold distro all aborted
with the same status and empty stdout, and the catch-all relabelled the lot as
`Managed WSL Codex home is outside Orca account storage.` No exit code was
evidence of *which* happened, so no caller could tell a proven trust failure
from "we could not look" — the exact category error STA-4422 removed from the
host lane.

That is not cosmetic. `removeUnlessUnproven` keeps the managed home only for a
`ManagedCodexHomeTemporarilyUnavailableError`; a fail-once probe during add
therefore produced a plain trust error, and the rollback's own re-gate then
succeeded (the fault was transient) and deleted the home with the credentials
`codex login` had just written into it.

The guest now states its observation on a tagged line and exits 0. Only a
parsed tag is dispositive; a throw from the runner, a timeout, no tag, extra
output, or an unknown tag are all indeterminate. Three lanes are classified
against the tri-state the host already had:

- the `wsl.exe` probe, through the new tagged protocol;
- the mounted lane, which used `existsSync` and so folded EPERM/EIO into
  "does not exist";
- `ensureExpectedWslHome`, where exits 41/42 stay dispositive and every other
  non-zero exit or timeout becomes indeterminate.

The two structural checks on the persisted path spelling now throw the typed
untrusted error rather than a bare `Error`, since the host already holds the
facts they test.

`assertOwnedCodexManagedHomeVerdict` and the ownership-marker message are
shared by both Codex lanes so neither can invent its own error mapping.
Reconciling this vocabulary with the Claude lane is still open: the module it
would merge with (`claude-managed-auth-ownership.ts`, PR #17993) is not on main
yet, so there is nothing here to extract against.

Fixes STA-5616.
This commit is contained in:
Merge Sim
2026-09-01 12:28:49 -07:00
parent 512ec1a0da
commit 4da6859b04
6 changed files with 746 additions and 68 deletions
@@ -1,16 +1,37 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { lstatSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { app } from 'electron'
import { isDefinitiveAbsence } from '../../shared/definitive-filesystem-absence'
import { quotePosixShell } from '../../shared/wsl-login-shell-command'
import { parseWslUncPath } from '../../shared/wsl-paths'
import type { CodexManagedAccount } from '../../shared/managed-account-types'
import { getSystemCodexHomePath } from '../codex/codex-home-paths'
import { toWindowsWslPath } from '../wsl'
import { runWslProcess } from '../wsl/wsl-runner'
import { assertOwnedHostCodexManagedHomePath } from './host-codex-managed-home-ownership'
import {
assertOwnedCodexManagedHomeVerdict,
assertOwnedHostCodexManagedHomePath,
ManagedCodexHomeTemporarilyUnavailableError,
MISSING_MANAGED_HOME_MESSAGE,
MISSING_OWNERSHIP_MARKER_MESSAGE,
UntrustedManagedCodexHomeError,
type HostCodexManagedHomeVerdict
} from './host-codex-managed-home-ownership'
import {
ACCOUNT_ID_MISMATCH_MESSAGE,
buildWslCodexManagedHomeProbeScript,
classifyWslCodexManagedHomeProbe,
MARKER_ACCOUNT_MISMATCH_MESSAGE,
OUTSIDE_MANAGED_ROOT_MESSAGE
} from './wsl-codex-managed-home-probe'
const WSL_MANAGED_HOME_TIMEOUT_MS = 5_000
/** Exit codes the preparation script uses to report a *proven* foreign directory. */
const WSL_PREPARE_UNTRUSTED_EXITS = new Map<number, string>([
[41, MISSING_OWNERSHIP_MARKER_MESSAGE],
[42, MARKER_ACCOUNT_MISMATCH_MESSAGE]
])
export class CodexManagedHomePath {
constructor(private readonly validateWslPath: (distro: string, script: string) => string) {}
@@ -56,22 +77,26 @@ export class CodexManagedHomePath {
expectedAccountId
})
}
// Why: the spelling of the persisted path is a fact the host already holds,
// so a mismatch is dispositive without asking the guest anything.
if (
!wslInfo.linuxPath.includes('/.local/share/orca/codex-accounts/') ||
!wslInfo.linuxPath.endsWith('/home')
) {
throw new Error('Managed WSL Codex home is outside Orca account storage.')
throw new UntrustedManagedCodexHomeError(OUTSIDE_MANAGED_ROOT_MESSAGE)
}
if (
expectedAccountId !== undefined &&
!wslInfo.linuxPath.endsWith(`/.local/share/orca/codex-accounts/${expectedAccountId}/home`)
) {
throw new Error('Managed WSL Codex home does not match its persisted account ID.')
throw new UntrustedManagedCodexHomeError(ACCOUNT_ID_MISMATCH_MESSAGE)
}
if (process.platform === 'win32') {
return this.assertWindowsWslPath(wslInfo, expectedAccountId)
}
return this.assertMountedWslPath(candidatePath, wslInfo.linuxPath, expectedAccountId)
return assertOwnedCodexManagedHomeVerdict(
this.resolveMountedWslVerdict(candidatePath, wslInfo.linuxPath, expectedAccountId)
)
}
private recreateExpectedHostHome(account: CodexManagedAccount, originalError: unknown): string {
@@ -113,12 +138,28 @@ export class CodexManagedHomePath {
shell: 'bash',
timeoutMs: WSL_MANAGED_HOME_TIMEOUT_MS
})
if (result.timedOut) {
throw new ManagedCodexHomeTemporarilyUnavailableError(undefined, {
cause: new Error(
`Preparing the managed Codex home in WSL ${wslInfo.distro} timed out after ${WSL_MANAGED_HOME_TIMEOUT_MS}ms.`
)
})
}
// Why: 41/42 mean the path is not this account's home; re-auth must refuse
// rather than write credentials into someone else's directory.
if (result.code !== 0 || result.timedOut) {
throw new Error(
`Could not prepare the managed Codex home in WSL ${wslInfo.distro} for re-authentication.`
)
// rather than write credentials into someone else's directory. Every other
// non-zero exit — a cold distro, a missing shell, a 9p hiccup — proves
// nothing about ownership and must not read as a trust failure (STA-5616).
const untrustedReason =
typeof result.code === 'number' ? WSL_PREPARE_UNTRUSTED_EXITS.get(result.code) : undefined
if (untrustedReason !== undefined) {
throw new UntrustedManagedCodexHomeError(untrustedReason)
}
if (result.code !== 0) {
throw new ManagedCodexHomeTemporarilyUnavailableError(undefined, {
cause: new Error(
`Preparing the managed Codex home in WSL ${wslInfo.distro} exited with code ${String(result.code)}.`
)
})
}
}
@@ -126,68 +167,64 @@ export class CodexManagedHomePath {
wslInfo: { distro: string; linuxPath: string },
expectedAccountId?: string
): string {
const script = buildWslCodexManagedHomeProbeScript(wslInfo.linuxPath, expectedAccountId)
let stdout: string
try {
const canonicalLinuxPath = this.validateWslPath(
wslInfo.distro,
[
'set -euo pipefail',
`candidate=${quotePosixShell(wslInfo.linuxPath)}`,
'managed_root="${HOME%/}/.local/share/orca/codex-accounts"',
'candidate_real=$(readlink -f -- "$candidate")',
'managed_root_real=$(readlink -f -- "$managed_root")',
'test -f "$candidate_real/.orca-managed-home"',
...(expectedAccountId === undefined
? [
'case "$candidate_real" in "$managed_root_real"/*/home) printf "%s\\n" "$candidate_real" ;; *) exit 35 ;; esac'
]
: [
`expected_marker=${quotePosixShell(expectedAccountId)}`,
'test "$candidate_real" = "$managed_root_real/$expected_marker/home"',
'test "$(cat "$candidate_real/.orca-managed-home")" = "$expected_marker"',
'printf "%s\\n" "$candidate_real"'
])
].join('\n')
).trim()
if (!canonicalLinuxPath) {
throw new Error('Managed Codex home directory does not exist on disk.')
}
return toWindowsWslPath(canonicalLinuxPath, wslInfo.distro)
stdout = this.validateWslPath(wslInfo.distro, script)
} catch (error) {
throw new Error('Managed WSL Codex home is outside Orca account storage.', {
cause: error
})
// Why: the guest reports its observation on a tagged line and always exits
// 0, so a throw here is the runner failing — never evidence about the home.
return assertOwnedCodexManagedHomeVerdict(
classifyWslCodexManagedHomeProbe({ ran: false, error }, wslInfo.distro)
)
}
return assertOwnedCodexManagedHomeVerdict(
classifyWslCodexManagedHomeProbe({ ran: true, stdout }, wslInfo.distro)
)
}
private assertMountedWslPath(
/**
* The same gate for a WSL home reached through a mount rather than `wsl.exe`.
* Reads go through `statSync`/`lstatSync` rather than `existsSync` so an EPERM
* or EIO cannot be folded into "does not exist" (STA-4422's collapse).
*/
private resolveMountedWslVerdict(
candidatePath: string,
linuxPath: string,
expectedAccountId?: string
): string {
): HostCodexManagedHomeVerdict {
if (linuxPath.split('/').includes('..')) {
throw new Error('Managed WSL Codex home is outside Orca account storage.')
return { kind: 'untrusted', reason: OUTSIDE_MANAGED_ROOT_MESSAGE }
}
if (!existsSync(candidatePath)) {
throw new Error('Managed Codex home directory does not exist on disk.')
try {
statSync(candidatePath)
} catch (error) {
if (isDefinitiveAbsence(error)) {
return { kind: 'untrusted', reason: MISSING_MANAGED_HOME_MESSAGE }
}
return { kind: 'indeterminate', error }
}
const markerPath = join(candidatePath, '.orca-managed-home')
if (!existsSync(markerPath)) {
throw new Error('Managed Codex home is missing Orca ownership marker.')
let markerContents: string
try {
if (!lstatSync(markerPath).isFile()) {
return { kind: 'untrusted', reason: MISSING_OWNERSHIP_MARKER_MESSAGE }
}
markerContents = readFileSync(markerPath, 'utf-8')
} catch (error) {
if (isDefinitiveAbsence(error)) {
return { kind: 'untrusted', reason: MISSING_OWNERSHIP_MARKER_MESSAGE }
}
return { kind: 'indeterminate', error }
}
if (
expectedAccountId !== undefined &&
readFileSync(markerPath, 'utf-8').trim() !== expectedAccountId
) {
throw new Error('Managed WSL Codex home ownership marker does not match its account ID.')
if (expectedAccountId !== undefined && markerContents.trim() !== expectedAccountId) {
return { kind: 'untrusted', reason: MARKER_ACCOUNT_MISMATCH_MESSAGE }
}
return candidatePath
return { kind: 'owned', homePath: candidatePath }
}
private isMissingHomeError(error: unknown): boolean {
return (
error instanceof Error &&
error.message === 'Managed Codex home directory does not exist on disk.'
)
return error instanceof Error && error.message === MISSING_MANAGED_HOME_MESSAGE
}
private pathsEqual(left: string, right: string): boolean {
@@ -10,6 +10,9 @@ type HostCodexManagedHomeOwnershipOptions = {
}
export const MISSING_MANAGED_HOME_MESSAGE = 'Managed Codex home directory does not exist on disk.'
/** Shared with the WSL probe so the two lanes cannot drift apart on this wording. */
export const MISSING_OWNERSHIP_MARKER_MESSAGE =
'Managed Codex home is missing Orca ownership marker.'
/**
* Why: the gate answers two different questions and callers act on them very
@@ -156,7 +159,7 @@ function evaluate({
// Why: the marker is required, so its definitive absence is structural — but
// an unreadable marker is not evidence of anything.
if (isDefinitiveAbsence(error)) {
return { kind: 'untrusted', reason: 'Managed Codex home is missing Orca ownership marker.' }
return { kind: 'untrusted', reason: MISSING_OWNERSHIP_MARKER_MESSAGE }
}
return { kind: 'indeterminate', error }
}
@@ -193,7 +196,11 @@ export function resolveHostCodexManagedHomeVerdict(
export function assertOwnedHostCodexManagedHomePath(
options: HostCodexManagedHomeOwnershipOptions
): string {
const verdict = evaluate(options)
return assertOwnedCodexManagedHomeVerdict(evaluate(options))
}
/** Shared by the host and WSL lanes so neither can invent its own error mapping. */
export function assertOwnedCodexManagedHomeVerdict(verdict: HostCodexManagedHomeVerdict): string {
if (verdict.kind === 'owned') {
return verdict.homePath
}
@@ -34,6 +34,11 @@ function decodeEncodedWslBashCommand(command: string): string {
return encoded ? Buffer.from(encoded, 'base64').toString('utf8') : command
}
/** The tagged line the guest prints for an Orca-owned home (STA-5616 protocol). */
function ownedProbeVerdict(linuxPath: string): string {
return `ORCA_CODEX_HOME_VERDICT:owned:${Buffer.from(linuxPath, 'utf-8').toString('base64')}\n`
}
function wslOk(stdout = ''): WslResult {
return { environmentResolved: true, code: 0, stdout, stderr: '', timedOut: false }
}
@@ -133,7 +138,7 @@ describe('CodexAccountService config sync', () => {
writeFileSync(wslCanonicalConfigPath, 'model_instructions_file = "instructions.md"\n', 'utf-8')
vi.doMock('node:child_process', () => ({
execFileSync: vi.fn(() => `${wslLinuxHomePath}\n`),
execFileSync: vi.fn(() => ownedProbeVerdict(wslLinuxHomePath)),
spawn: vi.fn()
}))
vi.doMock('../../shared/wsl-paths', () => ({
@@ -210,7 +215,7 @@ describe('CodexAccountService config sync', () => {
const script = decodeEncodedWslBashCommand(String(args.at(-1)))
expect(args.slice(0, 2)).toEqual(['-d', 'Debian'])
expect(script).toContain('readlink -f')
return `${wslLinuxHomePath}\n`
return ownedProbeVerdict(wslLinuxHomePath)
})
const runWslProcessMock = vi.fn(async (spec: WslSpec) => {
const script = String(spec.script)
@@ -343,7 +348,7 @@ describe('CodexAccountService config sync', () => {
const script = decodeEncodedWslBashCommand(String(args.at(-1)))
expect(args.slice(0, 2)).toEqual(['-d', 'Debian'])
expect(script).toContain('readlink -f')
return `${wslLinuxHomePath}\n`
return ownedProbeVerdict(wslLinuxHomePath)
})
const runWslProcessMock = vi.fn(async (spec: WslSpec) => {
const script = String(spec.script)
@@ -421,7 +426,7 @@ describe('CodexAccountService config sync', () => {
const script = decodeEncodedWslBashCommand(String(args.at(-1)))
expect(args.slice(0, 2)).toEqual(['-d', 'Debian'])
expect(script).toContain('readlink -f')
return `${wslLinuxHomePath}\n`
return ownedProbeVerdict(wslLinuxHomePath)
})
const runWslProcessMock = vi.fn(async (spec: WslSpec) => {
const script = String(spec.script)
@@ -508,7 +513,7 @@ describe('CodexAccountService config sync', () => {
const execFileSyncMock = vi.fn((_command: string, args: string[]) => {
const script = decodeEncodedWslBashCommand(String(args.at(-1)))
if (script.includes('readlink -f')) {
return `${wslLinuxHomePath}\n`
return ownedProbeVerdict(wslLinuxHomePath)
}
return ''
})
@@ -641,7 +646,7 @@ describe('CodexAccountService config sync', () => {
const execFileSyncMock = vi.fn((_command: string, args: string[]) => {
const script = decodeEncodedWslBashCommand(String(args.at(-1)))
if (script.includes('readlink -f')) {
return `${wslLinuxHomePath}\n`
return ownedProbeVerdict(wslLinuxHomePath)
}
return ''
})
@@ -762,10 +767,8 @@ describe('CodexAccountService config sync', () => {
expect(script).toContain(
'test "$candidate_real" = "$managed_root_real/$expected_marker/home"'
)
expect(script).toContain(
'test "$(cat "$candidate_real/.orca-managed-home")" = "$expected_marker"'
)
return `${wslLinuxHomePath}\n`
expect(script).toContain('test "$contents" = "$expected_marker"')
return ownedProbeVerdict(wslLinuxHomePath)
}
return ''
}),
@@ -0,0 +1,386 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
import type * as NodeFs from 'node:fs'
import type * as WslPaths from '../../shared/wsl-paths'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
// STA-5616 regression: the WSL ownership probe collapsed every failure into a
// TRUST verdict, so a cold distro, a 5s timeout, or a 9p hiccup was
// indistinguishable from "this home is not Orca-owned". The host lane has had
// the owned/untrusted/indeterminate tri-state since STA-4422; only a
// *dispositive* untrusted verdict may clear durable state.
const rmSyncMock = vi.hoisted(() => vi.fn())
/** `rmSync` is mocked below, so fixture teardown needs the unmocked original. */
const realFs = vi.hoisted(() => ({ rmSync: null as typeof NodeFs.rmSync | null }))
/** Paths that fail every read with an injected errno, modelling a held 9p/AV lock. */
const fsFaults = vi.hoisted(() => {
const held = new Map<string, string>()
return {
hold(path: string, code: string): void {
held.set(path, code)
},
reset(): void {
held.clear()
},
consume(target: unknown, syscall: string): void {
const code = typeof target === 'string' ? held.get(target) : undefined
if (!code) {
return
}
const error: NodeJS.ErrnoException = new Error(`${code}: ${syscall} '${String(target)}'`)
error.code = code
error.syscall = syscall
error.path = String(target)
throw error
}
}
})
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof NodeFs>()
realFs.rmSync = actual.rmSync
const patched = {
...actual,
rmSync: rmSyncMock,
statSync: (...args: unknown[]) => {
fsFaults.consume(args[0], 'stat')
return (actual.statSync as (...a: unknown[]) => unknown)(...args)
},
lstatSync: (...args: unknown[]) => {
fsFaults.consume(args[0], 'lstat')
return (actual.lstatSync as (...a: unknown[]) => unknown)(...args)
}
}
return { ...patched, default: patched }
})
vi.mock('electron', () => ({
app: { getPath: () => '/unused-user-data' }
}))
/** Lets a real temp dir stand in for a drvfs/9p mount of a WSL managed home. */
const mountedPathAlias = vi.hoisted(() => ({ hostPath: '', linuxPath: '' }))
vi.mock('../../shared/wsl-paths', async (importOriginal) => {
const actual = await importOriginal<typeof WslPaths>()
return {
...actual,
parseWslUncPath: (path: string) =>
path === mountedPathAlias.hostPath && path !== ''
? { distro: 'Ubuntu', linuxPath: mountedPathAlias.linuxPath }
: actual.parseWslUncPath(path)
}
})
const runWslProcessMock = vi.hoisted(() => vi.fn())
vi.mock('../wsl/wsl-runner', () => ({ runWslProcess: runWslProcessMock }))
import { CodexManagedHomeLifecycle } from './codex-managed-home-lifecycle'
import { CodexManagedHomePath } from './codex-managed-home-path'
import {
ManagedCodexHomeTemporarilyUnavailableError,
UntrustedManagedCodexHomeError
} from './host-codex-managed-home-ownership'
const DISTRO = 'Ubuntu'
const ACCOUNT_ID = 'account-1'
const LINUX_HOME = `/home/dev/.local/share/orca/codex-accounts/${ACCOUNT_ID}/home`
const UNC_HOME = `\\\\wsl.localhost\\${DISTRO}${LINUX_HOME.replace(/\//g, '\\')}`
/** The tagged line the guest prints for an Orca-owned home. */
function ownedVerdict(linuxPath = LINUX_HOME): string {
return `ORCA_CODEX_HOME_VERDICT:owned:${Buffer.from(linuxPath, 'utf-8').toString('base64')}\n`
}
/** A `wsl.exe` run that never produced output: killed at the 5s timeout. */
function timeoutFailure(): Error {
const error = new Error('spawnSync wsl.exe ETIMEDOUT') as NodeJS.ErrnoException & {
signal?: string
status?: number | null
}
error.code = 'ETIMEDOUT'
error.signal = 'SIGTERM'
error.status = null
return error
}
let originalPlatform: PropertyDescriptor | undefined
function setPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', { value: platform, configurable: true })
}
beforeEach(() => {
originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
setPlatform('win32')
rmSyncMock.mockReset()
runWslProcessMock.mockReset()
fsFaults.reset()
mountedPathAlias.hostPath = ''
mountedPathAlias.linuxPath = ''
})
afterEach(() => {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
vi.restoreAllMocks()
})
describe('WSL Codex ownership probe classification', () => {
it('reports a probe timeout as indeterminate, not as an untrusted home', () => {
const paths = new CodexManagedHomePath(() => {
throw timeoutFailure()
})
let thrown: unknown
try {
paths.assert(UNC_HOME, ACCOUNT_ID)
} catch (error) {
thrown = error
}
expect(thrown).toBeInstanceOf(ManagedCodexHomeTemporarilyUnavailableError)
expect(thrown).not.toBeInstanceOf(UntrustedManagedCodexHomeError)
})
it('reports a probe that emits no verdict as indeterminate', () => {
const paths = new CodexManagedHomePath(() => '')
expect(() => paths.assert(UNC_HOME, ACCOUNT_ID)).toThrow(
ManagedCodexHomeTemporarilyUnavailableError
)
})
it('still reports a marker owned by another account as a dispositive untrusted verdict', () => {
const paths = new CodexManagedHomePath(() => 'ORCA_CODEX_HOME_VERDICT:marker-mismatch\n')
let thrown: unknown
try {
paths.assert(UNC_HOME, ACCOUNT_ID)
} catch (error) {
thrown = error
}
expect(thrown).toBeInstanceOf(UntrustedManagedCodexHomeError)
expect(thrown).not.toBeInstanceOf(ManagedCodexHomeTemporarilyUnavailableError)
})
it('still reports a missing ownership marker as a dispositive untrusted verdict', () => {
const paths = new CodexManagedHomePath(() => 'ORCA_CODEX_HOME_VERDICT:missing-marker\n')
expect(() => paths.assert(UNC_HOME, ACCOUNT_ID)).toThrow(UntrustedManagedCodexHomeError)
})
it('resolves an owned home back to its Windows spelling', () => {
const paths = new CodexManagedHomePath(() => ownedVerdict())
expect(paths.assert(UNC_HOME, ACCOUNT_ID)).toBe(UNC_HOME)
})
it('rejects a structurally foreign WSL path as untrusted without running a probe', () => {
const probe = vi.fn(() => '')
const paths = new CodexManagedHomePath(probe)
expect(() =>
paths.assert(`\\\\wsl.localhost\\${DISTRO}\\home\\dev\\.codex`, ACCOUNT_ID)
).toThrow(UntrustedManagedCodexHomeError)
expect(probe).not.toHaveBeenCalled()
})
it('rejects a managed path spelled for another account as untrusted without a probe', () => {
const probe = vi.fn(() => '')
const paths = new CodexManagedHomePath(probe)
const otherAccountHome = UNC_HOME.replace(ACCOUNT_ID, 'someone-else')
expect(() => paths.assert(otherAccountHome, ACCOUNT_ID)).toThrow(UntrustedManagedCodexHomeError)
expect(probe).not.toHaveBeenCalled()
})
})
describe('WSL managed home cleanup after a fail-once probe', () => {
it('does not delete a freshly authenticated WSL home when the probe failed transiently', () => {
// Models the real add path: the ownership re-gate inside identity read hits a
// transient fault, `add` rolls back, and the rollback's own re-gate succeeds
// because the fault was transient. Before STA-5616 that deleted the home.
let calls = 0
const paths = new CodexManagedHomePath(() => {
calls += 1
if (calls === 1) {
throw timeoutFailure()
}
return ownedVerdict()
})
const lifecycle = new CodexManagedHomeLifecycle(paths)
let addFailure: unknown
try {
paths.assert(UNC_HOME, ACCOUNT_ID)
} catch (error) {
addFailure = error
}
lifecycle.removeUnlessUnproven(addFailure, UNC_HOME, ACCOUNT_ID)
expect(rmSyncMock).not.toHaveBeenCalled()
})
it('still deletes the home when the add failed for a proven reason', () => {
// Control for the case above: same rollback, same healthy probe, but a
// failure that is not an unproven observation. Cleanup must still run.
const paths = new CodexManagedHomePath(() => ownedVerdict())
const lifecycle = new CodexManagedHomeLifecycle(paths)
lifecycle.removeUnlessUnproven(
new Error('Codex login completed, but Orca could not resolve the account email.'),
UNC_HOME,
ACCOUNT_ID
)
expect(rmSyncMock).toHaveBeenCalled()
})
it('refuses to delete a home the probe proved belongs to another account', () => {
const paths = new CodexManagedHomePath(() => 'ORCA_CODEX_HOME_VERDICT:marker-mismatch\n')
const lifecycle = new CodexManagedHomeLifecycle(paths)
let addFailure: unknown
try {
paths.assert(UNC_HOME, ACCOUNT_ID)
} catch (error) {
addFailure = error
}
lifecycle.removeUnlessUnproven(addFailure, UNC_HOME, ACCOUNT_ID)
expect(addFailure).toBeInstanceOf(UntrustedManagedCodexHomeError)
expect(rmSyncMock).not.toHaveBeenCalled()
})
})
describe('WSL managed home preparation for re-authentication', () => {
const account = {
id: ACCOUNT_ID,
email: 'dev@example.com',
managedHomePath: UNC_HOME,
managedHomeRuntime: 'wsl' as const,
wslDistro: DISTRO,
wslLinuxHomePath: LINUX_HOME,
providerAccountId: null,
workspaceLabel: null,
workspaceAccountId: null,
createdAt: 0,
updatedAt: 0,
lastAuthenticatedAt: 0
}
// A kill at the deadline can still report a zero status, so `timedOut` has to
// be read on its own rather than inferred from the exit code.
it.each([null, 0])(
'reports a preparation timed out at exit %s as indeterminate',
async (code) => {
runWslProcessMock.mockResolvedValue({
code,
stdout: '',
stderr: '',
timedOut: true,
environmentResolved: true
})
const paths = new CodexManagedHomePath(() => ownedVerdict())
await expect(paths.ensureForReauthentication(account)).rejects.toBeInstanceOf(
ManagedCodexHomeTemporarilyUnavailableError
)
}
)
it('reports a foreign directory found during preparation as untrusted', async () => {
runWslProcessMock.mockResolvedValue({
code: 41,
stdout: '',
stderr: '',
timedOut: false,
environmentResolved: true
})
const paths = new CodexManagedHomePath(() => ownedVerdict())
await expect(paths.ensureForReauthentication(account)).rejects.toBeInstanceOf(
UntrustedManagedCodexHomeError
)
})
it('reports an unexplained preparation exit as indeterminate', async () => {
runWslProcessMock.mockResolvedValue({
code: 127,
stdout: '',
stderr: 'bash: not found',
timedOut: false,
environmentResolved: true
})
const paths = new CodexManagedHomePath(() => ownedVerdict())
await expect(paths.ensureForReauthentication(account)).rejects.toBeInstanceOf(
ManagedCodexHomeTemporarilyUnavailableError
)
})
})
describe('mounted WSL Codex home reached through the filesystem, not wsl.exe', () => {
let mountRoot: string
let mountedHome: string
beforeEach(() => {
setPlatform('linux')
mountRoot = mkdtempSync(join(tmpdir(), 'orca-sta-5616-'))
mountedHome = join(mountRoot, 'home')
mkdirSync(mountedHome, { recursive: true })
writeFileSync(join(mountedHome, '.orca-managed-home'), `${ACCOUNT_ID}\n`, 'utf-8')
mountedPathAlias.hostPath = mountedHome
mountedPathAlias.linuxPath = LINUX_HOME
})
afterEach(() => {
realFs.rmSync!(mountRoot, { recursive: true, force: true })
})
function assertMounted(): string {
return new CodexManagedHomePath(() => '').assert(mountedHome, ACCOUNT_ID)
}
it('accepts a marked home under the managed root', () => {
expect(assertMounted()).toBe(mountedHome)
})
it('reports a home the filesystem refuses to stat as indeterminate', () => {
fsFaults.hold(mountedHome, 'EPERM')
expect(assertMounted).toThrow(ManagedCodexHomeTemporarilyUnavailableError)
})
it('reports an unreadable ownership marker as indeterminate, not as a missing one', () => {
fsFaults.hold(join(mountedHome, '.orca-managed-home'), 'EBUSY')
expect(assertMounted).toThrow(ManagedCodexHomeTemporarilyUnavailableError)
})
it('still reports a definitively absent marker as untrusted', () => {
realFs.rmSync!(join(mountedHome, '.orca-managed-home'))
expect(assertMounted).toThrow(UntrustedManagedCodexHomeError)
})
it('still reports a definitively absent home as untrusted', () => {
realFs.rmSync!(mountedHome, { recursive: true })
expect(assertMounted).toThrow(UntrustedManagedCodexHomeError)
})
it('still reports a marker owned by another account as untrusted', () => {
writeFileSync(join(mountedHome, '.orca-managed-home'), 'someone-else\n', 'utf-8')
expect(assertMounted).toThrow(UntrustedManagedCodexHomeError)
})
})
@@ -0,0 +1,123 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('../wsl', () => ({
toWindowsWslPath: (linuxPath: string, distro: string) =>
`\\\\wsl.localhost\\${distro}${linuxPath.replace(/\//g, '\\')}`
}))
import {
ACCOUNT_ID_MISMATCH_MESSAGE,
buildWslCodexManagedHomeProbeScript,
classifyWslCodexManagedHomeProbe,
MARKER_ACCOUNT_MISMATCH_MESSAGE,
OUTSIDE_MANAGED_ROOT_MESSAGE
} from './wsl-codex-managed-home-probe'
import {
MISSING_MANAGED_HOME_MESSAGE,
MISSING_OWNERSHIP_MARKER_MESSAGE
} from './host-codex-managed-home-ownership'
const DISTRO = 'Ubuntu'
const LINUX_HOME = '/home/dev/.local/share/orca/codex-accounts/account-1/home'
function tagged(value: string): string {
return `ORCA_CODEX_HOME_VERDICT:${value}\n`
}
function owned(linuxPath = LINUX_HOME): string {
return tagged(`owned:${Buffer.from(linuxPath, 'utf-8').toString('base64')}`)
}
describe('buildWslCodexManagedHomeProbeScript', () => {
it('single-quotes the candidate path so a crafted path cannot inject shell', () => {
const script = buildWslCodexManagedHomeProbeScript("/home/dev/'; rm -rf /; '", 'account-1')
expect(script).toContain(`candidate='/home/dev/'\\''; rm -rf /; '\\'''`)
expect(script).not.toMatch(/^\s*rm -rf/m)
})
it('pins the home to the expected account when one is given', () => {
const script = buildWslCodexManagedHomeProbeScript(LINUX_HOME, 'account-1')
expect(script).toContain(`expected_marker='account-1'`)
expect(script).toContain('test "$candidate_real" = "$managed_root_real/$expected_marker/home"')
expect(script).toContain('test "$contents" = "$expected_marker"')
})
it('accepts any managed account home when no account is given', () => {
const script = buildWslCodexManagedHomeProbeScript(LINUX_HOME)
expect(script).not.toContain('expected_marker')
expect(script).toContain('test -n "$contents" || tag marker-mismatch')
})
it('never lets the guest end on a bare non-zero exit for an ownership fact', () => {
const script = buildWslCodexManagedHomeProbeScript(LINUX_HOME, 'account-1')
// Every structural fact is reported through `tag`, which exits 0; the bare
// `exit 1`s are reserved for reads that failed and prove nothing.
expect(script).not.toContain('exit 35')
expect(script.match(/\|\| exit 1$/gm)?.length).toBeGreaterThan(0)
})
})
describe('classifyWslCodexManagedHomeProbe', () => {
it('treats a runner failure as indeterminate', () => {
const cause = new Error('spawnSync wsl.exe ETIMEDOUT')
const verdict = classifyWslCodexManagedHomeProbe({ ran: false, error: cause }, DISTRO)
expect(verdict.kind).toBe('indeterminate')
expect((verdict as { error: Error }).error.cause).toBe(cause)
})
it.each([
['', 'no output at all'],
['/home/dev/some/path\n', 'a bare path, as the pre-STA-5616 protocol emitted'],
[`${owned()}trailing chatter\n`, 'output after the verdict'],
[`${owned()}${owned()}`, 'two verdicts'],
[tagged('who-knows'), 'an unrecognised tag'],
[tagged('owned:!!!not-base64!!!'), 'an undecodable path']
])('treats %j as indeterminate (%s)', (stdout) => {
expect(classifyWslCodexManagedHomeProbe({ ran: true, stdout }, DISTRO).kind).toBe(
'indeterminate'
)
})
it.each([
['missing-home', MISSING_MANAGED_HOME_MESSAGE],
['missing-marker', MISSING_OWNERSHIP_MARKER_MESSAGE],
['marker-mismatch', MARKER_ACCOUNT_MISMATCH_MESSAGE],
['account-mismatch', ACCOUNT_ID_MISMATCH_MESSAGE],
['outside-managed-root', OUTSIDE_MANAGED_ROOT_MESSAGE]
])('treats the %s tag as a dispositive untrusted verdict', (tag, reason) => {
expect(classifyWslCodexManagedHomeProbe({ ran: true, stdout: tagged(tag) }, DISTRO)).toEqual({
kind: 'untrusted',
reason
})
})
it('decodes an owned verdict back to its Windows spelling', () => {
expect(classifyWslCodexManagedHomeProbe({ ran: true, stdout: owned() }, DISTRO)).toEqual({
kind: 'owned',
homePath: `\\\\wsl.localhost\\Ubuntu${LINUX_HOME.replace(/\//g, '\\')}`
})
})
it('tolerates CRLF and NUL padding from the wsl.exe pipe', () => {
const stdout = `${String.fromCharCode(0)} ${owned().trimEnd()}\r\n`
expect(classifyWslCodexManagedHomeProbe({ ran: true, stdout }, DISTRO).kind).toBe('owned')
})
it('carries a home whose name embeds the tag without truncating it', () => {
const oddPath = '/home/dev/.local/share/orca/codex-accounts/ORCA_CODEX_HOME_VERDICT:x/home'
const verdict = classifyWslCodexManagedHomeProbe({ ran: true, stdout: owned(oddPath) }, DISTRO)
expect(verdict).toEqual({
kind: 'owned',
homePath: `\\\\wsl.localhost\\Ubuntu${oddPath.replace(/\//g, '\\')}`
})
})
})
@@ -0,0 +1,122 @@
import { quotePosixShell } from '../../shared/wsl-login-shell-command'
import { toWindowsWslPath } from '../wsl'
import {
MISSING_MANAGED_HOME_MESSAGE,
MISSING_OWNERSHIP_MARKER_MESSAGE,
type HostCodexManagedHomeVerdict
} from './host-codex-managed-home-ownership'
/**
* Why a tagged line instead of exit codes: under `set -e` an absent home, a
* marker owned by another account, a `readlink` failure, and `wsl.exe` failing
* to start a cold distro all abort with the same status and empty stdout, so no
* exit code is observable evidence of *which* happened. The guest states its
* observation and exits 0; only a parsed tag is dispositive, and everything else
* — no tag, extra output, a throw from the runner, a timeout — is indeterminate.
* That inverts the old default under which a cold distro read as "this home is
* not Orca-owned" (STA-5616).
*/
const VERDICT_TAG = 'ORCA_CODEX_HOME_VERDICT:'
export const OUTSIDE_MANAGED_ROOT_MESSAGE =
'Managed WSL Codex home is outside Orca account storage.'
export const ACCOUNT_ID_MISMATCH_MESSAGE =
'Managed WSL Codex home does not match its persisted account ID.'
export const MARKER_ACCOUNT_MISMATCH_MESSAGE =
'Managed WSL Codex home ownership marker does not match its account ID.'
/** What the host observed of the probe run itself, before any verdict parsing. */
export type WslCodexManagedHomeProbeOutcome =
| { ran: true; stdout: string }
| { ran: false; error: unknown }
/**
* The canonical path is base64'd because it is interpolated into a
* line-oriented protocol: a newline anywhere under `$HOME` would otherwise split
* the verdict in two and read as a malformed probe.
*/
export function buildWslCodexManagedHomeProbeScript(
linuxPath: string,
expectedAccountId?: string
): string {
return [
'set -uo pipefail',
`tag() { printf '${VERDICT_TAG}%s\\n' "$1"; exit 0; }`,
`candidate=${quotePosixShell(linuxPath)}`,
'managed_root="${HOME%/}/.local/share/orca/codex-accounts"',
// Definitive absence is the one structural fact the host lane also treats as
// a verdict; re-auth recreates the home from it rather than refusing.
'test -e "$candidate" || tag missing-home',
'candidate_real=$(readlink -f -- "$candidate") || exit 1',
'managed_root_real=$(readlink -f -- "$managed_root") || exit 1',
'marker="$candidate_real/.orca-managed-home"',
'test -f "$marker" || tag missing-marker',
'contents=$(cat -- "$marker") || exit 1',
'case "$candidate_real" in "$managed_root_real"/*/home) ;; *) tag outside-managed-root ;; esac',
...(expectedAccountId === undefined
? ['test -n "$contents" || tag marker-mismatch']
: [
`expected_marker=${quotePosixShell(expectedAccountId)}`,
'test "$candidate_real" = "$managed_root_real/$expected_marker/home" || tag account-mismatch',
'test "$contents" = "$expected_marker" || tag marker-mismatch'
]),
"encoded=$(printf '%s' \"$candidate_real\" | base64 | tr -d '\\n') || exit 1",
'tag "owned:$encoded"'
].join('\n')
}
function indeterminate(message: string, cause?: unknown): HostCodexManagedHomeVerdict {
return { kind: 'indeterminate', error: new Error(message, cause ? { cause } : undefined) }
}
/** Base64 round-trips so a truncated or garbled payload cannot become a path. */
function decodeCanonicalPath(encoded: string): string | null {
if (!encoded) {
return null
}
const decoded = Buffer.from(encoded, 'base64').toString('utf-8')
return decoded && Buffer.from(decoded, 'utf-8').toString('base64') === encoded ? decoded : null
}
const UNTRUSTED_TAGS = new Map<string, string>([
['missing-home', MISSING_MANAGED_HOME_MESSAGE],
['missing-marker', MISSING_OWNERSHIP_MARKER_MESSAGE],
['marker-mismatch', MARKER_ACCOUNT_MISMATCH_MESSAGE],
['account-mismatch', ACCOUNT_ID_MISMATCH_MESSAGE],
['outside-managed-root', OUTSIDE_MANAGED_ROOT_MESSAGE]
])
export function classifyWslCodexManagedHomeProbe(
outcome: WslCodexManagedHomeProbeOutcome,
distro: string
): HostCodexManagedHomeVerdict {
if (!outcome.ran) {
return {
kind: 'indeterminate',
error: new Error('WSL Codex ownership probe could not run.', { cause: outcome.error })
}
}
const lines = outcome.stdout
.replaceAll(String.fromCharCode(0), '')
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0)
const tagged = lines.filter((line) => line.startsWith(VERDICT_TAG))
// The tag is the guest's last act, so anything after it means the run did not
// end where the protocol says it ends.
if (tagged.length !== 1 || lines.at(-1) !== tagged[0]) {
return indeterminate('WSL Codex ownership probe did not report exactly one verdict.')
}
const value = tagged[0].slice(VERDICT_TAG.length)
const untrustedReason = UNTRUSTED_TAGS.get(value)
if (untrustedReason !== undefined) {
return { kind: 'untrusted', reason: untrustedReason }
}
if (!value.startsWith('owned:')) {
return indeterminate('WSL Codex ownership probe reported an unknown verdict.')
}
const canonicalPath = decodeCanonicalPath(value.slice('owned:'.length))
return canonicalPath
? { kind: 'owned', homePath: toWindowsWslPath(canonicalPath, distro) }
: indeterminate('WSL Codex ownership probe reported an undecodable path.')
}