/** * Codex CLI and app-server bundle MCP projection helpers. */ import { normalizeConfiguredMcpServers } from "../../config/mcp-config-normalize.js"; import type { SessionToolOverrides } from "../../config/sessions/types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { loadMcpToolGrants } from "../../infra/exec-approvals-mcp.js"; import type { BundleMcpConfig, BundleMcpServerConfig } from "../../plugins/bundle-mcp.js"; import { isValidAgentId, normalizeAgentId } from "../../routing/session-key.js"; import { acquireSessionMcpRuntime, releaseSessionMcpRuntime, } from "../agent-bundle-mcp-manager-api.js"; import type { PreparedNativeMcpPolicy } from "../agent-bundle-mcp-types.js"; import { resolveSessionAgentId } from "../agent-scope.js"; import { isRecord } from "../bundle-mcp-adapter.js"; import { applyCodexSessionMcpToolDenials, buildCodexMcpServersConfig, normalizeCodexMcpServerConfig, } from "../codex-mcp-config.js"; import { resolveConversationCapabilityProfile } from "../conversation-capability-profile.js"; import type { EmbeddedRunAttemptParams } from "../embedded-agent-runner/run/types.js"; import { requiresMcpBearerProjection, resolveMcpBearerBundleConfig } from "../mcp-auth-profile.js"; import { partitionMcpServersByConnectionScope } from "../mcp-connection-resolver.js"; import { applyPreparedNativeMcpPolicy, prepareNativeMcpPolicy } from "../native-mcp-policy.js"; import { resolveSandboxRuntimeStatus } from "../sandbox/runtime-status.js"; import { serializeTomlInlineValue } from "./toml-inline.js"; // Mutable JSON shape structurally compatible with the bundled Codex // app-server thread-config JsonObject (see the protocol module in the codex // plugin). Defined locally so this projection result stays assignable to // mergeCodexThreadConfigs without pulling plugin-local types across the // extensions boundary. type CodexThreadConfigValue = | string | number | boolean | null | CodexThreadConfigValue[] | { [key: string]: CodexThreadConfigValue }; type CodexThreadConfigObject = { [key: string]: CodexThreadConfigValue }; type CodexUserMcpServersProjectionOptions = { preparationOnly?: true; agentId?: string; agentDir?: string; allowLiteralOAuthProjection?: boolean; onServerUnavailable?: (serverName: string, error: unknown) => void; toolOverrides?: Pick; preparedNativeMcpPolicy?: PreparedNativeMcpPolicy; }; function normalizeAgentIds(value: unknown): string[] { if (!Array.isArray(value)) { return []; } return value .filter((entry): entry is string => typeof entry === "string") .map((entry) => entry.trim()) .filter((entry) => isValidAgentId(entry)) .map((entry) => normalizeAgentId(entry)); } function readCodexProjectionConfig(server: BundleMcpServerConfig): Record { return isRecord(server.codex) ? server.codex : {}; } function isCodexMcpServerAllowedForAgent( server: BundleMcpServerConfig, options: CodexUserMcpServersProjectionOptions | undefined, ): boolean { const codex = readCodexProjectionConfig(server); if (!Object.hasOwn(codex, "agents")) { return true; } const agentIds = normalizeAgentIds(codex.agents); if (agentIds.length === 0 || !options?.agentId) { return false; } return agentIds.includes(normalizeAgentId(options.agentId)); } /** * Applies Codex-only agent scoping before OpenClaw resolves credentials or opens transports. * Session overrides may narrow this result, but cannot widen `codex.agents`. */ export function resolveCodexMcpToolOverridesForAgent( cfg: OpenClawConfig | undefined, options: Pick, ): Pick | undefined { const deniedServerNames = Object.entries(normalizeConfiguredMcpServers(cfg?.mcp?.servers)) .filter(([, server]) => !isCodexMcpServerAllowedForAgent(server, options)) .map(([name]) => name); if (deniedServerNames.length === 0) { return options.toolOverrides; } const mcpServers = { ...options.toolOverrides?.mcpServers }; for (const serverName of deniedServerNames) { mcpServers[serverName] = false; } return { ...options.toolOverrides, mcpServers }; } function readSessionMcpServerOverride( options: CodexUserMcpServersProjectionOptions | undefined, name: string, ): boolean | undefined { const overrides = options?.toolOverrides?.mcpServers; return overrides && Object.hasOwn(overrides, name) ? overrides[name] : undefined; } function selectCodexProjectableMcpServers( cfg: OpenClawConfig | undefined, options: CodexUserMcpServersProjectionOptions | undefined, ): BundleMcpConfig["mcpServers"] { const userServers = normalizeConfiguredMcpServers(cfg?.mcp?.servers); // Fail-closed: requester-scoped servers never enter harness-native MCP config. const { staticServers } = partitionMcpServersByConnectionScope(userServers); return Object.fromEntries( Object.entries(staticServers).filter(([serverName, server]) => { const serverOverride = readSessionMcpServerOverride(options, serverName); const allowed = serverOverride !== false && (serverOverride === true || server.enabled !== false) && isCodexMcpServerAllowedForAgent(server as BundleMcpServerConfig, options); if (!allowed) { return false; } // Remote app servers cannot receive OpenClaw-managed bearer credentials. // Omit these servers before catalog discovery can use that credential. if (options?.allowLiteralOAuthProjection === false && requiresMcpBearerProjection(server)) { options.onServerUnavailable?.( serverName, new Error( `MCP OAuth bearer projection is only supported for local app-server connections.`, ), ); return false; } return true; }), ) as BundleMcpConfig["mcpServers"]; } /** Returns Codex CLI args with TOML MCP server overrides injected. */ export function injectCodexMcpConfigArgs( args: string[] | undefined, config: BundleMcpConfig, ): string[] { const overrides = serializeTomlInlineValue(buildCodexMcpServersConfig(config)); return [...(args ?? []), "-c", `mcp_servers=${overrides}`]; } /** Async runtime projection that resolves OpenClaw-managed MCP bearer tokens. */ export async function buildCodexUserMcpServersThreadConfigPatchForRuntime( cfg: OpenClawConfig | undefined, options?: CodexUserMcpServersProjectionOptions, ): Promise<{ mcp_servers: CodexThreadConfigObject } | undefined> { let allowedServers = selectCodexProjectableMcpServers(cfg, options); if (options?.preparationOnly && Object.values(allowedServers).some(requiresMcpBearerProjection)) { throw new Error( "Native fork preparation cannot resolve MCP bearer credentials. Fork an original imported message instead.", ); } if (options?.preparedNativeMcpPolicy) { allowedServers = applyPreparedNativeMcpPolicy( { mcpServers: allowedServers }, options.preparedNativeMcpPolicy, ).mcpServers; } if (Object.keys(allowedServers).length === 0) { return undefined; } const grants = options?.agentId ? await loadMcpToolGrants(options.agentId) : []; const resolvedConfig = await resolveMcpBearerBundleConfig({ config: { mcpServers: allowedServers }, cfg, agentDir: options?.agentDir, tokenProjection: "literal", omitUnavailableOAuthServers: true, onServerUnavailable: options?.onServerUnavailable, }); const mcp_servers: CodexThreadConfigObject = Object.fromEntries( Object.entries(resolvedConfig.config.mcpServers).map(([name, server]) => [ name, normalizeCodexMcpServerConfig( name, applyCodexSessionMcpToolDenials(name, server, options?.toolOverrides), grants, ) as CodexThreadConfigObject, ]), ); return Object.keys(mcp_servers).length === 0 ? undefined : { mcp_servers }; } /** Prepares canonical native MCP policy and projects it into Codex before thread creation. */ export async function buildCodexUserMcpServersThreadConfigPatchForRun(params: { run: Omit; cwd: string; agentId?: string; allowLiteralOAuthProjection?: boolean; onServerUnavailable?: (serverName: string, error: unknown) => void; warn?: (message: string) => void; }): Promise<{ mcp_servers: CodexThreadConfigObject } | undefined> { const run = params.run; const agentId = params.agentId ?? run.agentId; const scopedToolOverrides = resolveCodexMcpToolOverridesForAgent(run.config, { agentId, toolOverrides: run.toolOverrides, }); const policySessionKey = run.sandboxSessionKey ?? run.sessionKey; const policyAgentId = resolveSessionAgentId({ config: run.config, sessionKey: policySessionKey, agentId: run.sandboxAgentId, fallbackAgentId: agentId, }); const sandboxStatus = resolveSandboxRuntimeStatus({ cfg: run.config, sessionKey: policySessionKey, agentId: policyAgentId, }); const capabilityProfile = resolveConversationCapabilityProfile({ config: run.config, sessionKey: policySessionKey, runSessionKey: run.sessionKey && run.sessionKey !== policySessionKey ? run.sessionKey : undefined, sessionId: run.sessionId, runId: run.runId, agentId: policyAgentId, agentDir: run.agentDir, agentAccountId: run.agentAccountId, messageProvider: run.messageProvider ?? run.messageChannel, messageChannel: run.messageChannel, chatType: run.chatType, messageTo: run.messageTo, messageThreadId: run.messageThreadId, currentChannelId: run.currentChannelId, currentMessagingTarget: run.currentMessagingTarget, currentThreadTs: run.currentThreadTs, currentMessageId: run.currentMessageId, groupId: run.groupId, groupChannel: run.groupChannel, groupSpace: run.groupSpace, memberRoleIds: run.memberRoleIds, spawnedBy: run.spawnedBy, senderId: run.senderId, senderName: run.senderName, senderUsername: run.senderUsername, senderE164: run.senderE164, senderIsOwner: run.senderIsOwner, modelProvider: run.provider, modelId: run.modelId, modelApi: run.model?.api, modelContextWindowTokens: run.model?.contextWindow, modelHasVision: run.model?.input?.includes("image") ?? false, workspaceDir: run.workspaceDir, cwd: params.cwd, skillsSnapshot: run.skillsSnapshot, sandboxToolPolicy: sandboxStatus.sandboxed ? sandboxStatus.toolPolicy : undefined, runtimeToolAllowlist: run.toolsAllow, inheritRuntimeToolAllowlist: true, runtimePluginToolGrant: run.runtimePluginToolGrant, inputProvenance: run.inputProvenance, trustedInternalHandoff: run.trustedInternalHandoff, scheduledToolPolicy: run.scheduledToolPolicy, }); const configuredMcpServers = selectCodexProjectableMcpServers(run.config, { agentId, allowLiteralOAuthProjection: params.allowLiteralOAuthProjection, onServerUnavailable: params.onServerUnavailable, toolOverrides: scopedToolOverrides, }); if (Object.keys(configuredMcpServers).length === 0) { return undefined; } const projectionConfig: OpenClawConfig = { ...run.config, mcp: { ...run.config?.mcp, servers: configuredMcpServers }, }; const acquisition = await acquireSessionMcpRuntime({ sessionId: run.sessionId, sessionKey: run.sessionKey, workspaceDir: run.workspaceDir, agentDir: run.agentDir, cfg: projectionConfig, requesterSenderId: run.senderId, agentAccountId: run.agentAccountId, messageChannel: run.messageChannel, toolOverrides: scopedToolOverrides, }); let preparedNativeMcpPolicy: PreparedNativeMcpPolicy; try { preparedNativeMcpPolicy = await prepareNativeMcpPolicy({ runtime: acquisition.runtime, config: run.config, workspaceDir: run.workspaceDir, capabilityProfile, runtimeToolsAllow: run.toolsAllow, warn: params.warn ?? (() => {}), }); } finally { await releaseSessionMcpRuntime(acquisition); } return await buildCodexUserMcpServersThreadConfigPatchForRuntime(projectionConfig, { agentId, agentDir: run.agentDir, allowLiteralOAuthProjection: params.allowLiteralOAuthProjection, onServerUnavailable: params.onServerUnavailable, toolOverrides: scopedToolOverrides, preparedNativeMcpPolicy, }); }