/** * Codex app-server agent harness registration and lazy runtime boundaries. */ import type { AgentHarnessV2, 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"); // Audited against @openai/codex 0.147.0 (rust-v0.147.0). These exact denies // target OpenClaw-owned capabilities with no Codex-native equivalent. Keep the // list positive and conservative: an omitted tool isolates the native surface. const CODEX_TOOL_POLICY_SAFE_DENY_NAMES = [ "web_fetch", "x_search", "memory_search", "memory_get", "dashboard", "canvas", "show_widget", "message", "heartbeat_respond", "automations", "gateway", "skill_workshop", "music_generate", "video_generate", "tts", ] as const; 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 = AgentHarnessV2 & { compactAfterContextEngine?( params: AgentHarnessCompactParams, ): Promise; }; type CodexHostPreparedIsolatedCompletionParams = Parameters< NonNullable >[0]; async function runCodexHostPreparedIsolatedCompletion( params: CodexHostPreparedIsolatedCompletionParams, ) { 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 }; } 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; }): AgentHarnessV2 { 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, conversationToolPolicySupport: "exact", conversationToolPolicySafeDenyTools: CODEX_TOOL_POLICY_SAFE_DENY_NAMES, 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 }, }); }, runIsolatedCompletionV2: async (params) => { if (params.authorization.owner === "host") { const { authorization, ...commonParams } = params; return runCodexHostPreparedIsolatedCompletion({ ...commonParams, model: authorization.model, auth: authorization.auth, ...(authorization.sourceAuthFingerprint ? { sourceAuthFingerprint: authorization.sourceAuthFingerprint } : {}), }); } const { runCodexIsolatedCompletion } = await import("./src/app-server/isolated-completion.js"); return runCodexIsolatedCompletion(params, { pluginConfig: options?.resolvePluginConfig?.() ?? options?.pluginConfig, }); }, runIsolatedCompletion: async (params) => { // Keep the deprecated V1 contract on its exact host-prepared transport. // V2 owns native Codex auth and zero-tool attestation above. return runCodexHostPreparedIsolatedCompletion(params); }, 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 }, { retireCodexAppServerSessionGeneration }, ] = await Promise.all([ import("./src/app-server/session-binding.js"), import("./src/app-server/session-retirement.js"), ]); const identity = sessionBindingIdentity({ agentId: params.agentId, sessionId: params.sessionId, sessionKey: params.sessionKey, }); const resetGeneration = () => retireCodexAppServerSessionGeneration({ bindingStore: options.bindingStore, identity, mode: params.reason === "deleted" ? "retire" : "reset", }); let reset = await resetGeneration(); if (reset === "conflict") { const reclaimed = await reclaimCurrentCodexSessionGeneration({ bindingStore: options.bindingStore, identity, config: options.resolveConfig?.(), }); if (reclaimed) { reset = await resetGeneration(); } } if (reset === "conflict") { throw new Error( `Codex binding generation changed before session ${params.sessionId} could reset`, ); } } }, dispose: disposeSharedCodexAppServerClients, }; return harness; }