fix(codex): treat non-Codex transcript as stale resume metadata (#10538)

When a pane is mislabeled agent:codex but still holds a Claude
transcriptPath, the Codex resume guard threw and blocked relaunch.

Only hard-fail when the path claims Codex's dated rollout layout
(sessions/YYYY/MM/DD/rollout-*.jsonl), under any home and without
requiring the file to exist. Paths that never claimed Codex provenance
return null so the pane can relaunch.

Keying on rollout shape rather than trusted-home membership matters:
returning null only declines to override CODEX_HOME, and the renderer
has already baked 'codex resume <id>' into the command. A real rollout
under an untrusted home would otherwise resume under whichever account
is selected — and once the session bridge hardlinks rollouts across
homes, codex would find that id and resume silently under the wrong
account. Trust is decided upstream by findTrustedCodexSessionResume.

Co-authored-by: Wooseong Kim <innocarpe@users.noreply.github.com>

Closes #10517
This commit is contained in:
Wooseong Kim 2026-07-27 07:41:31 +09:00 committed by GitHub
parent c8e4488479
commit 0b0e42ab60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 70 additions and 3 deletions

View File

@ -3,6 +3,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
claimsCodexRolloutLayout,
findTrustedCodexSessionResume,
resolveTrustedCodexSessionResumeHome
} from './codex-session-resume-home'
@ -224,3 +225,47 @@ describe('resolveTrustedCodexSessionResumeHome', () => {
).resolves.toBeNull()
})
})
describe('claimsCodexRolloutLayout', () => {
it('is true for a rollout path even if the file is missing', () => {
expect(
claimsCodexRolloutLayout('/Users/example/.codex/sessions/2026/07/20/rollout-session.jsonl')
).toBe(true)
})
it('is true for compressed rollouts and Windows-separated paths', () => {
expect(
claimsCodexRolloutLayout(
'/Users/example/.codex/sessions/2026/07/20/rollout-session.jsonl.zst'
)
).toBe(true)
expect(
claimsCodexRolloutLayout(
'C:\\Users\\example\\.codex\\sessions\\2026\\07\\20\\rollout-session.jsonl'
)
).toBe(true)
})
it('is true for a rollout under a home Orca no longer trusts, so resume cannot silently fall through to the selected account', () => {
expect(
claimsCodexRolloutLayout('/removed/account/home/sessions/2026/07/20/rollout-a.jsonl')
).toBe(true)
})
it('is false for Claude (or other non-Codex) transcript paths', () => {
expect(
claimsCodexRolloutLayout(
'/Users/example/.claude/projects/-Users-example-repo/019f81b9-19a9-7651-a8d1-352d9420bd11.jsonl'
)
).toBe(false)
})
it('is false for empty provenance and JSONL misplaced inside a sessions root', () => {
expect(claimsCodexRolloutLayout(undefined)).toBe(false)
expect(claimsCodexRolloutLayout(' ')).toBe(false)
expect(claimsCodexRolloutLayout('/Users/example/.codex/sessions/rollout-a.jsonl')).toBe(false)
expect(
claimsCodexRolloutLayout('/Users/example/.codex/sessions/2026/07/20/nested/rollout-a.jsonl')
).toBe(false)
})
})

View File

@ -8,7 +8,10 @@ import {
import { listCodexSessionRolloutFilesIncrementally } from './codex-session-file-listing'
// Why: only Codex's dated rollout layout may establish account-home provenance; nested/misplaced JSONL must not select credentials.
const ROLLOUT_RELATIVE_PATH = /^\d{4}\/\d{2}\/\d{2}\/rollout-[^/]+\.jsonl(?:\.zst)?$/
const DATED_ROLLOUT_TAIL = String.raw`\d{4}/\d{2}/\d{2}/rollout-[^/]+\.jsonl(?:\.zst)?`
const ROLLOUT_RELATIVE_PATH = new RegExp(`^${DATED_ROLLOUT_TAIL}$`)
// Why: case-insensitive because trusted-home matching folds Windows path case too.
const CODEX_ROLLOUT_LAYOUT_PATH = new RegExp(`(?:^|/)sessions/${DATED_ROLLOUT_TAIL}$`, 'i')
function isCodexRolloutInsideSessionsRoot(sessionsRoot: string, filePath: string): boolean {
const relativePath = relativePathInsideRoot(sessionsRoot, filePath)
@ -76,6 +79,20 @@ export function resolveTrustedCodexSessionResumeHome(args: {
return resolveTrustedCodexSessionResume(args)?.homePath ?? null
}
/**
* True when transcriptPath claims Codex's dated rollout layout, under any home and without
* checking existence separating rejected Codex provenance from cross-agent/stale metadata.
* Not scoped to trusted homes: a rollout under a removed home is still rejected provenance,
* and admitting it would resume that session under whichever account is selected now.
*/
export function claimsCodexRolloutLayout(transcriptPath: string | undefined): boolean {
const persistedPath = transcriptPath?.trim()
if (!persistedPath) {
return false
}
return CODEX_ROLLOUT_LAYOUT_PATH.test(persistedPath.replace(/\\/g, '/'))
}
export async function findTrustedCodexSessionResume(args: {
sessionId: string
transcriptPath: string | undefined

View File

@ -172,7 +172,10 @@ import { startCodexSessionIndexHealInBackground } from './codex/codex-session-in
import { createCodexSessionMigrationScheduler } from './codex/codex-session-migration-scheduler'
import { prepareLegacySharedCodexSessionResume } from './codex/codex-legacy-session-resume'
import { resolveHostCodexSessionSourceHome } from './codex/codex-session-source-home'
import { findTrustedCodexSessionResume } from './codex/codex-session-resume-home'
import {
claimsCodexRolloutLayout,
findTrustedCodexSessionResume
} from './codex/codex-session-resume-home'
import { getSystemCodexHomePath } from './codex/codex-home-paths'
import { normalizeRuntimePathForComparison } from '../shared/cross-platform-path'
import type { AgentProviderSessionMetadata } from '../shared/agent-session-resume'
@ -852,7 +855,9 @@ async function prepareCodexSessionResumeForLaunch(args: {
trustedCodexHomes: trustedHomes
})
if (!sessionSource) {
if (args.providerSession.transcriptPath) {
// Why: an unverifiable Codex rollout still blocks; only paths that never claimed Codex
// provenance (e.g. ~/.claude/… on a pane mislabeled "codex") fall through to a normal launch.
if (claimsCodexRolloutLayout(args.providerSession.transcriptPath)) {
throw new Error(
'Orca could not verify the originating Codex session file, so automatic resume was stopped to avoid using a different account.'
)