fix(codex): preserve MCP servers in app-server harness (#81551)

* Plumb bundle MCP config into Codex app server

* fix: align codex mcp thread config with pi

* fix: rotate codex mcp threads when disabled

* fix: scope codex bundle mcp to bundled servers

* fix(codex): resend user MCP config on resume

---------

Co-authored-by: Josh Lehman <phaedrus@Mac.hsd1.ca.comcast.net>
This commit is contained in:
Josh Lehman
2026-05-13 21:05:20 -07:00
committed by GitHub
co-authored by Josh Lehman
parent 4935e24c7a
commit 1ee0d51e92
10 changed files with 490 additions and 12 deletions
@@ -651,6 +651,109 @@ describe("runCodexAppServerAttempt", () => {
}
});
it("passes MCP server config through to Codex thread/start", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const request = vi.fn(async (method: string, _params: unknown) => {
if (method === "thread/start") {
return threadStartResult();
}
throw new Error(`unexpected method: ${method}`);
});
await startOrResumeThread({
client: { request } as never,
params: createParams(sessionFile, workspaceDir),
cwd: workspaceDir,
dynamicTools: [],
appServer: createThreadLifecycleAppServerOptions(),
config: {
mcp_servers: {
search: {
url: "https://mcp.example.com/mcp",
},
},
},
mcpServersFingerprint: "mcp-v1",
mcpServersFingerprintEvaluated: true,
});
const startRequest = request.mock.calls.find(([method]) => method === "thread/start");
expect((startRequest?.[1] as { config?: unknown } | undefined)?.config).toMatchObject({
mcp_servers: {
search: {
url: "https://mcp.example.com/mcp",
},
},
"features.code_mode": true,
"features.code_mode_only": true,
});
const binding = await readCodexAppServerBinding(sessionFile);
expect(binding?.mcpServersFingerprint).toBe("mcp-v1");
});
it("starts a new Codex thread when the MCP server fingerprint changes", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
await writeCodexAppServerBinding(sessionFile, {
threadId: "old-thread",
cwd: workspaceDir,
dynamicToolsFingerprint: JSON.stringify([]),
mcpServersFingerprint: "mcp-v1",
});
const request = vi.fn(async (method: string, _params: unknown) => {
if (method === "thread/start") {
return threadStartResult("new-thread");
}
throw new Error(`unexpected method: ${method}`);
});
const binding = await startOrResumeThread({
client: { request } as never,
params: createParams(sessionFile, workspaceDir),
cwd: workspaceDir,
dynamicTools: [],
appServer: createThreadLifecycleAppServerOptions(),
mcpServersFingerprint: "mcp-v2",
mcpServersFingerprintEvaluated: true,
});
expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start"]);
expect(binding.threadId).toBe("new-thread");
expect(binding.mcpServersFingerprint).toBe("mcp-v2");
});
it("starts a no-MCP Codex thread when MCP config is evaluated empty", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
await writeCodexAppServerBinding(sessionFile, {
threadId: "old-thread",
cwd: workspaceDir,
dynamicToolsFingerprint: JSON.stringify([]),
mcpServersFingerprint: "mcp-v1",
});
const request = vi.fn(async (method: string, _params: unknown) => {
if (method === "thread/start") {
return threadStartResult("new-thread");
}
throw new Error(`unexpected method: ${method}`);
});
const binding = await startOrResumeThread({
client: { request } as never,
params: createParams(sessionFile, workspaceDir),
cwd: workspaceDir,
dynamicTools: [],
appServer: createThreadLifecycleAppServerOptions(),
mcpServersFingerprintEvaluated: true,
});
expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start"]);
expect(binding.threadId).toBe("new-thread");
expect(binding.mcpServersFingerprint).toBeUndefined();
expect((await readCodexAppServerBinding(sessionFile))?.mcpServersFingerprint).toBeUndefined();
});
it("does not expose OpenClaw Tool Search controls through Codex dynamic tools", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
+18 -1
View File
@@ -15,6 +15,7 @@ import {
formatErrorMessage,
isActiveHarnessContextEngine,
isSubagentSessionKey,
loadCodexBundleMcpThreadConfig,
normalizeAgentRuntimeTools,
resolveAttemptSpawnWorkspaceDir,
resolveAgentHarnessBeforePromptBuildResult,
@@ -87,6 +88,7 @@ import { buildCodexPluginAppCacheKey } from "./plugin-app-cache-key.js";
import {
buildCodexPluginThreadConfig,
buildCodexPluginThreadConfigInputFingerprint,
mergeCodexThreadConfigs,
shouldBuildCodexPluginThreadConfig,
} from "./plugin-thread-config.js";
import {
@@ -517,6 +519,16 @@ export async function runCodexAppServerAttempt(
: resolveCodexAppServerEnvApiKeyCacheKey({
startOptions: appServer.start,
});
const bundleMcpThreadConfig = await loadCodexBundleMcpThreadConfig({
workspaceDir: effectiveWorkspace,
cfg: params.config,
toolsEnabled: supportsModelTools(params.model),
disableTools: params.disableTools,
toolsAllow: params.toolsAllow,
});
for (const diagnostic of bundleMcpThreadConfig.diagnostics) {
embeddedAgentLog.warn(`bundle-mcp: ${diagnostic.pluginId}: ${diagnostic.message}`);
}
const activeContextEngine = isActiveHarnessContextEngine(params.contextEngine)
? params.contextEngine
: undefined;
@@ -713,7 +725,10 @@ export async function runCodexAppServerAttempt(
: options.nativeHookRelay?.enabled === false
? buildCodexNativeHookRelayDisabledConfig()
: undefined;
const threadConfig = nativeHookRelayConfig;
const threadConfig = mergeCodexThreadConfigs(
nativeHookRelayConfig,
bundleMcpThreadConfig?.configPatch as JsonObject | undefined,
);
const pluginThreadConfigEnabled = shouldBuildCodexPluginThreadConfig(pluginConfig);
const pluginAppCacheKey = buildCodexPluginAppCacheKey({
appServer,
@@ -772,6 +787,8 @@ export async function runCodexAppServerAttempt(
appServer: pluginAppServer,
developerInstructions: promptBuild.developerInstructions,
config: threadConfig,
mcpServersFingerprint: bundleMcpThreadConfig.fingerprint,
mcpServersFingerprintEvaluated: bundleMcpThreadConfig.evaluated,
pluginThreadConfig: pluginThreadConfigEnabled
? {
enabled: true,
@@ -41,6 +41,7 @@ export type CodexAppServerThreadBinding = {
serviceTier?: CodexServiceTier;
dynamicToolsFingerprint?: string;
userMcpServersFingerprint?: string;
mcpServersFingerprint?: string;
pluginAppsFingerprint?: string;
pluginAppsInputFingerprint?: string;
pluginAppPolicyContext?: PluginAppPolicyContext;
@@ -104,6 +105,8 @@ export async function readCodexAppServerBinding(
typeof parsed.userMcpServersFingerprint === "string"
? parsed.userMcpServersFingerprint
: undefined,
mcpServersFingerprint:
typeof parsed.mcpServersFingerprint === "string" ? parsed.mcpServersFingerprint : undefined,
pluginAppsFingerprint:
typeof parsed.pluginAppsFingerprint === "string" ? parsed.pluginAppsFingerprint : undefined,
pluginAppsInputFingerprint:
@@ -149,6 +152,7 @@ export async function writeCodexAppServerBinding(
serviceTier: binding.serviceTier,
dynamicToolsFingerprint: binding.dynamicToolsFingerprint,
userMcpServersFingerprint: binding.userMcpServersFingerprint,
mcpServersFingerprint: binding.mcpServersFingerprint,
pluginAppsFingerprint: binding.pluginAppsFingerprint,
pluginAppsInputFingerprint: binding.pluginAppsInputFingerprint,
pluginAppPolicyContext: binding.pluginAppPolicyContext,
@@ -73,6 +73,8 @@ export async function startOrResumeThread(params: {
appServer: CodexAppServerRuntimeOptions;
developerInstructions?: string;
config?: JsonObject;
mcpServersFingerprint?: string;
mcpServersFingerprintEvaluated?: boolean;
pluginThreadConfig?: CodexPluginThreadConfigProvider;
}): Promise<CodexAppServerThreadLifecycleBinding> {
const dynamicToolsFingerprint = fingerprintDynamicTools(params.dynamicTools);
@@ -112,6 +114,17 @@ export async function startOrResumeThread(params: {
await clearCodexAppServerBinding(params.params.sessionFile);
binding = undefined;
}
if (
binding?.threadId &&
params.mcpServersFingerprintEvaluated === true &&
binding.mcpServersFingerprint !== params.mcpServersFingerprint
) {
embeddedAgentLog.debug("codex app-server MCP config changed; starting a new thread", {
threadId: binding.threadId,
});
await clearCodexAppServerBinding(params.params.sessionFile);
binding = undefined;
}
if (binding?.threadId) {
let pluginBindingStale = isCodexPluginThreadBindingStale({
codexPluginsEnabled: params.pluginThreadConfig?.enabled ?? false,
@@ -146,6 +159,17 @@ export async function startOrResumeThread(params: {
binding = undefined;
}
}
if (
binding?.threadId &&
params.mcpServersFingerprintEvaluated === true &&
binding.mcpServersFingerprint !== params.mcpServersFingerprint
) {
embeddedAgentLog.debug("codex app-server MCP config changed; starting a new thread", {
threadId: binding.threadId,
});
await clearCodexAppServerBinding(params.params.sessionFile);
binding = undefined;
}
if (binding?.threadId) {
// `/codex resume <thread>` writes a binding before the next turn can know
// the dynamic tool catalog, so only invalidate fingerprints we actually have.
@@ -179,6 +203,7 @@ export async function startOrResumeThread(params: {
} else {
try {
const authProfileId = params.params.authProfileId ?? binding.authProfileId;
const resumeConfig = mergeCodexThreadConfigs(params.config, userMcpServersConfigPatch);
const response = assertCodexThreadResumeResponse(
await params.client.request(
"thread/resume",
@@ -187,7 +212,7 @@ export async function startOrResumeThread(params: {
authProfileId,
appServer: params.appServer,
developerInstructions: params.developerInstructions,
config: params.config,
config: resumeConfig,
}),
),
);
@@ -199,6 +224,10 @@ export async function startOrResumeThread(params: {
agentDir: params.params.agentDir,
config: params.params.config,
});
const nextMcpServersFingerprint =
params.mcpServersFingerprintEvaluated === true
? params.mcpServersFingerprint
: binding.mcpServersFingerprint;
await writeCodexAppServerBinding(
params.params.sessionFile,
{
@@ -209,6 +238,7 @@ export async function startOrResumeThread(params: {
modelProvider: response.modelProvider ?? fallbackModelProvider,
dynamicToolsFingerprint,
userMcpServersFingerprint,
mcpServersFingerprint: nextMcpServersFingerprint,
pluginAppsFingerprint: binding.pluginAppsFingerprint,
pluginAppsInputFingerprint: binding.pluginAppsInputFingerprint,
pluginAppPolicyContext: binding.pluginAppPolicyContext,
@@ -230,6 +260,7 @@ export async function startOrResumeThread(params: {
modelProvider: response.modelProvider ?? fallbackModelProvider,
dynamicToolsFingerprint,
userMcpServersFingerprint,
mcpServersFingerprint: nextMcpServersFingerprint,
pluginAppsFingerprint: binding.pluginAppsFingerprint,
pluginAppsInputFingerprint: binding.pluginAppsInputFingerprint,
pluginAppPolicyContext: binding.pluginAppPolicyContext,
@@ -276,6 +307,8 @@ export async function startOrResumeThread(params: {
config: params.params.config,
});
const createdAt = new Date().toISOString();
const nextMcpServersFingerprint =
params.mcpServersFingerprintEvaluated === true ? params.mcpServersFingerprint : undefined;
if (!preserveExistingBinding) {
await writeCodexAppServerBinding(
params.params.sessionFile,
@@ -287,6 +320,7 @@ export async function startOrResumeThread(params: {
modelProvider: response.modelProvider ?? modelProvider,
dynamicToolsFingerprint,
userMcpServersFingerprint,
mcpServersFingerprint: nextMcpServersFingerprint,
pluginAppsFingerprint: pluginThreadConfig?.fingerprint,
pluginAppsInputFingerprint: pluginThreadConfig?.inputFingerprint,
pluginAppPolicyContext: pluginThreadConfig?.policyContext,
@@ -310,6 +344,7 @@ export async function startOrResumeThread(params: {
modelProvider: response.modelProvider ?? modelProvider,
dynamicToolsFingerprint,
userMcpServersFingerprint,
mcpServersFingerprint: nextMcpServersFingerprint,
pluginAppsFingerprint: pluginThreadConfig?.fingerprint,
pluginAppsInputFingerprint: pluginThreadConfig?.inputFingerprint,
pluginAppPolicyContext: pluginThreadConfig?.policyContext,
@@ -205,7 +205,7 @@ describe("startOrResumeThread — user mcp.servers projection (regression: #8081
});
});
it("resumes a thread with the matching user MCP fingerprint without resending ignored MCP config", async () => {
it("resends user MCP config when resuming a thread with the matching fingerprint", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
const config = {
@@ -252,6 +252,8 @@ describe("startOrResumeThread — user mcp.servers projection (regression: #8081
config?: { mcp_servers?: Record<string, unknown> };
};
expect(resumeCall).toBeDefined();
expect(resumeParams?.config?.mcp_servers).toBeUndefined();
expect(resumeParams?.config?.mcp_servers).toMatchObject({
notes: { command: "node", args: ["/opt/notes-mcp/dist/index.js"] },
});
});
});
+2 -8
View File
@@ -7,6 +7,7 @@ import {
decodeHeaderEnvPlaceholder,
normalizeStringRecord,
} from "./bundle-mcp-adapter-shared.js";
import { buildCodexMcpServersConfig } from "../codex-mcp-config.js";
import { serializeTomlInlineValue } from "./toml-inline.js";
// Mutable JSON shape structurally compatible with the bundled Codex
@@ -70,14 +71,7 @@ export function injectCodexMcpConfigArgs(
args: string[] | undefined,
config: BundleMcpConfig,
): string[] {
const overrides = serializeTomlInlineValue(
Object.fromEntries(
Object.entries(config.mcpServers).map(([name, server]) => [
name,
normalizeCodexServerConfig(name, server),
]),
),
);
const overrides = serializeTomlInlineValue(buildCodexMcpServersConfig(config));
return [...(args ?? []), "-c", `mcp_servers=${overrides}`];
}
+155
View File
@@ -0,0 +1,155 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { buildCodexMcpServersConfig, loadCodexBundleMcpThreadConfig } from "./codex-mcp-config.js";
const mocks = vi.hoisted(() => ({
bundleMcp: {
config: {
mcpServers: {},
},
diagnostics: [],
},
}));
vi.mock("../plugins/bundle-mcp.js", () => ({
loadEnabledBundleMcpConfig: () => mocks.bundleMcp,
}));
beforeEach(() => {
mocks.bundleMcp = {
config: {
mcpServers: {},
},
diagnostics: [],
};
});
describe("buildCodexMcpServersConfig", () => {
it("normalizes OpenClaw MCP servers into Codex app-server mcp_servers shape", () => {
expect(
buildCodexMcpServersConfig({
mcpServers: {
openclaw: {
type: "http",
url: "http://127.0.0.1:23119/mcp",
headers: {
Authorization: "Bearer ${OPENCLAW_MCP_TOKEN}",
"x-session-key": "${OPENCLAW_MCP_SESSION_KEY}",
"x-static": "static-value",
},
},
},
}),
).toEqual({
openclaw: {
url: "http://127.0.0.1:23119/mcp",
default_tools_approval_mode: "approve",
bearer_token_env_var: "OPENCLAW_MCP_TOKEN",
http_headers: {
"x-static": "static-value",
},
env_http_headers: {
"x-session-key": "OPENCLAW_MCP_SESSION_KEY",
},
},
});
});
});
describe("loadCodexBundleMcpThreadConfig", () => {
it("loads enabled bundled MCP servers as a Codex thread config patch", () => {
mocks.bundleMcp = {
config: {
mcpServers: {
search: {
type: "http",
url: "https://mcp.example.com/mcp",
},
},
},
diagnostics: [],
};
const loaded = loadCodexBundleMcpThreadConfig({
workspaceDir: "/workspace",
cfg: {
plugins: {
entries: {
"bundle-probe": { enabled: true },
},
},
},
});
expect(loaded.configPatch).toEqual({
mcp_servers: {
search: {
url: "https://mcp.example.com/mcp",
},
},
});
expect(loaded.fingerprint).toMatch(/^[a-f0-9]{64}$/);
});
it("leaves user mcp.servers to the Codex user MCP projection path", () => {
const loaded = loadCodexBundleMcpThreadConfig({
workspaceDir: "/workspace",
cfg: {
mcp: {
servers: {
search: {
transport: "streamable-http",
url: "https://mcp.example.com/mcp",
},
},
},
},
toolsEnabled: true,
});
expect(loaded.configPatch).toBeUndefined();
expect(loaded.fingerprint).toBeUndefined();
expect(loaded.evaluated).toBe(true);
});
it("returns an evaluated empty MCP config when Pi would not create a bundle MCP runtime", () => {
const cfg = {
mcp: {
servers: {
search: {
transport: "streamable-http",
url: "https://mcp.example.com/mcp",
},
},
},
} as const;
for (const params of [
{ toolsEnabled: false },
{ toolsEnabled: true, disableTools: true },
{ toolsEnabled: true, toolsAllow: [] },
{ toolsEnabled: true, toolsAllow: ["memory_search"] },
]) {
const loaded = loadCodexBundleMcpThreadConfig({
workspaceDir: "/workspace",
cfg,
...params,
});
expect(loaded.configPatch).toBeUndefined();
expect(loaded.fingerprint).toBeUndefined();
expect(loaded.evaluated).toBe(true);
}
});
it("omits the config patch when no MCP servers are configured", () => {
const loaded = loadCodexBundleMcpThreadConfig({
workspaceDir: "/workspace",
cfg: {},
toolsEnabled: true,
});
expect(loaded.configPatch).toBeUndefined();
expect(loaded.fingerprint).toBeUndefined();
expect(loaded.evaluated).toBe(true);
});
});
+132
View File
@@ -0,0 +1,132 @@
import crypto from "node:crypto";
import {
loadEnabledBundleMcpConfig,
type BundleMcpConfig,
type BundleMcpServerConfig,
} from "../plugins/bundle-mcp.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import {
applyCommonServerConfig,
decodeHeaderEnvPlaceholder,
normalizeStringRecord,
} from "./cli-runner/bundle-mcp-adapter-shared.js";
import type {
CodexBundleMcpThreadConfig,
CodexMcpServersConfig,
LoadCodexBundleMcpThreadConfigParams,
} from "./codex-mcp-config.types.js";
import { shouldCreateBundleMcpRuntimeForAttempt } from "./pi-embedded-runner/run/attempt-tool-construction-plan.js";
export type {
CodexBundleMcpThreadConfig,
CodexMcpServersConfig,
LoadCodexBundleMcpThreadConfigParams,
} from "./codex-mcp-config.types.js";
function isOpenClawLoopbackMcpServer(name: string, server: BundleMcpServerConfig): boolean {
return (
name === "openclaw" &&
typeof server.url === "string" &&
/^https?:\/\/(?:127\.0\.0\.1|localhost):\d+\/mcp(?:[?#].*)?$/.test(server.url)
);
}
export function normalizeCodexMcpServerConfig(
name: string,
server: BundleMcpServerConfig,
): Record<string, unknown> {
const next: Record<string, unknown> = {};
applyCommonServerConfig(next, server);
if (isOpenClawLoopbackMcpServer(name, server)) {
next.default_tools_approval_mode = "approve";
}
const httpHeaders = normalizeStringRecord(server.headers);
if (httpHeaders) {
const staticHeaders: Record<string, string> = {};
const envHeaders: Record<string, string> = {};
for (const [name, value] of Object.entries(httpHeaders)) {
const decoded = decodeHeaderEnvPlaceholder(value);
if (!decoded) {
staticHeaders[name] = value;
continue;
}
if (decoded.bearer && normalizeOptionalLowercaseString(name) === "authorization") {
next.bearer_token_env_var = decoded.envVar;
continue;
}
envHeaders[name] = decoded.envVar;
}
if (Object.keys(staticHeaders).length > 0) {
next.http_headers = staticHeaders;
}
if (Object.keys(envHeaders).length > 0) {
next.env_http_headers = envHeaders;
}
}
return next;
}
export function buildCodexMcpServersConfig(config: BundleMcpConfig): CodexMcpServersConfig {
return Object.fromEntries(
Object.entries(config.mcpServers).map(([name, server]) => [
name,
normalizeCodexMcpServerConfig(name, server),
]),
);
}
function stableJsonValue(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(stableJsonValue);
}
if (!value || typeof value !== "object") {
return value;
}
return Object.fromEntries(
Object.entries(value as Record<string, unknown>)
.toSorted(([left], [right]) => left.localeCompare(right))
.map(([key, child]) => [key, stableJsonValue(child)]),
);
}
function fingerprintCodexMcpServersConfig(config: CodexMcpServersConfig): string {
return crypto
.createHash("sha256")
.update(JSON.stringify(stableJsonValue(config)))
.digest("hex");
}
export function loadCodexBundleMcpThreadConfig(
params: LoadCodexBundleMcpThreadConfigParams,
): CodexBundleMcpThreadConfig {
const shouldCreateRuntime = shouldCreateBundleMcpRuntimeForAttempt({
toolsEnabled: params.toolsEnabled ?? true,
disableTools: params.disableTools,
toolsAllow: params.toolsAllow,
});
if (!shouldCreateRuntime) {
return {
diagnostics: [],
evaluated: true,
};
}
const bundleMcp = loadEnabledBundleMcpConfig({
workspaceDir: params.workspaceDir,
cfg: params.cfg,
});
const mcpServers = buildCodexMcpServersConfig(bundleMcp.config);
if (Object.keys(mcpServers).length === 0) {
return {
diagnostics: bundleMcp.diagnostics,
evaluated: true,
};
}
return {
configPatch: {
mcp_servers: mcpServers,
},
diagnostics: bundleMcp.diagnostics,
evaluated: true,
fingerprint: fingerprintCodexMcpServersConfig(mcpServers),
};
}
+21
View File
@@ -0,0 +1,21 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { BundleMcpDiagnostic } from "../plugins/bundle-mcp.js";
export type CodexMcpServersConfig = Record<string, Record<string, unknown>>;
export type CodexBundleMcpThreadConfig = {
configPatch?: {
mcp_servers: CodexMcpServersConfig;
};
diagnostics: BundleMcpDiagnostic[];
evaluated: boolean;
fingerprint?: string;
};
export type LoadCodexBundleMcpThreadConfigParams = {
workspaceDir: string;
cfg?: OpenClawConfig;
toolsEnabled?: boolean;
disableTools?: boolean;
toolsAllow?: string[];
};
+15
View File
@@ -2,6 +2,10 @@
// Keep heavyweight tool construction out of this module so harness imports can
// register quickly inside gateway startup and Docker e2e runs.
import type {
CodexBundleMcpThreadConfig,
LoadCodexBundleMcpThreadConfigParams,
} from "../agents/codex-mcp-config.types.js";
import type { EmbeddedRunAttemptResult } from "../agents/pi-embedded-runner/run/types.js";
import {
abortEmbeddedPiRun,
@@ -134,7 +138,18 @@ export {
logAgentRuntimeToolDiagnostics,
normalizeAgentRuntimeTools,
} from "../agents/runtime-plan/tools.js";
export type {
CodexBundleMcpThreadConfig,
LoadCodexBundleMcpThreadConfigParams,
} from "../agents/codex-mcp-config.types.js";
export { normalizeProviderToolSchemas } from "../agents/pi-embedded-runner/tool-schema-runtime.js";
export async function loadCodexBundleMcpThreadConfig(
params: LoadCodexBundleMcpThreadConfigParams,
): Promise<CodexBundleMcpThreadConfig> {
const { loadCodexBundleMcpThreadConfig: load } = await import("../agents/codex-mcp-config.js");
return load(params);
}
export { resolveSandboxContext } from "../agents/sandbox.js";
export { resolveBootstrapContextForRun } from "../agents/bootstrap-files.js";
export type { EmbeddedContextFile } from "../agents/pi-embedded-helpers/types.js";