openclaw / src /agents /cli-runner /bundle-mcp-codex.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
e249c6d verified
Raw History Blame Contribute Delete
12.5 kB
/**
* 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<SessionToolOverrides, "mcpServers" | "mcpToolsDeny">;
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<string, unknown> {
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<CodexUserMcpServersProjectionOptions, "agentId" | "toolOverrides">,
): Pick<SessionToolOverrides, "mcpServers" | "mcpToolsDeny"> | 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<EmbeddedRunAttemptParams, "admittedRunContext">;
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,
});
}