Skip to content

Commit 37c5003

Browse files
nxmxbbdsteipete
andauthored
fix(auth): harden Codex auth probes (#87559)
* fix(auth): harden Codex auth probes * fix: preserve Codex probe auth overlay (#87559) --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
1 parent ca41fa2 commit 37c5003

5 files changed

Lines changed: 170 additions & 10 deletions

File tree

src/agents/embedded-agent-runner/run.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -781,17 +781,26 @@ export async function runEmbeddedAgent(
781781
pluginHarnessOwnsTransport &&
782782
provider === OPENAI_CODEX_PROVIDER_ID &&
783783
effectiveModel.api === "openai-codex-responses";
784+
const openClawNativeCodexResponsesNeedsAuthBootstrap =
785+
!pluginHarnessOwnsTransport &&
786+
provider === OPENAI_CODEX_PROVIDER_ID &&
787+
effectiveModel.api === "openai-codex-responses";
784788
let piExternalCliAuthScope = pluginHarnessOwnsTransport
785789
? { ignoreAutoPreferredProfile: false }
786-
: resolveExternalCliAuthOverlayScopeFromSelection({
787-
provider,
788-
cfg: params.config,
789-
agentId: params.agentId,
790-
modelId,
791-
workspaceDir: resolvedWorkspace,
792-
userLockedAuthProfileId:
793-
params.authProfileIdSource === "user" ? params.authProfileId : undefined,
794-
});
790+
: openClawNativeCodexResponsesNeedsAuthBootstrap
791+
? {
792+
providerIds: [OPENAI_CODEX_PROVIDER_ID],
793+
ignoreAutoPreferredProfile: false,
794+
}
795+
: resolveExternalCliAuthOverlayScopeFromSelection({
796+
provider,
797+
cfg: params.config,
798+
agentId: params.agentId,
799+
modelId,
800+
workspaceDir: resolvedWorkspace,
801+
userLockedAuthProfileId:
802+
params.authProfileIdSource === "user" ? params.authProfileId : undefined,
803+
});
795804
let noExternalAuthStore: AuthProfileStore | undefined;
796805
if (
797806
!pluginHarnessOwnsTransport &&

src/agents/openai-transport-stream.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2293,6 +2293,65 @@ describe("openai transport stream", () => {
22932293
expect(params.prompt_cache_key).toBeUndefined();
22942294
});
22952295

2296+
it("adds fallback instructions for raw native Codex responses probes", () => {
2297+
const params = buildOpenAIResponsesParams(
2298+
{
2299+
id: "gpt-5.5",
2300+
name: "GPT-5.5",
2301+
api: "openai-codex-responses",
2302+
provider: "openai-codex",
2303+
baseUrl: "https://chatgpt.com/backend-api/codex",
2304+
reasoning: true,
2305+
input: ["text"],
2306+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
2307+
contextWindow: 400000,
2308+
maxTokens: 128000,
2309+
} satisfies Model<"openai-codex-responses">,
2310+
{
2311+
systemPrompt: "",
2312+
messages: [{ role: "user", content: "Reply OK", timestamp: 1 }],
2313+
tools: [],
2314+
} as never,
2315+
{
2316+
maxTokens: 16,
2317+
sessionId: "session-123",
2318+
},
2319+
) as Record<string, unknown>;
2320+
2321+
expect(params.instructions).toBe("Follow the user request.");
2322+
expect(params.max_output_tokens).toBeUndefined();
2323+
expect(params.prompt_cache_retention).toBeUndefined();
2324+
});
2325+
2326+
it("does not add fallback instructions for custom Codex-compatible responses backends", () => {
2327+
const params = buildOpenAIResponsesParams(
2328+
{
2329+
id: "gpt-5.5",
2330+
name: "GPT-5.5",
2331+
api: "openai-codex-responses",
2332+
provider: "openai-codex",
2333+
baseUrl: "https://proxy.example.com/v1",
2334+
reasoning: true,
2335+
input: ["text"],
2336+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
2337+
contextWindow: 400000,
2338+
maxTokens: 128000,
2339+
} satisfies Model<"openai-codex-responses">,
2340+
{
2341+
systemPrompt: "",
2342+
messages: [{ role: "user", content: "Reply OK", timestamp: 1 }],
2343+
tools: [],
2344+
} as never,
2345+
{
2346+
maxTokens: 16,
2347+
sessionId: "session-123",
2348+
},
2349+
) as Record<string, unknown>;
2350+
2351+
expect(params.instructions).toBeUndefined();
2352+
expect(params.max_output_tokens).toBe(16);
2353+
});
2354+
22962355
it("uses top-level instructions for Codex responses and preserves prompt cache identity", () => {
22972356
const params = buildOpenAIResponsesParams(
22982357
{

src/agents/openai-transport-stream.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import {
7979

8080
const DEFAULT_AZURE_OPENAI_API_VERSION = "preview";
8181
const OPENAI_CODEX_RESPONSES_EMPTY_INPUT_TEXT = " ";
82+
const OPENAI_CODEX_RESPONSES_DEFAULT_INSTRUCTIONS = "Follow the user request.";
8283
const GEMINI_THOUGHT_SIGNATURE_VALIDATOR_SKIP = "skip_thought_signature_validator";
8384
const AZURE_RESPONSES_FIRST_EVENT_TIMEOUT_MS = 30_000;
8485
const MODEL_STREAM_COOPERATIVE_YIELD_INTERVAL_MS = 12;
@@ -1992,6 +1993,19 @@ function buildOpenAICodexResponsesInstructions(context: Context): string | undef
19921993
return sanitizeTransportPayloadText(stripSystemPromptCacheBoundary(context.systemPrompt));
19931994
}
19941995

1996+
function resolveOpenAICodexResponsesInstructions(
1997+
model: Model,
1998+
context: Context,
1999+
): string | undefined {
2000+
const instructions = buildOpenAICodexResponsesInstructions(context);
2001+
if (instructions && instructions.trim().length > 0) {
2002+
return instructions;
2003+
}
2004+
return usesNativeOpenAICodexResponsesBackend(model)
2005+
? OPENAI_CODEX_RESPONSES_DEFAULT_INSTRUCTIONS
2006+
: undefined;
2007+
}
2008+
19952009
function ensureOpenAICodexResponsesInput(messages: ResponseInput, context: Context): void {
19962010
if (messages.length > 0 || !context.systemPrompt) {
19972011
return;
@@ -2063,7 +2077,9 @@ export function buildOpenAIResponsesParams(
20632077
stream: true,
20642078
prompt_cache_key: promptCacheKey,
20652079
prompt_cache_retention: getPromptCacheRetention(model.baseUrl, cacheRetention),
2066-
...(isCodexResponses ? { instructions: buildOpenAICodexResponsesInstructions(context) } : {}),
2080+
...(isCodexResponses
2081+
? { instructions: resolveOpenAICodexResponsesInstructions(model, context) }
2082+
: {}),
20672083
...(metadata ? { metadata } : {}),
20682084
};
20692085
const effectiveMaxTokens = options?.maxTokens || model.maxTokens;

src/commands/models/list.probe.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,75 @@ describe("mapFailoverReasonToProbeStatus", () => {
3838
expect(mapFailoverReasonToProbeStatus("something_else")).toBe("unknown");
3939
});
4040
});
41+
42+
describe("runAuthProbes", () => {
43+
it("runs Codex auth probes through raw OpenClaw model-run mode", async () => {
44+
const runEmbeddedAgent = vi.fn(async () => ({ text: "OK" }));
45+
vi.doMock("../../agents/embedded-agent.js", () => ({ runEmbeddedAgent }));
46+
vi.doMock("../../agents/auth-profiles.js", () => ({
47+
externalCliDiscoveryScoped: () => undefined,
48+
ensureAuthProfileStore: () => ({
49+
version: 1,
50+
profiles: {
51+
"openai-codex:profile": {
52+
type: "oauth",
53+
provider: "openai-codex",
54+
access: "access-token",
55+
refresh: "refresh-token",
56+
expires: Date.now() + 60_000,
57+
},
58+
},
59+
order: {},
60+
}),
61+
listProfilesForProvider: () => ["openai-codex:profile"],
62+
resolveAuthProfileDisplayLabel: ({ profileId }: { profileId: string }) => profileId,
63+
resolveAuthProfileEligibility: () => ({ eligible: true }),
64+
resolveAuthProfileOrder: () => ["openai-codex:profile"],
65+
}));
66+
vi.doMock("../../agents/model-auth.js", () => ({
67+
hasUsableCustomProviderApiKey: () => false,
68+
resolveEnvApiKey: () => null,
69+
}));
70+
vi.doMock("../../agents/model-catalog.js", () => ({
71+
loadModelCatalog: async () => [{ provider: "openai-codex", id: "gpt-5.5" }],
72+
}));
73+
try {
74+
const module = await importFreshModule<typeof import("./list.probe.js")>(
75+
import.meta.url,
76+
`./list.probe.js?scope=${Math.random().toString(36).slice(2)}`,
77+
);
78+
const result = await module.runAuthProbes({
79+
cfg: {} as never,
80+
agentId: "probe-agent",
81+
agentDir: "/tmp/openclaw-probe-agent",
82+
workspaceDir: "/tmp/openclaw-probe-workspace",
83+
providers: ["openai-codex"],
84+
modelCandidates: ["openai-codex/gpt-5.5"],
85+
options: {
86+
provider: "openai-codex",
87+
profileIds: ["openai-codex:profile"],
88+
timeoutMs: 5_000,
89+
concurrency: 1,
90+
maxTokens: 8,
91+
},
92+
});
93+
94+
expect(result.results[0]?.status).toBe("ok");
95+
expect(runEmbeddedAgent).toHaveBeenCalledWith(
96+
expect.objectContaining({
97+
agentHarnessId: "openclaw",
98+
agentHarnessRuntimeOverride: "openclaw",
99+
modelRun: true,
100+
disableTools: true,
101+
authProfileId: "openai-codex:profile",
102+
authProfileIdSource: "user",
103+
}),
104+
);
105+
} finally {
106+
vi.doUnmock("../../agents/embedded-agent.js");
107+
vi.doUnmock("../../agents/auth-profiles.js");
108+
vi.doUnmock("../../agents/model-auth.js");
109+
vi.doUnmock("../../agents/model-catalog.js");
110+
}
111+
});
112+
});

src/commands/models/list.probe.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,9 @@ async function probeTarget(params: {
522522
model: target.model.model,
523523
authProfileId: target.profileId,
524524
authProfileIdSource: target.profileId ? "user" : undefined,
525+
...(target.provider === "openai-codex"
526+
? { agentHarnessId: "openclaw", agentHarnessRuntimeOverride: "openclaw" }
527+
: {}),
525528
timeoutMs,
526529
runId: `probe-${crypto.randomUUID()}`,
527530
lane: `auth-probe:${target.provider}:${target.profileId ?? target.source}`,
@@ -530,6 +533,7 @@ async function probeTarget(params: {
530533
verboseLevel: "off",
531534
streamParams: { maxTokens },
532535
disableTools: true,
536+
modelRun: true,
533537
cleanupBundleMcpOnRunEnd: true,
534538
});
535539
return buildResult("ok");

0 commit comments

Comments
 (0)