mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(codex): preserve beta5 MCP bindings
This commit is contained in:
@@ -380,6 +380,8 @@ export async function startOrResumeThread(params: {
|
||||
: buildCodexUserMcpServersThreadConfigPatch(params.params.config, {
|
||||
agentId: params.agentId ?? params.params.agentId,
|
||||
});
|
||||
const legacyUserMcpServersFingerprint =
|
||||
legacyFingerprintUserMcpServersConfigPatch(userMcpServersConfigPatch);
|
||||
const userMcpServersFingerprint =
|
||||
fingerprintUserMcpServersConfigPatch(userMcpServersConfigPatch);
|
||||
const environmentSelectionFingerprint = fingerprintEnvironmentSelection(
|
||||
@@ -581,7 +583,14 @@ export async function startOrResumeThread(params: {
|
||||
rotatedContextEngineBinding = true;
|
||||
}
|
||||
}
|
||||
if (binding?.threadId && binding.userMcpServersFingerprint !== userMcpServersFingerprint) {
|
||||
if (
|
||||
binding?.threadId &&
|
||||
!areUserMcpServersFingerprintsCompatible({
|
||||
previous: binding.userMcpServersFingerprint,
|
||||
next: userMcpServersFingerprint,
|
||||
nextLegacy: legacyUserMcpServersFingerprint,
|
||||
})
|
||||
) {
|
||||
embeddedAgentLog.debug("codex app-server user MCP config changed; starting a new thread", {
|
||||
threadId: binding.threadId,
|
||||
});
|
||||
@@ -1691,6 +1700,12 @@ function legacyFingerprintDynamicTools(dynamicTools: CodexDynamicToolSpec[]): st
|
||||
);
|
||||
}
|
||||
|
||||
function legacyFingerprintUserMcpServersConfigPatch(
|
||||
configPatch: JsonObject | undefined,
|
||||
): string | undefined {
|
||||
return configPatch ? JSON.stringify(stabilizeJsonValue(configPatch)) : undefined;
|
||||
}
|
||||
|
||||
function fingerprintUserMcpServersConfigPatch(
|
||||
configPatch: JsonObject | undefined,
|
||||
): string | undefined {
|
||||
@@ -1770,6 +1785,21 @@ function areDynamicToolFingerprintsCompatible(
|
||||
return !previous || previous === next || previous === nextLegacy;
|
||||
}
|
||||
|
||||
function areUserMcpServersFingerprintsCompatible(params: {
|
||||
previous?: string;
|
||||
next?: string;
|
||||
nextLegacy?: string;
|
||||
}): boolean {
|
||||
// Beta 5 stored raw stabilized JSON, while doctor hashes those exact bytes.
|
||||
// A successful resume rewrites either legacy form to the current redacted hash.
|
||||
return (
|
||||
params.previous === params.next ||
|
||||
params.previous === params.nextLegacy ||
|
||||
(params.nextLegacy !== undefined &&
|
||||
params.previous === hashCodexAppServerBindingFingerprint(params.nextLegacy))
|
||||
);
|
||||
}
|
||||
|
||||
function shouldStartTransientNoToolThread(params: {
|
||||
previous: string | undefined;
|
||||
nextHasDynamicTools: boolean;
|
||||
|
||||
@@ -6,9 +6,11 @@ import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { CodexAppServerRuntimeOptions } from "./config.js";
|
||||
import {
|
||||
hashCodexAppServerBindingFingerprint,
|
||||
readCodexAppServerBinding,
|
||||
registerCodexTestSessionIdentity,
|
||||
resetCodexTestBindingStore,
|
||||
seedCodexTestBinding,
|
||||
testCodexAppServerBindingStore,
|
||||
writeCodexAppServerBinding,
|
||||
} from "./session-binding.test-helpers.js";
|
||||
@@ -194,6 +196,85 @@ describe("startOrResumeThread — user mcp.servers projection (regression: #8081
|
||||
expect(binding?.userMcpServersFingerprint).not.toContain("server_119");
|
||||
});
|
||||
|
||||
it.each(["raw", "doctor-hashed"] as const)(
|
||||
"resumes beta5 user MCP bindings stored as %s fingerprints",
|
||||
async (legacyForm) => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
registerCodexTestSessionIdentity(sessionFile, "session-1", "agent:main:session-1");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const authorization = "Bearer beta5-access-token";
|
||||
const config = {
|
||||
mcp: {
|
||||
servers: {
|
||||
ducktape: {
|
||||
transport: "streamable-http",
|
||||
url: "https://agents.ducktape.xyz/mcp",
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
"x-tenant": "keep",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as EmbeddedRunAttemptParams["config"];
|
||||
const request = vi.fn(async (method: string, _params: unknown) => {
|
||||
if (method === "thread/start") {
|
||||
return threadStartResult("thread-beta5");
|
||||
}
|
||||
if (method === "thread/resume") {
|
||||
return threadResumeResult("thread-beta5");
|
||||
}
|
||||
throw new Error(`unexpected method: ${method}`);
|
||||
});
|
||||
const run = () =>
|
||||
startOrResumeThread({
|
||||
client: { request } as never,
|
||||
params: createParams(sessionFile, workspaceDir, config),
|
||||
cwd: workspaceDir,
|
||||
dynamicTools: [],
|
||||
appServer: createAppServerOptions(),
|
||||
});
|
||||
|
||||
await run();
|
||||
const currentBinding = await readCodexAppServerBinding(sessionFile);
|
||||
expect(currentBinding).toBeDefined();
|
||||
|
||||
const legacyFingerprint = JSON.stringify({
|
||||
mcp_servers: {
|
||||
ducktape: {
|
||||
http_headers: {
|
||||
Authorization: authorization,
|
||||
"x-tenant": "keep",
|
||||
},
|
||||
url: "https://agents.ducktape.xyz/mcp",
|
||||
},
|
||||
},
|
||||
});
|
||||
seedCodexTestBinding(sessionFile, {
|
||||
...currentBinding!,
|
||||
userMcpServersFingerprint:
|
||||
legacyForm === "raw"
|
||||
? legacyFingerprint
|
||||
: hashCodexAppServerBindingFingerprint(legacyFingerprint),
|
||||
});
|
||||
|
||||
request.mockClear();
|
||||
await run();
|
||||
expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/resume"]);
|
||||
const convergedBinding = await readCodexAppServerBinding(sessionFile);
|
||||
expect(convergedBinding?.userMcpServersFingerprint).toMatch(/^sha256:[a-f0-9]{64}$/);
|
||||
expect(convergedBinding?.userMcpServersFingerprint).not.toContain("beta5-access-token");
|
||||
expect(convergedBinding?.userMcpServersFingerprint).not.toBe(legacyFingerprint);
|
||||
expect(convergedBinding?.userMcpServersFingerprint).not.toBe(
|
||||
hashCodexAppServerBindingFingerprint(legacyFingerprint),
|
||||
);
|
||||
|
||||
request.mockClear();
|
||||
await run();
|
||||
expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/resume"]);
|
||||
},
|
||||
);
|
||||
|
||||
it("projects only Codex user MCP servers scoped to the current agent", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
|
||||
Reference in New Issue
Block a user