diff --git a/extensions/codex/src/app-server/config.test.ts b/extensions/codex/src/app-server/config.test.ts index 80edd717d4c..cba17376d2e 100644 --- a/extensions/codex/src/app-server/config.test.ts +++ b/extensions/codex/src/app-server/config.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import { describe, expect, it } from "vitest"; import { CODEX_APP_SERVER_CONFIG_KEYS, + codexAppServerStartOptionsKey, readCodexPluginConfig, resolveCodexAppServerRuntimeOptions, } from "./config.js"; @@ -75,6 +76,29 @@ describe("Codex app-server config", () => { ); }); + it("derives distinct shared-client keys for distinct auth tokens without exposing them", () => { + const first = codexAppServerStartOptionsKey({ + transport: "websocket", + command: "codex", + args: [], + url: "ws://127.0.0.1:39175", + authToken: "tok_first", + headers: {}, + }); + const second = codexAppServerStartOptionsKey({ + transport: "websocket", + command: "codex", + args: [], + url: "ws://127.0.0.1:39175", + authToken: "tok_second", + headers: {}, + }); + + expect(first).not.toEqual(second); + expect(first).not.toContain("tok_first"); + expect(second).not.toContain("tok_second"); + }); + it("keeps runtime config keys aligned with manifest schema and UI hints", async () => { const manifest = JSON.parse( await fs.readFile(new URL("../../openclaw.plugin.json", import.meta.url), "utf8"), diff --git a/extensions/codex/src/app-server/config.ts b/extensions/codex/src/app-server/config.ts index 12a9c749cf4..3b2875588da 100644 --- a/extensions/codex/src/app-server/config.ts +++ b/extensions/codex/src/app-server/config.ts @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto"; import { z } from "zod"; export type CodexAppServerTransportMode = "stdio" | "websocket"; @@ -156,7 +157,7 @@ export function codexAppServerStartOptionsKey(options: CodexAppServerStartOption command: options.command, args: options.args, url: options.url ?? null, - authToken: options.authToken ? "" : null, + authToken: hashSecretForKey(options.authToken), headers: Object.entries(options.headers).toSorted(([left], [right]) => left.localeCompare(right), ), @@ -223,6 +224,13 @@ function readNonEmptyString(value: unknown): string | undefined { return trimmed || undefined; } +function hashSecretForKey(value: string | undefined): string | null { + if (!value) { + return null; + } + return createHash("sha256").update(value).digest("hex"); +} + function splitShellWords(value: string): string[] { const words: string[] = []; let current = ""; diff --git a/extensions/codex/src/app-server/shared-client.test.ts b/extensions/codex/src/app-server/shared-client.test.ts index 54ef15aa6bd..a41109bde1a 100644 --- a/extensions/codex/src/app-server/shared-client.test.ts +++ b/extensions/codex/src/app-server/shared-client.test.ts @@ -102,4 +102,46 @@ describe("shared Codex app-server client", () => { }), ); }); + + it("restarts the shared client when the bridged auth token changes", async () => { + const first = createClientHarness(); + const second = createClientHarness(); + const startSpy = vi + .spyOn(CodexAppServerClient, "start") + .mockReturnValueOnce(first.client) + .mockReturnValueOnce(second.client); + + const firstList = listCodexAppServerModels({ + timeoutMs: 1000, + startOptions: { + transport: "websocket", + command: "codex", + args: [], + url: "ws://127.0.0.1:39175", + authToken: "tok-first", + headers: {}, + }, + }); + await sendInitializeResult(first, "openclaw/0.118.0 (macOS; test)"); + await sendEmptyModelList(first); + await expect(firstList).resolves.toEqual({ models: [] }); + + const secondList = listCodexAppServerModels({ + timeoutMs: 1000, + startOptions: { + transport: "websocket", + command: "codex", + args: [], + url: "ws://127.0.0.1:39175", + authToken: "tok-second", + headers: {}, + }, + }); + await sendInitializeResult(second, "openclaw/0.118.0 (macOS; test)"); + await sendEmptyModelList(second); + await expect(secondList).resolves.toEqual({ models: [] }); + + expect(startSpy).toHaveBeenCalledTimes(2); + expect(first.process.kill).toHaveBeenCalledWith("SIGTERM"); + }); });