mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 11:47:28 +00:00
fix(openai-codex): request required oauth api scopes (#24720)
This commit is contained in:
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/chat streaming tool-boundary text retention: merge assistant delta segments into per-run chat buffers so pre-tool text is preserved in live chat deltas/finals when providers emit post-tool assistant segments as non-prefix snapshots. (#36957) Thanks @Datyedyeguy.
|
||||
- TUI/model indicator freshness: prevent stale session snapshots from overwriting freshly patched model selection (and reset per-session freshness when switching session keys) so `/model` updates reflect immediately instead of lagging by one or more commands. (#21255) Thanks @kowza.
|
||||
- OpenAI Codex OAuth/login hardening: fail OAuth completion early when the returned token is missing `api.responses.write`, and allow `openclaw models auth login --provider openai-codex` to use the built-in OAuth path even when no provider plugins are installed. (#36660) Thanks @driesvints.
|
||||
- OpenAI Codex OAuth/scope request parity: augment the OAuth authorize URL with required API scopes (`api.responses.write`, `model.request`, `api.model.read`) before browser handoff so OAuth tokens include runtime model/request permissions expected by OpenAI API calls. (#24720) Thanks @Skippy-Gunboat.
|
||||
- Gateway/remote WS break-glass hostname support: honor `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` for `ws://` hostname URLs (not only private IP literals) across onboarding validation and runtime gateway connection checks, while still rejecting public IP literals and non-unicast IPv6 endpoints. (#36930) Thanks @manju-rn.
|
||||
- Routing/binding lookup scalability: pre-index route bindings by channel/account and avoid full binding-list rescans on channel-account cache rollover, preventing multi-second `resolveAgentRoute` stalls in large binding configurations. (#36915) Thanks @songchenghao.
|
||||
- Browser/session cleanup: track browser tabs opened by session-scoped browser tool runs and close tracked tabs during `sessions.reset`/`sessions.delete` runtime cleanup, preventing orphaned tabs and unbounded browser memory growth after session teardown. (#36666) Thanks @Harnoor6693.
|
||||
|
||||
@@ -104,6 +104,42 @@ describe("loginOpenAICodexOAuth", () => {
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("augments OAuth authorize URL with required OpenAI API scopes", async () => {
|
||||
const creds = {
|
||||
provider: "openai-codex" as const,
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
email: "user@example.com",
|
||||
};
|
||||
const onAuthSpy = vi.fn();
|
||||
mocks.createVpsAwareOAuthHandlers.mockReturnValue({
|
||||
onAuth: onAuthSpy,
|
||||
onPrompt: vi.fn(),
|
||||
});
|
||||
mocks.loginOpenAICodex.mockImplementation(
|
||||
async (opts: { onAuth: (event: { url: string }) => Promise<void> }) => {
|
||||
await opts.onAuth({
|
||||
url: "https://auth.openai.com/oauth/authorize?scope=openid+profile+email+offline_access&state=abc",
|
||||
});
|
||||
return creds;
|
||||
},
|
||||
);
|
||||
|
||||
await runCodexOAuth({ isRemote: false });
|
||||
|
||||
expect(onAuthSpy).toHaveBeenCalledTimes(1);
|
||||
const event = onAuthSpy.mock.calls[0]?.[0] as { url: string };
|
||||
const scopes = new Set((new URL(event.url).searchParams.get("scope") ?? "").split(/\s+/));
|
||||
expect(scopes.has("openid")).toBe(true);
|
||||
expect(scopes.has("profile")).toBe(true);
|
||||
expect(scopes.has("email")).toBe(true);
|
||||
expect(scopes.has("offline_access")).toBe(true);
|
||||
expect(scopes.has("api.responses.write")).toBe(true);
|
||||
expect(scopes.has("model.request")).toBe(true);
|
||||
expect(scopes.has("api.model.read")).toBe(true);
|
||||
});
|
||||
|
||||
it("reports oauth errors and rethrows", async () => {
|
||||
mocks.createVpsAwareOAuthHandlers.mockReturnValue({
|
||||
onAuth: vi.fn(),
|
||||
|
||||
@@ -10,6 +10,46 @@ import {
|
||||
|
||||
const OPENAI_RESPONSES_ENDPOINT = "https://api.openai.com/v1/responses";
|
||||
const OPENAI_RESPONSES_WRITE_SCOPE = "api.responses.write";
|
||||
const OPENAI_REQUIRED_OAUTH_SCOPES = [
|
||||
OPENAI_RESPONSES_WRITE_SCOPE,
|
||||
"model.request",
|
||||
"api.model.read",
|
||||
] as const;
|
||||
|
||||
function augmentOpenAIOAuthScopes(authUrl: string): string {
|
||||
try {
|
||||
const parsed = new URL(authUrl);
|
||||
const scopeParam = parsed.searchParams.get("scope");
|
||||
if (!scopeParam) {
|
||||
return authUrl;
|
||||
}
|
||||
const scopes = scopeParam
|
||||
.split(/\s+/)
|
||||
.map((scope) => scope.trim())
|
||||
.filter(Boolean);
|
||||
if (scopes.length === 0) {
|
||||
return authUrl;
|
||||
}
|
||||
const seen = new Set(scopes.map((scope) => scope.toLowerCase()));
|
||||
let changed = false;
|
||||
for (const requiredScope of OPENAI_REQUIRED_OAUTH_SCOPES) {
|
||||
const normalized = requiredScope.toLowerCase();
|
||||
if (seen.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
scopes.push(requiredScope);
|
||||
seen.add(normalized);
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) {
|
||||
return authUrl;
|
||||
}
|
||||
parsed.searchParams.set("scope", scopes.join(" "));
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
return authUrl;
|
||||
}
|
||||
}
|
||||
|
||||
function extractResponsesScopeErrorMessage(status: number, bodyText: string): string | null {
|
||||
if (status !== 401) {
|
||||
@@ -76,7 +116,7 @@ export async function loginOpenAICodexOAuth(params: {
|
||||
|
||||
const spin = prompter.progress("Starting OAuth flow…");
|
||||
try {
|
||||
const { onAuth, onPrompt } = createVpsAwareOAuthHandlers({
|
||||
const { onAuth: baseOnAuth, onPrompt } = createVpsAwareOAuthHandlers({
|
||||
isRemote,
|
||||
prompter,
|
||||
runtime,
|
||||
@@ -84,6 +124,12 @@ export async function loginOpenAICodexOAuth(params: {
|
||||
openUrl,
|
||||
localBrowserMessage: localBrowserMessage ?? "Complete sign-in in browser…",
|
||||
});
|
||||
const onAuth = async (event: { url: string }) => {
|
||||
await baseOnAuth({
|
||||
...event,
|
||||
url: augmentOpenAIOAuthScopes(event.url),
|
||||
});
|
||||
};
|
||||
|
||||
const creds = await loginOpenAICodex({
|
||||
onAuth,
|
||||
|
||||
Reference in New Issue
Block a user