2526 lines · 91.9 KB · TypeScript
RawDownload
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.