import { ApprovalRequestId, DEFAULT_MODEL, EventId, ProviderDriverKind, ProviderItemId, type ProviderInstanceId, type ProviderApprovalDecision, type ProviderApprovalOption, type ProviderEvent, type ProviderInteractionMode, type ProviderRequestKind, type ProviderSession, type ProviderTurnStartResult, type ProviderUserInputAnswers, RuntimeMode, ThreadId, TurnId,} from "@t3tools/contracts";import { resolveSpawnCommand } from "@t3tools/shared/shell";import { normalizeModelSlug } from "@t3tools/shared/model";import * as Crypto from "effect/Crypto";import * as DateTime from "effect/DateTime";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Layer from "effect/Layer";import * as Queue from "effect/Queue";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";import * as CodexClient from "effect-codex-app-server/client";import * as CodexErrors from "effect-codex-app-server/errors";import * as CodexRpc from "effect-codex-app-server/rpc";import * as EffectCodexSchema from "effect-codex-app-server/schema"; import { buildCodexInitializeParams } from "./CodexProvider.ts";import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts";import { expandHomePath } from "../../pathExpansion.ts";import { buildCodexDeveloperInstructions, type T3CodeToolAvailability,} from "../CodexDeveloperInstructions.ts";const decodeV2TurnStartResponse = Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse); const PROVIDER = ProviderDriverKind.make("codex"); const ANSI_ESCAPE_CHAR = String.fromCharCode(27);const ANSI_ESCAPE_REGEX = new RegExp(`${ANSI_ESCAPE_CHAR}\\[[0-9;]*m`, "g");const CODEX_STDERR_LOG_REGEX = /^\d{4}-\d{2}-\d{2}T\S+\s+(TRACE|DEBUG|INFO|WARN|ERROR)\s+\S+:\s+(.*)$/;const BENIGN_ERROR_LOG_SNIPPETS = [ "state db missing rollout path for thread", "state db record_discrepancy: find_thread_path_by_id_str_in_subdir, falling_back",];const CODEX_APP_SERVER_FORCE_KILL_AFTER = "2 seconds" as const;const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [ "not found", "missing thread", "no such thread", "unknown thread", "does not exist", "no rollout found",]; export function hasConfiguredMcpServer(appServerArgs: ReadonlyArray<string> | undefined): boolean { return appServerArgs?.some((argument) => argument.includes("mcp_servers.")) === true;} function configuredMcpToolAvailability( appServerArgs: ReadonlyArray<string> | undefined, mcpCapabilities: ReadonlySet<string> | undefined,): T3CodeToolAvailability { if (!hasConfiguredMcpServer(appServerArgs)) return { browser: false, device: false }; // Callers predating the capability set attached the browser toolkit only. if (mcpCapabilities === undefined) return { browser: true, device: false }; return { browser: mcpCapabilities.has("preview"), device: mcpCapabilities.has("device") };} export const CodexResumeCursorSchema = Schema.Struct({ threadId: Schema.String,});const CodexUserInputAnswerObject = Schema.Struct({ answers: Schema.Array(Schema.String),});const isCodexResumeCursorSchema = Schema.is(CodexResumeCursorSchema);const isCodexUserInputAnswerObject = Schema.is(CodexUserInputAnswerObject);const NullableMcpElicitationString = Schema.NullOr(Schema.String);const McpElicitationMetadata = Schema.Struct({ app: Schema.optionalKey(NullableMcpElicitationString), app_name: Schema.optionalKey(NullableMcpElicitationString), appName: Schema.optionalKey(NullableMcpElicitationString), connector_name: Schema.optionalKey(NullableMcpElicitationString), connectorName: Schema.optionalKey(NullableMcpElicitationString), allowPersistentApproval: Schema.optionalKey(Schema.NullOr(Schema.Boolean)), persist: Schema.optionalKey( Schema.NullOr(Schema.Union([Schema.String, Schema.Array(Schema.String)])), ), target: Schema.optionalKey( Schema.NullOr( Schema.Struct({ app: Schema.optionalKey(NullableMcpElicitationString), name: Schema.optionalKey(NullableMcpElicitationString), }), ), ), tool_params: Schema.optionalKey( Schema.NullOr( Schema.Struct({ app: Schema.optionalKey(NullableMcpElicitationString), app_name: Schema.optionalKey(NullableMcpElicitationString), }), ), ),});const McpElicitationFormField = Schema.Struct({ type: Schema.optionalKey(NullableMcpElicitationString), title: Schema.optionalKey(NullableMcpElicitationString), description: Schema.optionalKey(NullableMcpElicitationString), default: Schema.optionalKey(Schema.Unknown), enum: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.String))), enumNames: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.String))), oneOf: Schema.optionalKey( Schema.NullOr( Schema.Array( Schema.Struct({ const: Schema.String, title: Schema.optionalKey(NullableMcpElicitationString), }), ), ), ),});const McpElicitationForm = Schema.Struct({ properties: Schema.optionalKey(Schema.Record(Schema.String, McpElicitationFormField)), required: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.String))),});const isMcpElicitationMetadata = Schema.is(McpElicitationMetadata);const isMcpElicitationForm = Schema.is(McpElicitationForm); // TODO: Verify `packages/effect-codex-app-server/scripts/generate.ts` so the generated// `V2TurnStartParams` schema includes `collaborationMode` directly.const CodexTurnStartParamsWithCollaborationMode = EffectCodexSchema.V2TurnStartParams.pipe( Schema.fieldsAssign({ collaborationMode: Schema.optionalKey(EffectCodexSchema.V2TurnStartParams__CollaborationMode), }),);const decodeCodexTurnStartParamsWithCollaborationMode = Schema.decodeUnknownEffect( CodexTurnStartParamsWithCollaborationMode,);const CodexChildResumeMetadata = Schema.Struct({ thread: Schema.Struct({ id: Schema.String }), model: Schema.String, reasoningEffort: Schema.optionalKey(Schema.NullOr(Schema.String)),});const decodeCodexChildResumeMetadata = Schema.decodeUnknownEffect(CodexChildResumeMetadata); export type CodexTurnStartParamsWithCollaborationMode = typeof CodexTurnStartParamsWithCollaborationMode.Type; export type CodexResumeCursor = typeof CodexResumeCursorSchema.Type;type CodexServiceTier = NonNullable<EffectCodexSchema.V2ThreadStartParams["serviceTier"]>;type CodexThreadItem = | EffectCodexSchema.V2ThreadReadResponse["thread"]["turns"][number]["items"][number] | EffectCodexSchema.V2ThreadRollbackResponse["thread"]["turns"][number]["items"][number]; export interface CodexSessionRuntimeOptions { readonly threadId: ThreadId; readonly providerInstanceId?: ProviderInstanceId; readonly binaryPath: string; readonly homePath?: string; readonly launchArgs?: string; readonly environment?: NodeJS.ProcessEnv; readonly cwd: string; readonly runtimeMode: RuntimeMode; readonly model?: string; readonly serviceTier?: CodexServiceTier | undefined; readonly resumeCursor?: CodexResumeCursor; readonly appServerArgs?: ReadonlyArray<string>; /** Capabilities the session's `t3-code` MCP credential grants; drives the prompt blocks. */ readonly mcpCapabilities?: ReadonlySet<string>;} export interface CodexSessionRuntimeSendTurnInput { readonly input?: string; readonly attachments?: ReadonlyArray<{ readonly type: "image"; readonly url: string; }>; readonly model?: string; readonly serviceTier?: CodexServiceTier | undefined; readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort | undefined; readonly interactionMode?: ProviderInteractionMode;} export interface CodexThreadTurnSnapshot { readonly id: TurnId; readonly items: ReadonlyArray<CodexThreadItem>;} export interface CodexThreadSnapshot { readonly threadId: string; readonly turns: ReadonlyArray<CodexThreadTurnSnapshot>;} export interface CodexSessionRuntimeShape { readonly start: () => Effect.Effect<ProviderSession, CodexSessionRuntimeError>; readonly getSession: Effect.Effect<ProviderSession>; readonly sendTurn: ( input: CodexSessionRuntimeSendTurnInput, ) => Effect.Effect<ProviderTurnStartResult, CodexSessionRuntimeError>; readonly compactThread: Effect.Effect<void, CodexSessionRuntimeError>; readonly interruptTurn: (turnId?: TurnId) => Effect.Effect<void, CodexSessionRuntimeError>; readonly readThread: Effect.Effect<CodexThreadSnapshot, CodexSessionRuntimeError>; readonly rollbackThread: ( numTurns: number, ) => Effect.Effect<CodexThreadSnapshot, CodexSessionRuntimeError>; readonly uploadFeedback: ( reason?: string, ) => Effect.Effect<EffectCodexSchema.V2FeedbackUploadResponse, CodexSessionRuntimeError>; readonly respondToRequest: ( requestId: ApprovalRequestId, decision: ProviderApprovalDecision, ) => Effect.Effect<void, CodexSessionRuntimeError>; readonly respondToUserInput: ( requestId: ApprovalRequestId, answers: ProviderUserInputAnswers, ) => Effect.Effect<void, CodexSessionRuntimeError>; readonly events: Stream.Stream<ProviderEvent, never>; readonly close: Effect.Effect<void>;} export type CodexSessionRuntimeError = | CodexErrors.CodexAppServerError | CodexSessionRuntimePendingApprovalNotFoundError | CodexSessionRuntimePendingUserInputNotFoundError | CodexSessionRuntimeInvalidUserInputAnswersError | CodexSessionRuntimeThreadIdMissingError; export class CodexSessionRuntimePendingApprovalNotFoundError extends Schema.TaggedError<CodexSessionRuntimePendingApprovalNotFoundError>()( "CodexSessionRuntimePendingApprovalNotFoundError", { requestId: Schema.String, },) { override get message(): string { return `Unknown pending Codex approval request: ${this.requestId}`; }} export class CodexSessionRuntimePendingUserInputNotFoundError extends Schema.TaggedError<CodexSessionRuntimePendingUserInputNotFoundError>()( "CodexSessionRuntimePendingUserInputNotFoundError", { requestId: Schema.String, },) { override get message(): string { return `Unknown pending Codex user input request: ${this.requestId}`; }} export class CodexSessionRuntimeInvalidUserInputAnswersError extends Schema.TaggedError<CodexSessionRuntimeInvalidUserInputAnswersError>()( "CodexSessionRuntimeInvalidUserInputAnswersError", { questionId: Schema.String, },) { override get message(): string { return `Invalid Codex user input answers for question '${this.questionId}'`; }} export class CodexSessionRuntimeThreadIdMissingError extends Schema.TaggedError<CodexSessionRuntimeThreadIdMissingError>()( "CodexSessionRuntimeThreadIdMissingError", { threadId: Schema.String, },) { override get message(): string { return `Codex session is missing a provider thread id for ${this.threadId}`; }} interface PendingApproval { readonly requestId: ApprovalRequestId; readonly jsonRpcId: string; readonly requestKind: ProviderRequestKind; readonly turnId: TurnId | undefined; readonly itemId: ProviderItemId | undefined; readonly decision: Deferred.Deferred<ProviderApprovalDecision>;} interface ApprovalCorrelation { readonly requestId: ApprovalRequestId; readonly requestKind: ProviderRequestKind; readonly turnId: TurnId | undefined; readonly itemId: ProviderItemId | undefined;} interface PendingUserInput { readonly requestId: ApprovalRequestId; readonly turnId: TurnId | undefined; readonly itemId: ProviderItemId | undefined; readonly answers: Deferred.Deferred<ProviderUserInputAnswers>;} type McpElicitationPersistenceDecision = Extract< ProviderApprovalDecision, "acceptForSession" | "acceptAlways">; function mcpElicitationPersistenceDecision( value: string,): McpElicitationPersistenceDecision | null { const normalized = value.toLowerCase(); if (normalized.includes("session")) return "acceptForSession"; if ( normalized.includes("always") || normalized.includes("permanent") || normalized.includes("forever") || normalized.includes("persistent") ) { return "acceptAlways"; } return null;} function mcpElicitationFormFields(payload: EffectCodexSchema.McpServerElicitationRequestParams) { if (payload.mode === "url" || !isMcpElicitationForm(payload.requestedSchema)) { return undefined; } return payload.requestedSchema;} function mcpElicitationFieldOptions(field: typeof McpElicitationFormField.Type) { if (field.oneOf) { return field.oneOf.map((option) => ({ value: option.const, label: option.title })); } return (field.enum ?? []).map((value, index) => ({ value, label: field.enumNames?.[index], }));} function isMcpElicitationPersistenceField( key: string, field: typeof McpElicitationFormField.Type,): boolean { return ( mcpElicitationPersistenceDecision(key) !== null || key.toLowerCase() === "persist" || mcpElicitationPersistenceDecision(field.title ?? "") !== null || mcpElicitationPersistenceDecision(field.description ?? "") !== null );} /** Returns the app and approval choices advertised by an MCP elicitation. */export function describeMcpElicitation( payload: EffectCodexSchema.McpServerElicitationRequestParams,): { readonly appName: string; readonly options: ReadonlyArray<ProviderApprovalOption> } { const metadata = isMcpElicitationMetadata(payload._meta) ? payload._meta : undefined; const appName = metadata?.app_name ?? metadata?.appName ?? metadata?.app ?? metadata?.target?.app ?? metadata?.target?.name ?? metadata?.tool_params?.app_name ?? metadata?.tool_params?.app ?? payload.message.match(/^Allow ChatGPT to use (.+?)\?$/i)?.[1] ?? metadata?.connector_name ?? metadata?.connectorName ?? payload.serverName; const persistenceOptions = new Map<McpElicitationPersistenceDecision, string>(); const persist = metadata?.persist; for (const value of typeof persist === "string" ? [persist] : (persist ?? [])) { const decision = mcpElicitationPersistenceDecision(value); if (decision) persistenceOptions.set(decision, ""); } if (metadata?.allowPersistentApproval) { persistenceOptions.set("acceptAlways", ""); } const form = mcpElicitationFormFields(payload); for (const [key, field] of Object.entries(form?.properties ?? {})) { for (const option of mcpElicitationFieldOptions(field)) { const decision = mcpElicitationPersistenceDecision(option.value); if (decision) persistenceOptions.set(decision, option.label ?? ""); } if (field.type === "boolean" && isMcpElicitationPersistenceField(key, field)) { persistenceOptions.set("acceptAlways", field.title ?? ""); } } return { appName, options: [ { decision: "cancel", label: "Cancel" }, { decision: "decline", label: "Decline" }, ...(persistenceOptions.has("acceptForSession") && toMcpElicitationResponse(payload, "acceptForSession").action === "accept" ? [ { decision: "acceptForSession" as const, label: persistenceOptions.get("acceptForSession") || "Always allow this session", }, ] : []), ...(persistenceOptions.has("acceptAlways") && toMcpElicitationResponse(payload, "acceptAlways").action === "accept" ? [ { decision: "acceptAlways" as const, label: persistenceOptions.get("acceptAlways") || "Always allow", }, ] : []), { decision: "accept", label: "Approve" }, ], };} /** Converts a T3 approval decision into the MCP elicitation wire response. */export function toMcpElicitationResponse( payload: EffectCodexSchema.McpServerElicitationRequestParams, decision: ProviderApprovalDecision,): EffectCodexSchema.McpServerElicitationRequestResponse { if (decision === "decline" || decision === "cancel") { return { action: decision }; } if (payload.mode === "url") { return { action: "decline" }; } const persist = decision === "acceptForSession" ? "session" : decision === "acceptAlways" ? "always" : undefined; const form = mcpElicitationFormFields(payload); const content: Record<string, unknown> = {}; for (const [key, field] of Object.entries(form?.properties ?? {})) { const options = mcpElicitationFieldOptions(field); const chosenOption = options.find((option) => persist ? mcpElicitationPersistenceDecision(option.value) === decision : /once|accept|approve|allow/i.test(option.value) && mcpElicitationPersistenceDecision(option.value) === null, ); if (chosenOption) { content[key] = chosenOption.value; } else if (field.type === "boolean" && isMcpElicitationPersistenceField(key, field)) { content[key] = decision === "acceptAlways"; } else if (field.default !== undefined && field.default !== null) { content[key] = field.default; } } if (form?.required?.some((key) => !Object.hasOwn(content, key))) { return { action: "decline" }; } return { action: "accept", ...(persist ? { _meta: { persist } } : {}), ...(form ? { content } : {}), };} type CodexServerNotification = { readonly [M in CodexRpc.ServerNotificationMethod]: { readonly method: M; readonly params: CodexRpc.ServerNotificationParamsByMethod[M]; };}[CodexRpc.ServerNotificationMethod]; function makeCodexServerNotification<M extends CodexRpc.ServerNotificationMethod>( method: M, params: CodexRpc.ServerNotificationParamsByMethod[M],): CodexServerNotification { return { method, params } as CodexServerNotification;} function normalizeCodexModelSlug( model: string | undefined | null, preferredId?: string,): string | undefined { const normalized = normalizeModelSlug(model); if (!normalized) { return undefined; } if (preferredId?.endsWith("-codex") && preferredId !== normalized) { return preferredId; } return normalized;Lines 1–500 of 2526. Load more
import { ApprovalRequestId, DEFAULT_MODEL, EventId, ProviderDriverKind, ProviderItemId, type ProviderInstanceId, type ProviderApprovalDecision, type ProviderApprovalOption, type ProviderEvent, type ProviderInteractionMode, type ProviderRequestKind, type ProviderSession, type ProviderTurnStartResult, type ProviderUserInputAnswers, RuntimeMode, ThreadId, TurnId, } from "@t3tools/contracts"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { normalizeModelSlug } from "@t3tools/shared/model"; import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import * as CodexClient from "effect-codex-app-server/client"; import * as CodexErrors from "effect-codex-app-server/errors"; import * as CodexRpc from "effect-codex-app-server/rpc"; import * as EffectCodexSchema from "effect-codex-app-server/schema"; import { buildCodexInitializeParams } from "./CodexProvider.ts"; import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts"; import { expandHomePath } from "../../pathExpansion.ts"; import { buildCodexDeveloperInstructions, type T3CodeToolAvailability, } from "../CodexDeveloperInstructions.ts"; const decodeV2TurnStartResponse = Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse); const PROVIDER = ProviderDriverKind.make("codex"); const ANSI_ESCAPE_CHAR = String.fromCharCode(27); const ANSI_ESCAPE_REGEX = new RegExp(`${ANSI_ESCAPE_CHAR}\\[[0-9;]*m`, "g"); const CODEX_STDERR_LOG_REGEX = /^\d{4}-\d{2}-\d{2}T\S+\s+(TRACE|DEBUG|INFO|WARN|ERROR)\s+\S+:\s+(.*)$/; const BENIGN_ERROR_LOG_SNIPPETS = [ "state db missing rollout path for thread", "state db record_discrepancy: find_thread_path_by_id_str_in_subdir, falling_back", ]; const CODEX_APP_SERVER_FORCE_KILL_AFTER = "2 seconds" as const; const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [ "not found", "missing thread", "no such thread", "unknown thread", "does not exist", "no rollout found", ]; export function hasConfiguredMcpServer(appServerArgs: ReadonlyArray<string> | undefined): boolean { return appServerArgs?.some((argument) => argument.includes("mcp_servers.")) === true; } function configuredMcpToolAvailability( appServerArgs: ReadonlyArray<string> | undefined, mcpCapabilities: ReadonlySet<string> | undefined, ): T3CodeToolAvailability { if (!hasConfiguredMcpServer(appServerArgs)) return { browser: false, device: false }; // Callers predating the capability set attached the browser toolkit only. if (mcpCapabilities === undefined) return { browser: true, device: false }; return { browser: mcpCapabilities.has("preview"), device: mcpCapabilities.has("device") }; } export const CodexResumeCursorSchema = Schema.Struct({ threadId: Schema.String, }); const CodexUserInputAnswerObject = Schema.Struct({ answers: Schema.Array(Schema.String), }); const isCodexResumeCursorSchema = Schema.is(CodexResumeCursorSchema); const isCodexUserInputAnswerObject = Schema.is(CodexUserInputAnswerObject); const NullableMcpElicitationString = Schema.NullOr(Schema.String); const McpElicitationMetadata = Schema.Struct({ app: Schema.optionalKey(NullableMcpElicitationString), app_name: Schema.optionalKey(NullableMcpElicitationString), appName: Schema.optionalKey(NullableMcpElicitationString), connector_name: Schema.optionalKey(NullableMcpElicitationString), connectorName: Schema.optionalKey(NullableMcpElicitationString), allowPersistentApproval: Schema.optionalKey(Schema.NullOr(Schema.Boolean)), persist: Schema.optionalKey( Schema.NullOr(Schema.Union([Schema.String, Schema.Array(Schema.String)])), ), target: Schema.optionalKey( Schema.NullOr( Schema.Struct({ app: Schema.optionalKey(NullableMcpElicitationString), name: Schema.optionalKey(NullableMcpElicitationString), }), ), ), tool_params: Schema.optionalKey( Schema.NullOr( Schema.Struct({ app: Schema.optionalKey(NullableMcpElicitationString), app_name: Schema.optionalKey(NullableMcpElicitationString), }), ), ), }); const McpElicitationFormField = Schema.Struct({ type: Schema.optionalKey(NullableMcpElicitationString), title: Schema.optionalKey(NullableMcpElicitationString), description: Schema.optionalKey(NullableMcpElicitationString), default: Schema.optionalKey(Schema.Unknown), enum: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.String))), enumNames: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.String))), oneOf: Schema.optionalKey( Schema.NullOr( Schema.Array( Schema.Struct({ const: Schema.String, title: Schema.optionalKey(NullableMcpElicitationString), }), ), ), ), }); const McpElicitationForm = Schema.Struct({ properties: Schema.optionalKey(Schema.Record(Schema.String, McpElicitationFormField)), required: Schema.optionalKey(Schema.NullOr(Schema.Array(Schema.String))), }); const isMcpElicitationMetadata = Schema.is(McpElicitationMetadata); const isMcpElicitationForm = Schema.is(McpElicitationForm); // TODO: Verify `packages/effect-codex-app-server/scripts/generate.ts` so the generated // `V2TurnStartParams` schema includes `collaborationMode` directly. const CodexTurnStartParamsWithCollaborationMode = EffectCodexSchema.V2TurnStartParams.pipe( Schema.fieldsAssign({ collaborationMode: Schema.optionalKey(EffectCodexSchema.V2TurnStartParams__CollaborationMode), }), ); const decodeCodexTurnStartParamsWithCollaborationMode = Schema.decodeUnknownEffect( CodexTurnStartParamsWithCollaborationMode, ); const CodexChildResumeMetadata = Schema.Struct({ thread: Schema.Struct({ id: Schema.String }), model: Schema.String, reasoningEffort: Schema.optionalKey(Schema.NullOr(Schema.String)), }); const decodeCodexChildResumeMetadata = Schema.decodeUnknownEffect(CodexChildResumeMetadata); export type CodexTurnStartParamsWithCollaborationMode = typeof CodexTurnStartParamsWithCollaborationMode.Type; export type CodexResumeCursor = typeof CodexResumeCursorSchema.Type; type CodexServiceTier = NonNullable<EffectCodexSchema.V2ThreadStartParams["serviceTier"]>; type CodexThreadItem = | EffectCodexSchema.V2ThreadReadResponse["thread"]["turns"][number]["items"][number] | EffectCodexSchema.V2ThreadRollbackResponse["thread"]["turns"][number]["items"][number]; export interface CodexSessionRuntimeOptions { readonly threadId: ThreadId; readonly providerInstanceId?: ProviderInstanceId; readonly binaryPath: string; readonly homePath?: string; readonly launchArgs?: string; readonly environment?: NodeJS.ProcessEnv; readonly cwd: string; readonly runtimeMode: RuntimeMode; readonly model?: string; readonly serviceTier?: CodexServiceTier | undefined; readonly resumeCursor?: CodexResumeCursor; readonly appServerArgs?: ReadonlyArray<string>; /** Capabilities the session's `t3-code` MCP credential grants; drives the prompt blocks. */ readonly mcpCapabilities?: ReadonlySet<string>; } export interface CodexSessionRuntimeSendTurnInput { readonly input?: string; readonly attachments?: ReadonlyArray<{ readonly type: "image"; readonly url: string; }>; readonly model?: string; readonly serviceTier?: CodexServiceTier | undefined; readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort | undefined; readonly interactionMode?: ProviderInteractionMode; } export interface CodexThreadTurnSnapshot { readonly id: TurnId; readonly items: ReadonlyArray<CodexThreadItem>; } export interface CodexThreadSnapshot { readonly threadId: string; readonly turns: ReadonlyArray<CodexThreadTurnSnapshot>; } export interface CodexSessionRuntimeShape { readonly start: () => Effect.Effect<ProviderSession, CodexSessionRuntimeError>; readonly getSession: Effect.Effect<ProviderSession>; readonly sendTurn: ( input: CodexSessionRuntimeSendTurnInput, ) => Effect.Effect<ProviderTurnStartResult, CodexSessionRuntimeError>; readonly compactThread: Effect.Effect<void, CodexSessionRuntimeError>; readonly interruptTurn: (turnId?: TurnId) => Effect.Effect<void, CodexSessionRuntimeError>; readonly readThread: Effect.Effect<CodexThreadSnapshot, CodexSessionRuntimeError>; readonly rollbackThread: ( numTurns: number, ) => Effect.Effect<CodexThreadSnapshot, CodexSessionRuntimeError>; readonly uploadFeedback: ( reason?: string, ) => Effect.Effect<EffectCodexSchema.V2FeedbackUploadResponse, CodexSessionRuntimeError>; readonly respondToRequest: ( requestId: ApprovalRequestId, decision: ProviderApprovalDecision, ) => Effect.Effect<void, CodexSessionRuntimeError>; readonly respondToUserInput: ( requestId: ApprovalRequestId, answers: ProviderUserInputAnswers, ) => Effect.Effect<void, CodexSessionRuntimeError>; readonly events: Stream.Stream<ProviderEvent, never>; readonly close: Effect.Effect<void>; } export type CodexSessionRuntimeError = | CodexErrors.CodexAppServerError | CodexSessionRuntimePendingApprovalNotFoundError | CodexSessionRuntimePendingUserInputNotFoundError | CodexSessionRuntimeInvalidUserInputAnswersError | CodexSessionRuntimeThreadIdMissingError; export class CodexSessionRuntimePendingApprovalNotFoundError extends Schema.TaggedError<CodexSessionRuntimePendingApprovalNotFoundError>()( "CodexSessionRuntimePendingApprovalNotFoundError", { requestId: Schema.String, }, ) { override get message(): string { return `Unknown pending Codex approval request: ${this.requestId}`; } } export class CodexSessionRuntimePendingUserInputNotFoundError extends Schema.TaggedError<CodexSessionRuntimePendingUserInputNotFoundError>()( "CodexSessionRuntimePendingUserInputNotFoundError", { requestId: Schema.String, }, ) { override get message(): string { return `Unknown pending Codex user input request: ${this.requestId}`; } } export class CodexSessionRuntimeInvalidUserInputAnswersError extends Schema.TaggedError<CodexSessionRuntimeInvalidUserInputAnswersError>()( "CodexSessionRuntimeInvalidUserInputAnswersError", { questionId: Schema.String, }, ) { override get message(): string { return `Invalid Codex user input answers for question '${this.questionId}'`; } } export class CodexSessionRuntimeThreadIdMissingError extends Schema.TaggedError<CodexSessionRuntimeThreadIdMissingError>()( "CodexSessionRuntimeThreadIdMissingError", { threadId: Schema.String, }, ) { override get message(): string { return `Codex session is missing a provider thread id for ${this.threadId}`; } } interface PendingApproval { readonly requestId: ApprovalRequestId; readonly jsonRpcId: string; readonly requestKind: ProviderRequestKind; readonly turnId: TurnId | undefined; readonly itemId: ProviderItemId | undefined; readonly decision: Deferred.Deferred<ProviderApprovalDecision>; } interface ApprovalCorrelation { readonly requestId: ApprovalRequestId; readonly requestKind: ProviderRequestKind; readonly turnId: TurnId | undefined; readonly itemId: ProviderItemId | undefined; } interface PendingUserInput { readonly requestId: ApprovalRequestId; readonly turnId: TurnId | undefined; readonly itemId: ProviderItemId | undefined; readonly answers: Deferred.Deferred<ProviderUserInputAnswers>; } type McpElicitationPersistenceDecision = Extract< ProviderApprovalDecision, "acceptForSession" | "acceptAlways" >; function mcpElicitationPersistenceDecision( value: string, ): McpElicitationPersistenceDecision | null { const normalized = value.toLowerCase(); if (normalized.includes("session")) return "acceptForSession"; if ( normalized.includes("always") || normalized.includes("permanent") || normalized.includes("forever") || normalized.includes("persistent") ) { return "acceptAlways"; } return null; } function mcpElicitationFormFields(payload: EffectCodexSchema.McpServerElicitationRequestParams) { if (payload.mode === "url" || !isMcpElicitationForm(payload.requestedSchema)) { return undefined; } return payload.requestedSchema; } function mcpElicitationFieldOptions(field: typeof McpElicitationFormField.Type) { if (field.oneOf) { return field.oneOf.map((option) => ({ value: option.const, label: option.title })); } return (field.enum ?? []).map((value, index) => ({ value, label: field.enumNames?.[index], })); } function isMcpElicitationPersistenceField( key: string, field: typeof McpElicitationFormField.Type, ): boolean { return ( mcpElicitationPersistenceDecision(key) !== null || key.toLowerCase() === "persist" || mcpElicitationPersistenceDecision(field.title ?? "") !== null || mcpElicitationPersistenceDecision(field.description ?? "") !== null ); } /** Returns the app and approval choices advertised by an MCP elicitation. */ export function describeMcpElicitation( payload: EffectCodexSchema.McpServerElicitationRequestParams, ): { readonly appName: string; readonly options: ReadonlyArray<ProviderApprovalOption> } { const metadata = isMcpElicitationMetadata(payload._meta) ? payload._meta : undefined; const appName = metadata?.app_name ?? metadata?.appName ?? metadata?.app ?? metadata?.target?.app ?? metadata?.target?.name ?? metadata?.tool_params?.app_name ?? metadata?.tool_params?.app ?? payload.message.match(/^Allow ChatGPT to use (.+?)\?$/i)?.[1] ?? metadata?.connector_name ?? metadata?.connectorName ?? payload.serverName; const persistenceOptions = new Map<McpElicitationPersistenceDecision, string>(); const persist = metadata?.persist; for (const value of typeof persist === "string" ? [persist] : (persist ?? [])) { const decision = mcpElicitationPersistenceDecision(value); if (decision) persistenceOptions.set(decision, ""); } if (metadata?.allowPersistentApproval) { persistenceOptions.set("acceptAlways", ""); } const form = mcpElicitationFormFields(payload); for (const [key, field] of Object.entries(form?.properties ?? {})) { for (const option of mcpElicitationFieldOptions(field)) { const decision = mcpElicitationPersistenceDecision(option.value); if (decision) persistenceOptions.set(decision, option.label ?? ""); } if (field.type === "boolean" && isMcpElicitationPersistenceField(key, field)) { persistenceOptions.set("acceptAlways", field.title ?? ""); } } return { appName, options: [ { decision: "cancel", label: "Cancel" }, { decision: "decline", label: "Decline" }, ...(persistenceOptions.has("acceptForSession") && toMcpElicitationResponse(payload, "acceptForSession").action === "accept" ? [ { decision: "acceptForSession" as const, label: persistenceOptions.get("acceptForSession") || "Always allow this session", }, ] : []), ...(persistenceOptions.has("acceptAlways") && toMcpElicitationResponse(payload, "acceptAlways").action === "accept" ? [ { decision: "acceptAlways" as const, label: persistenceOptions.get("acceptAlways") || "Always allow", }, ] : []), { decision: "accept", label: "Approve" }, ], }; } /** Converts a T3 approval decision into the MCP elicitation wire response. */ export function toMcpElicitationResponse( payload: EffectCodexSchema.McpServerElicitationRequestParams, decision: ProviderApprovalDecision, ): EffectCodexSchema.McpServerElicitationRequestResponse { if (decision === "decline" || decision === "cancel") { return { action: decision }; } if (payload.mode === "url") { return { action: "decline" }; } const persist = decision === "acceptForSession" ? "session" : decision === "acceptAlways" ? "always" : undefined; const form = mcpElicitationFormFields(payload); const content: Record<string, unknown> = {}; for (const [key, field] of Object.entries(form?.properties ?? {})) { const options = mcpElicitationFieldOptions(field); const chosenOption = options.find((option) => persist ? mcpElicitationPersistenceDecision(option.value) === decision : /once|accept|approve|allow/i.test(option.value) && mcpElicitationPersistenceDecision(option.value) === null, ); if (chosenOption) { content[key] = chosenOption.value; } else if (field.type === "boolean" && isMcpElicitationPersistenceField(key, field)) { content[key] = decision === "acceptAlways"; } else if (field.default !== undefined && field.default !== null) { content[key] = field.default; } } if (form?.required?.some((key) => !Object.hasOwn(content, key))) { return { action: "decline" }; } return { action: "accept", ...(persist ? { _meta: { persist } } : {}), ...(form ? { content } : {}), }; } type CodexServerNotification = { readonly [M in CodexRpc.ServerNotificationMethod]: { readonly method: M; readonly params: CodexRpc.ServerNotificationParamsByMethod[M]; }; }[CodexRpc.ServerNotificationMethod]; function makeCodexServerNotification<M extends CodexRpc.ServerNotificationMethod>( method: M, params: CodexRpc.ServerNotificationParamsByMethod[M], ): CodexServerNotification { return { method, params } as CodexServerNotification; } function normalizeCodexModelSlug( model: string | undefined | null, preferredId?: string, ): string | undefined { const normalized = normalizeModelSlug(model); if (!normalized) { return undefined; } if (preferredId?.endsWith("-codex") && preferredId !== normalized) { return preferredId; } return normalized;