/** * Codex app-server agent harness registration and lazy runtime boundaries. */ import type { AgentHarness, AgentHarnessCompactParams, AgentHarnessCompactResult, ContextEngineHostCapability, } from "openclaw/plugin-sdk/agent-harness-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import type { PluginRuntime } from "openclaw/plugin-sdk/plugin-runtime"; import { completeWithPreparedSimpleCompletionModel } from "openclaw/plugin-sdk/simple-completion-runtime"; import type { CodexAppServerBindingStore } from "./src/app-server/session-binding.js"; import type { CodexSessionCatalogControl } from "./src/session-catalog-types.js"; // `codex` is legacy input only until Part 2 doctor migration rewrites stored refs. // New runtime identity uses the `openai` provider. const DEFAULT_CODEX_HARNESS_PROVIDER_IDS = new Set(["codex", "openai"]); const SHARED_CODEX_APP_SERVER_CLIENT_DISPOSER = Symbol.for("openclaw.codexAppServerClientDisposer"); const CODEX_APP_SERVER_CONTEXT_ENGINE_HOST_CAPABILITIES = [ "bootstrap", "assemble-before-prompt", "after-turn", "maintain", "compact", "runtime-llm-complete", "thread-bootstrap-projection", ] as const satisfies readonly ContextEngineHostCapability[]; type CodexAppServerAgentHarness = AgentHarness & { compactAfterContextEngine?( params: AgentHarnessCompactParams, ): Promise; }; async function disposeSharedCodexAppServerClients(): Promise { const dispose = ( globalThis as typeof globalThis & { [SHARED_CODEX_APP_SERVER_CLIENT_DISPOSER]?: () => Promise; } )[SHARED_CODEX_APP_SERVER_CLIENT_DISPOSER]; await dispose?.(); } /** * Creates the Codex app-server harness used for attempts, side questions, * compaction, reset, and disposal. */ export function createCodexAppServerAgentHarness(options: { id?: string; label?: string; providerIds?: Iterable; pluginConfig?: unknown; resolvePluginConfig?: () => unknown; resolveConfig?: () => OpenClawConfig | undefined; runtime?: PluginRuntime; bindingStore: CodexAppServerBindingStore; sessionCatalogControl?: CodexSessionCatalogControl; }): AgentHarness { const harnessRuntimeId = options?.id ?? "codex"; const normalizedHarnessRuntimeId = harnessRuntimeId.trim().toLowerCase(); const providerIds = new Set( [...(options?.providerIds ?? DEFAULT_CODEX_HARNESS_PROVIDER_IDS)].map((id) => id.trim().toLowerCase(), ), ); const sessionCatalogControl = options.sessionCatalogControl; const sessionRuntime = options.runtime; const harness: CodexAppServerAgentHarness = { id: harnessRuntimeId, label: options?.label ?? "Codex agent harness", autoSelection: { providerIds: [...providerIds] }, delegatedExecutionPluginIds: ["voice-call"], contextEngineHostCapabilities: CODEX_APP_SERVER_CONTEXT_ENGINE_HOST_CAPABILITIES, deliveryDefaults: { visibleReplies: "message_tool", }, authBootstrap: "harness", ...(sessionCatalogControl && sessionRuntime ? { sessionFork: { upstreamKinds: ["codex-app-server"] as const, fork: async (params) => { const { forkCodexUpstreamSession } = await import("./src/app-server/upstream-session-fork.js"); return await forkCodexUpstreamSession(params, { bindingStore: options.bindingStore, control: sessionCatalogControl, harnessRuntimeId, resolveConfig: options.resolveConfig, runtime: sessionRuntime, }); }, }, } : {}), authBinding: { fingerprint: async (params) => { const { fingerprintCodexAppServerAuthBinding } = await import("./src/app-server/auth-binding.js"); return fingerprintCodexAppServerAuthBinding(params); }, }, runtimeArtifact: { validate: async (binding) => { const { validateCodexAppServerRuntimeArtifact } = await import("./src/app-server/runtime-artifact.js"); return validateCodexAppServerRuntimeArtifact(binding); }, }, fetchUsageSnapshot: async (ctx) => { const { fetchCodexAppServerUsageSnapshot } = await import("./src/app-server/usage.js"); return await fetchCodexAppServerUsageSnapshot(ctx, { pluginConfig: options?.resolvePluginConfig?.() ?? options?.pluginConfig, }); }, loadMcpToolCatalog: async (params) => { const { loadCodexEffectiveMcpCatalog } = await import("./src/app-server/effective-mcp-catalog.js"); return await loadCodexEffectiveMcpCatalog(params, { bindingStore: options.bindingStore }); }, supports: (ctx) => { const provider = ctx.provider.trim().toLowerCase(); if (!providerIds.has(provider)) { return { supported: false, reason: `provider is not one of: ${[...providerIds].toSorted().join(", ")}`, }; } if (ctx.modelProvider?.requestTransportOverrides === "present") { return { supported: false, reason: "Codex cannot reproduce authored request transport overrides", }; } const preparedAuth = ctx.modelProvider?.preparedAuth; const runtimePolicy = ctx.modelProvider?.runtimePolicy; if (runtimePolicy) { const compatible = runtimePolicy.compatibleIds.some( (id) => id.trim().toLowerCase() === normalizedHarnessRuntimeId, ); if (!compatible) { return { supported: false, reason: "Codex cannot reproduce the prepared provider route", }; } } else if (ctx.modelProvider && provider !== "codex") { return { supported: false, reason: "provider route compatibility with Codex is not declared", }; } if (preparedAuth?.requirement === "subscription") { const reproducibleSubscription = preparedAuth.source === "profile" && (preparedAuth.mode === "oauth" || preparedAuth.mode === "token"); if (!reproducibleSubscription) { return { supported: false, reason: "Codex subscription auth requires a prepared OAuth or token profile", }; } } else if (preparedAuth?.requirement === "api-key") { const reproducibleApiKey = preparedAuth.source !== "none" && preparedAuth.source !== "harness" && (preparedAuth.mode === "api-key" || preparedAuth.mode === "api_key"); if (!reproducibleApiKey) { return { supported: false, reason: "Codex Platform auth requires a prepared API key", }; } } return { supported: true, priority: 100 }; }, runAttempt: async (params) => { // Keep app-server runtime code behind lazy imports so plugin discovery and // cold provider catalog reads do not pull in the whole Codex runtime. const { runCodexAppServerAttempt } = await import("./src/app-server/run-attempt.js"); return runCodexAppServerAttempt(params, { bindingStore: options.bindingStore, pluginConfig: options?.resolvePluginConfig?.() ?? options?.pluginConfig, nativeHookRelay: { enabled: true }, }); }, runIsolatedCompletion: async (params) => { // Codex app-server always exposes update_plan. Pure inference therefore // uses the already-prepared OpenAI/ChatGPT transport and credential // directly, without entering a Codex thread or re-resolving the route. const timeoutSignal = AbortSignal.timeout(params.timeoutMs); const signal = params.abortSignal ? AbortSignal.any([params.abortSignal, timeoutSignal]) : timeoutSignal; const assistant = await completeWithPreparedSimpleCompletionModel({ model: params.model, auth: params.auth, cfg: params.config, context: { systemPrompt: params.systemPrompt, messages: [{ role: "user", content: params.prompt, timestamp: Date.now() }], tools: [], }, options: { maxTokens: params.streamParams?.maxTokens, temperature: params.streamParams?.temperature, reasoning: params.thinkLevel, signal, }, }); return { assistant }; }, finalizeSettledTurn: async (params) => { const { runCodexSettledTurnFinalization } = await import("./src/app-server/settled-turn-finalizer.js"); return runCodexSettledTurnFinalization(params, { pluginConfig: options?.resolvePluginConfig?.() ?? options?.pluginConfig, }); }, runSideQuestion: async (params) => { const { runCodexAppServerSideQuestion } = await import("./src/app-server/side-question.js"); return runCodexAppServerSideQuestion(params, { bindingStore: options.bindingStore, pluginConfig: options?.resolvePluginConfig?.() ?? options?.pluginConfig, nativeHookRelay: { enabled: true }, }); }, compact: async (params) => { const { maybeCompactCodexAppServerSession } = await import("./src/app-server/compact.js"); return maybeCompactCodexAppServerSession(params, { bindingStore: options.bindingStore, pluginConfig: options?.resolvePluginConfig?.() ?? options?.pluginConfig, }); }, compactAfterContextEngine: async (params) => { const { maybeCompactCodexAppServerSession } = await import("./src/app-server/compact.js"); return maybeCompactCodexAppServerSession(params, { bindingStore: options.bindingStore, pluginConfig: options?.resolvePluginConfig?.() ?? options?.pluginConfig, allowNonManualNativeRequest: true, }); }, reset: async (params) => { if (params.sessionId) { const { reclaimCurrentCodexSessionGeneration, sessionBindingIdentity } = await import("./src/app-server/session-binding.js"); const identity = sessionBindingIdentity({ agentId: params.agentId, sessionId: params.sessionId, sessionKey: params.sessionKey, }); const resetGeneration = params.reason === "deleted" ? options.bindingStore.retireSessionGeneration.bind(options.bindingStore) : options.bindingStore.resetSessionGeneration.bind(options.bindingStore); let reset = await resetGeneration(identity); if (reset === "conflict") { const reclaimed = await reclaimCurrentCodexSessionGeneration({ bindingStore: options.bindingStore, identity, config: options.resolveConfig?.(), }); if (reclaimed) { reset = await resetGeneration(identity); } } if (reset === "conflict") { throw new Error( `Codex binding generation changed before session ${params.sessionId} could reset`, ); } } }, dispose: disposeSharedCodexAppServerClients, }; return harness; }