// How a durable session record becomes a Codex process launch. // // Every input is read back from the record the store already made durable, not // from the call that triggered the acquire. A client that attaches twice must // land in the same working directory under the same account home, and a resume // must name the thread this session actually proved — never one a caller asks // for, which is how a resume becomes a fork wearing a resume's name. import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' import { agentSessionProviderHandleChainHead } from '../../shared/agent-session-provider-handle' import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' import { resolveCodexCommand } from '../codex-cli/command' import type { AgentSessionRecordStore } from '../runtime/agent-session-record-store' import type { CodexStructuredLaunch } from './codex-structured-session-adapter' import type { CodexStructuredPermissionPolicy } from './codex-structured-permission-policy' import { resolvePinnedCodexRolloutProof } from './codex-pinned-rollout-proof' import { isWindowsProcessStartTimeAvailable } from '../windows/windows-process-table' export type CodexStructuredLaunchResolverDeps = { store: AgentSessionRecordStore /** Absolute path of a workspace on this host. Rejects when the workspace no * longer resolves, which is the case a stale mobile client hits. */ resolveWorkspacePath: (workspaceId: string) => Promise /** Overridden in tests; production scans the boot-cached PATH and version-manager dirs. */ resolveCommand?: (options?: { pathEnv?: string | null; homePath?: string }) => string /** Fresh shell/configured environment for this spawn; never written to the session record. */ resolveEnvironment?: () => Promise resolveRollout?: typeof resolvePinnedCodexRolloutProof /** Test seam for the host capability; production uses the native process table. */ isWindowsProcessStartTimeAvailable?: () => boolean /** The user's Agent Permissions setting as thread policy, re-read per acquisition. * States both postures outright — a resume inherits the last one for any field left absent. */ resolvePermissionPolicy?: () => CodexStructuredPermissionPolicy } export type CodexStructuredInvocation = { command: string environment: NodeJS.ProcessEnv | undefined } /** * The one place a structured Codex child's binary and environment are * resolved. The session launch and the session-less catalog probe both build * on it, so a probe can never list under a different binary or env than the * session it stands in for. Env VALUES stay out of the catalog fingerprint: * drift there heals on the next refresh. */ export async function resolveCodexStructuredInvocation( deps: Pick ): Promise { const environment = await deps.resolveEnvironment?.() const pathEnv = environment?.PATH ?? environment?.Path ?? null const homePath = environment?.HOME ?? environment?.USERPROFILE const command = (deps.resolveCommand ?? resolveCodexCommand)({ pathEnv, ...(homePath ? { homePath } : {}) }) return { command, environment } } export function createCodexStructuredLaunchResolver( deps: CodexStructuredLaunchResolverDeps ): (input: { identity: AgentSessionJournalIdentity }) => Promise { return async ({ identity }) => { const record = deps.store.getRecord(identity.sessionId) if (!record) { throw new Error(`no durable agent-session record for ${identity.sessionId}`) } const { location, accountHome } = record if (record.provider !== 'codex') { throw new Error(`session ${identity.sessionId} is a ${record.provider} session`) } // This adapter spawns a child on the machine the runtime itself runs on. // A session pinned elsewhere belongs to that host's runtime, and quietly // starting it here would put a second writer on the same thread. if (location.executionHostId !== LOCAL_EXECUTION_HOST_ID || location.wslDistro !== null) { throw new Error( `codex structured sessions run on the local host, not ${location.executionHostId}` ) } // Refuse before resolving launch data; a PID alone cannot prove Windows ownership. if ( process.platform === 'win32' && !(deps.isWindowsProcessStartTimeAvailable ?? isWindowsProcessStartTimeAvailable)() ) { throw new Error('codex structured sessions require Windows process creation-time proof') } if (accountHome.variable !== 'CODEX_HOME') { throw new Error(`codex sessions pin CODEX_HOME, not ${accountHome.variable}`) } const { command, environment } = await resolveCodexStructuredInvocation(deps) // `record.launchArgs` is deliberately not read: the configured CLI arguments are a terminal // concern, and the permission posture they used to smuggle in is derived per acquisition. const permissionPolicy = deps.resolvePermissionPolicy?.() const head = agentSessionProviderHandleChainHead(record.providerHandleChain) const resumeThreadId = head?.handle.provider === 'codex' ? head.handle.threadId : null // The same saved options every turn sends, so the thread and its turns name one model. const model = record.options?.model return { command, args: ['app-server'], cwd: await deps.resolveWorkspacePath(location.workspaceId), codexHome: accountHome.path, ...(environment ? { env: { ...environment } as Record } : {}), // An empty chain is a session that has never proved a thread, so it // starts one; anything else resumes the last link this session proved. resumeThreadId, // Only a thread this session created may still be one Codex never saved: a resumed, // forked or adopted head names a conversation Codex held. ...(resumeThreadId && head?.origin === 'created' ? { supersedeIfUnsaved: true } : {}), ...(permissionPolicy ? { permissionPolicy } : {}), ...(model ? { model } : {}), ...(resumeThreadId ? { resumePath: await (deps.resolveRollout ?? resolvePinnedCodexRolloutProof)( accountHome.path, resumeThreadId ) } : {}) } } }