fix(openai): harden codex device auth prep

This commit is contained in:
Val Alexander
2026-04-22 02:37:23 -05:00
parent cb353c0c5a
commit b9452d2df8
5 changed files with 56 additions and 2 deletions

View File

@@ -185,6 +185,17 @@ Docs: https://docs.openclaw.ai
- Control UI/device pairing: explain scope and role approval upgrades during reconnects, and show requested versus approved access in the Control UI and `openclaw devices` so broader reconnects no longer look like lost pairings. (#69221) Thanks @obviyus.
- Gateway/Control UI: surface pending scope, role, and device-metadata pairing approvals in auth errors and Control UI hints so broader reconnects no longer look like random auth breakage. (#69226) Thanks @obviyus.
## 2026.4.19-beta.2
### Fixes
- Agents/openai-completions: always send `stream_options.include_usage` on streaming requests, so local and custom OpenAI-compatible backends report real context usage instead of showing 0%. (#68746) Thanks @kagura-agent.
- Agents/nested lanes: scope nested agent work per target session so a long-running nested run on one session no longer head-of-line blocks unrelated sessions across the gateway. (#67785) Thanks @stainlu.
- Agents/status: preserve carried-forward session token totals for providers that omit usage metadata, so `/status` and `openclaw sessions` keep showing the last known context usage instead of dropping back to unknown/0%. (#67695) Thanks @stainlu.
- Install/update: keep legacy update verification compatible with the QA Lab runtime shim, so updating older global installs to beta no longer fails after npm installs the package successfully.
## 2026.4.19-beta.1
### Fixes
- Agents/channels: route cross-agent subagent spawns through the target agent's bound channel account while preserving peer and workspace/role-scoped bindings, so child sessions no longer inherit the caller's account in shared rooms, workspaces, or multi-account setups. (#67508) Thanks @lukeboyett and @gumadeiras.

View File

@@ -182,4 +182,34 @@ describe("loginOpenAICodexDeviceCode", () => {
"OpenAI device authorization failed: authorization_declined spoofed (Denied next line)",
);
});
it("strips C1 terminal controls from reflected device-code errors", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(
createJsonResponse({
device_auth_id: "device-auth-123",
user_code: "CODE-12345",
interval: "0",
}),
)
.mockResolvedValueOnce(
createJsonResponse(
{
error: `authorization_declined${String.fromCharCode(0x9b)}spoofed`,
error_description: `Denied${String.fromCharCode(0x9d)}next line`,
},
{ status: 401 },
),
);
await expect(
loginOpenAICodexDeviceCode({
fetchFn: fetchMock as typeof fetch,
onVerification: async () => {},
}),
).rejects.toThrow(
"OpenAI device authorization failed: authorization_declined spoofed (Denied next line)",
);
});
});

View File

@@ -88,7 +88,9 @@ function sanitizeDeviceCodeErrorText(value: string): string {
const c0Start = String.fromCharCode(0x00);
const c0End = String.fromCharCode(0x1f);
const del = String.fromCharCode(0x7f);
const controlCharsRegex = new RegExp(`[${c0Start}-${c0End}${del}]`, "g");
const c1Start = String.fromCharCode(0x80);
const c1End = String.fromCharCode(0x9f);
const controlCharsRegex = new RegExp(`[${c0Start}-${c0End}${del}${c1Start}-${c1End}]`, "g");
return value
.replace(osc8Regex, "")
.replace(ansiCsiRegex, "")

View File

@@ -333,6 +333,14 @@ describe("openai codex provider", () => {
const logOutput = runtime.log.mock.calls.flat().join("\n");
expect(logOutput).toContain("https://auth.openai.com/codex/device");
expect(logOutput).not.toContain("CODE-12345");
expect(note).toHaveBeenCalledWith(
expect.stringContaining("Code: [shown on the local device only]"),
"OpenAI Codex device code",
);
expect(note).not.toHaveBeenCalledWith(
expect.stringContaining("Code: CODE-12345"),
"OpenAI Codex device code",
);
});
it("exposes Codex CLI auth as a runtime-only external profile", () => {

View File

@@ -319,13 +319,16 @@ async function runOpenAICodexDeviceCode(ctx: ProviderAuthContext) {
onProgress: (message) => spin.update(message),
onVerification: async ({ verificationUrl, userCode, expiresInMs }) => {
const expiresInMinutes = Math.max(1, Math.round(expiresInMs / 60_000));
const codeLine = ctx.isRemote
? "Code: [shown on the local device only]"
: `Code: ${userCode}`;
await ctx.prompter.note(
[
ctx.isRemote
? "Open this URL in your LOCAL browser and enter the code below."
: "Open this URL in your browser and enter the code below.",
`URL: ${verificationUrl}`,
`Code: ${userCode}`,
codeLine,
`Code expires in ${expiresInMinutes} minutes. Never share it.`,
].join("\n"),
"OpenAI Codex device code",