fix(codex): keep env fallback local to stdio app-server

This commit is contained in:
pashpashpash
2026-04-27 19:08:48 -04:00
committed by Peter Steinberger
parent 5f15bea6ce
commit 401ae38f13
7 changed files with 84 additions and 18 deletions

View File

@@ -389,6 +389,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
startOptions: createStartOptions(),
});
expect(request).toHaveBeenNthCalledWith(1, "account/read", { refreshToken: false });
@@ -415,6 +416,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
startOptions: createStartOptions(),
});
expect(request).toHaveBeenNthCalledWith(1, "account/read", { refreshToken: false });
@@ -443,6 +445,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
startOptions: createStartOptions(),
});
expect(request).toHaveBeenCalledTimes(1);
@@ -465,6 +468,7 @@ describe("bridgeCodexAppServerStartOptions", () => {
await applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
startOptions: createStartOptions(),
});
expect(request).toHaveBeenCalledTimes(1);
@@ -474,6 +478,32 @@ describe("bridgeCodexAppServerStartOptions", () => {
}
});
it("does not send env API-key fallback to websocket app-server connections", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
const request = vi.fn(async (method: string) => {
if (method === "account/read") {
return { account: null, requiresOpenaiAuth: true };
}
return { type: "apiKey" };
});
vi.stubEnv("CODEX_API_KEY", "codex-env-api-key");
vi.stubEnv("OPENAI_API_KEY", "openai-env-api-key");
try {
await applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
startOptions: createStartOptions({
transport: "websocket",
url: "ws://127.0.0.1:1455",
}),
});
expect(request).not.toHaveBeenCalled();
} finally {
await fs.rm(agentDir, { recursive: true, force: true });
}
});
it("applies an OpenAI Codex token profile backed by a secret ref", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));

View File

@@ -41,12 +41,16 @@ export async function applyCodexAppServerAuthProfile(params: {
client: CodexAppServerClient;
agentDir: string;
authProfileId?: string;
startOptions?: CodexAppServerStartOptions;
}): Promise<void> {
const loginParams = await resolveCodexAppServerAuthProfileLoginParams({
agentDir: params.agentDir,
authProfileId: params.authProfileId,
});
if (!loginParams) {
if (params.startOptions?.transport !== "stdio") {
return;
}
const fallbackLoginParams = await resolveCodexAppServerEnvApiKeyLoginParams({
client: params.client,
env: process.env,

View File

@@ -311,6 +311,16 @@ describe("Codex app-server config", () => {
});
expect(first).not.toEqual(second);
expect(
codexAppServerStartOptionsKey({
transport: "websocket",
command: "codex",
args: [],
url: "ws://127.0.0.1:39175",
authToken: "tok_first",
headers: {},
}),
).toEqual(first);
expect(first).not.toContain("tok_first");
expect(second).not.toContain("tok_second");
});
@@ -332,6 +342,15 @@ describe("Codex app-server config", () => {
});
expect(first).not.toEqual(second);
expect(
codexAppServerStartOptionsKey({
transport: "stdio",
command: "codex",
args: ["app-server"],
headers: {},
env: { OPENAI_API_KEY: "sk-first" },
}),
).toEqual(first);
expect(first).not.toContain("sk-first");
expect(second).not.toContain("sk-second");
});

View File

@@ -1,7 +1,9 @@
import { createHash } from "node:crypto";
import { createHmac, randomBytes } from "node:crypto";
import { z } from "zod";
import type { CodexSandboxPolicy, CodexServiceTier } from "./protocol.js";
const START_OPTIONS_KEY_SECRET = randomBytes(32);
export type CodexAppServerTransportMode = "stdio" | "websocket";
export type CodexAppServerPolicyMode = "yolo" | "guardian";
export type CodexAppServerApprovalPolicy = "never" | "on-request" | "on-failure" | "untrusted";
@@ -300,13 +302,13 @@ export function codexAppServerStartOptionsKey(
commandSource: options.commandSource ?? null,
args: options.args,
url: options.url ?? null,
authToken: hashSecretForKey(options.authToken),
authToken: hashSecretForKey(options.authToken, "authToken"),
headers: Object.entries(options.headers).toSorted(([left], [right]) =>
left.localeCompare(right),
),
env: Object.entries(options.env ?? {})
.toSorted(([left], [right]) => left.localeCompare(right))
.map(([key, value]) => [key, hashSecretForKey(value)]),
.map(([key, value]) => [key, hashSecretForKey(value, `env:${key}`)]),
clearEnv: [...(options.clearEnv ?? [])].toSorted(),
authProfileId: params.authProfileId ?? null,
});
@@ -431,11 +433,15 @@ function readNonEmptyString(value: unknown): string | undefined {
return trimmed || undefined;
}
function hashSecretForKey(value: string | undefined): string | null {
function hashSecretForKey(value: string | undefined, label: string): string | null {
if (!value) {
return null;
}
return createHash("sha256").update(value).digest("hex");
return createHmac("sha256", START_OPTIONS_KEY_SECRET)
.update(label)
.update("\0")
.update(value)
.digest("hex");
}
function splitShellWords(value: string): string[] {

View File

@@ -59,6 +59,7 @@ export async function getSharedCodexAppServerClient(options?: {
client,
agentDir,
authProfileId: options?.authProfileId,
startOptions,
});
return client;
} catch (error) {
@@ -104,6 +105,7 @@ export async function createIsolatedCodexAppServerClient(options?: {
client,
agentDir,
authProfileId: options?.authProfileId,
startOptions,
});
return client;
} catch (error) {