fix(codex): rotate shared app-server clients on auth changes

This commit is contained in:
Lucenx9
2026-04-22 22:02:49 +02:00
committed by Peter Steinberger
parent f4c4e940a6
commit 0bc5ccc706
3 changed files with 75 additions and 1 deletions

View File

@@ -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"),

View File

@@ -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 ? "<set>" : 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 = "";

View File

@@ -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");
});
});