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:
parent
c8e4488479
commit
0b0e42ab60
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue