mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:40:44 +00:00
fix(auth): quiet codex oauth manual fallback
This commit is contained in:
@@ -61,6 +61,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord: clear stale startup probe bot/application status when the async bot probe throws, not just when it returns a degraded probe result. Thanks @vincentkoc.
|
||||
- Web search: scope explicit bundled `web_search` provider runtime loading through manifest ownership, so selecting DuckDuckGo/Gemini/etc. does not import unrelated bundled providers or log their optional dependency failures. Thanks @vincentkoc.
|
||||
- Plugins/discovery: demote the source-only TypeScript runtime check on already-installed `origin: "global"` plugin packages from a config-blocking error to a warning and let the runtime fall through to the TypeScript source via jiti, so a single broken installed package no longer blocks `plugins install` for unrelated plugins; install-time rejection of newly-installed source-only packages is unchanged. Thanks @romneyda.
|
||||
- Providers/OpenAI Codex: stop the OAuth progress spinner before showing the manual redirect paste prompt, so callback timeouts do not spam `Browser callback did not finish` across terminals.
|
||||
- Release/beta smoke: resolve the dispatched Telegram beta E2E run from `gh run list` when `gh workflow run` returns no run URL, so the maintainer helper does not fail immediately after dispatch. Thanks @vincentkoc.
|
||||
- Media/images: keep HEIC/HEIF attachments fail-closed when optional Sharp conversion is unavailable instead of sending originals that still need conversion. Thanks @vincentkoc.
|
||||
- Google Meet: fork the caller's current agent transcript into agent-mode meeting consultant sessions, so Meet replies inherit the context from the tool call that joined the meeting.
|
||||
|
||||
@@ -34,12 +34,13 @@ type CodexLoginOptions = {
|
||||
|
||||
function createPrompter() {
|
||||
const spin = { update: vi.fn(), stop: vi.fn() };
|
||||
const text = vi.fn(async () => "http://localhost:1455/auth/callback?code=test");
|
||||
const prompter: Pick<WizardPrompter, "note" | "progress" | "text"> = {
|
||||
note: vi.fn(async () => {}),
|
||||
progress: vi.fn(() => spin),
|
||||
text: vi.fn(async () => "http://localhost:1455/auth/callback?code=test"),
|
||||
text,
|
||||
};
|
||||
return { prompter: prompter as unknown as WizardPrompter, spin };
|
||||
return { prompter: prompter as unknown as WizardPrompter, spin, text };
|
||||
}
|
||||
|
||||
function createRuntime(): RuntimeEnv {
|
||||
@@ -222,7 +223,7 @@ describe("loginOpenAICodexOAuth", () => {
|
||||
|
||||
it("waits briefly before prompting for manual input after the local browser flow starts", async () => {
|
||||
vi.useFakeTimers();
|
||||
const { prompter } = createPrompter();
|
||||
const { prompter, spin, text } = createPrompter();
|
||||
const runtime = createRuntime();
|
||||
mocks.loginOpenAICodex.mockImplementation(async (opts: CodexLoginOptions) => {
|
||||
await startCodexAuth(opts);
|
||||
@@ -252,6 +253,54 @@ describe("loginOpenAICodexOAuth", () => {
|
||||
message: "Paste the authorization code (or full redirect URL):",
|
||||
validate: expect.any(Function),
|
||||
});
|
||||
expect(spin.stop).toHaveBeenCalledWith("Manual OAuth entry required");
|
||||
expect(spin.stop.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
text.mock.invocationCallOrder[0] ?? 0,
|
||||
);
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
"OpenAI Codex OAuth callback did not arrive within 15000ms; switching to manual entry (callback_timeout).",
|
||||
);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("reuses one local manual prompt when the oauth helper repeats fallback calls", async () => {
|
||||
vi.useFakeTimers();
|
||||
const { prompter, spin, text } = createPrompter();
|
||||
const runtime = createRuntime();
|
||||
mocks.loginOpenAICodex.mockImplementation(async (opts: CodexLoginOptions) => {
|
||||
await startCodexAuth(opts);
|
||||
const firstManualPromise = opts.onManualCodeInput?.();
|
||||
const secondManualPromise = opts.onManualCodeInput?.();
|
||||
await vi.advanceTimersByTimeAsync(16_000);
|
||||
const [firstManualCode, secondManualCode] = await Promise.all([
|
||||
firstManualPromise,
|
||||
secondManualPromise,
|
||||
]);
|
||||
expect(secondManualCode).toBe(firstManualCode);
|
||||
return createCodexCredentials({ manualCode: firstManualCode });
|
||||
});
|
||||
|
||||
await expect(
|
||||
loginOpenAICodexOAuth({
|
||||
prompter,
|
||||
runtime,
|
||||
isRemote: false,
|
||||
openUrl: async () => {},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
});
|
||||
|
||||
expect(text).toHaveBeenCalledOnce();
|
||||
expect(spin.stop).toHaveBeenCalledWith("Manual OAuth entry required");
|
||||
expect(
|
||||
spin.update.mock.calls.filter(
|
||||
([message]) =>
|
||||
message === "Browser callback did not finish. Paste the redirect URL to continue…",
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(runtime.log).toHaveBeenCalledTimes(2);
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
"OpenAI Codex OAuth callback did not arrive within 15000ms; switching to manual entry (callback_timeout).",
|
||||
);
|
||||
@@ -326,7 +375,7 @@ describe("loginOpenAICodexOAuth", () => {
|
||||
|
||||
it("prompts for manual input immediately when the local callback flow never starts", async () => {
|
||||
vi.useFakeTimers();
|
||||
const { prompter } = createPrompter();
|
||||
const { prompter, spin, text } = createPrompter();
|
||||
const runtime = createRuntime();
|
||||
mocks.loginOpenAICodex.mockImplementation(
|
||||
async (opts: { onManualCodeInput?: () => Promise<string> }) => {
|
||||
@@ -352,6 +401,49 @@ describe("loginOpenAICodexOAuth", () => {
|
||||
message: "Paste the authorization code (or full redirect URL):",
|
||||
validate: expect.any(Function),
|
||||
});
|
||||
expect(spin.stop).toHaveBeenCalledWith("Manual OAuth entry required");
|
||||
expect(spin.stop.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
text.mock.invocationCallOrder[0] ?? 0,
|
||||
);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("reuses one immediate manual prompt when the local callback flow never starts", async () => {
|
||||
vi.useFakeTimers();
|
||||
const { prompter, spin, text } = createPrompter();
|
||||
const runtime = createRuntime();
|
||||
mocks.loginOpenAICodex.mockImplementation(async (opts: CodexLoginOptions) => {
|
||||
expect(opts.onManualCodeInput).toBeTypeOf("function");
|
||||
const [firstManualCode, secondManualCode] = await Promise.all([
|
||||
opts.onManualCodeInput?.(),
|
||||
opts.onManualCodeInput?.(),
|
||||
]);
|
||||
expect(secondManualCode).toBe(firstManualCode);
|
||||
return createCodexCredentials({ manualCode: firstManualCode });
|
||||
});
|
||||
|
||||
await expect(
|
||||
loginOpenAICodexOAuth({
|
||||
prompter,
|
||||
runtime,
|
||||
isRemote: false,
|
||||
openUrl: async () => {},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
});
|
||||
|
||||
expect(text).toHaveBeenCalledOnce();
|
||||
expect(spin.stop).toHaveBeenCalledWith("Manual OAuth entry required");
|
||||
expect(
|
||||
spin.update.mock.calls.filter(
|
||||
([message]) =>
|
||||
message === "Local OAuth callback was unavailable. Paste the redirect URL to continue…",
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
@@ -77,25 +77,30 @@ function createManualCodeInputHandler(params: {
|
||||
isRemote: boolean;
|
||||
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
|
||||
runtime: RuntimeEnv;
|
||||
spin: ReturnType<WizardPrompter["progress"]>;
|
||||
updateProgress: (message: string) => void;
|
||||
stopProgress: (message?: string) => void;
|
||||
waitForLoginToSettle: Promise<void>;
|
||||
hasBrowserAuthStarted: () => boolean;
|
||||
}): (() => Promise<string>) | undefined {
|
||||
let manualFallbackPromise: Promise<string> | undefined;
|
||||
if (params.isRemote) {
|
||||
return async () =>
|
||||
await params.onPrompt({
|
||||
return async () => {
|
||||
manualFallbackPromise ??= params.onPrompt({
|
||||
message: manualInputPromptMessage,
|
||||
});
|
||||
return await manualFallbackPromise;
|
||||
};
|
||||
}
|
||||
|
||||
return async () => {
|
||||
const runLocalManualFallback = async () => {
|
||||
if (!params.hasBrowserAuthStarted()) {
|
||||
params.spin.update(
|
||||
params.updateProgress(
|
||||
"Local OAuth callback was unavailable. Paste the redirect URL to continue…",
|
||||
);
|
||||
params.runtime.log(
|
||||
"OpenAI Codex OAuth local callback did not start; switching to manual entry immediately.",
|
||||
);
|
||||
params.stopProgress("Manual OAuth entry required");
|
||||
return await params.onPrompt({
|
||||
message: manualInputPromptMessage,
|
||||
});
|
||||
@@ -121,14 +126,20 @@ function createManualCodeInputHandler(params: {
|
||||
return await createNeverSettlingPromptResult();
|
||||
}
|
||||
|
||||
params.spin.update("Browser callback did not finish. Paste the redirect URL to continue…");
|
||||
params.updateProgress("Browser callback did not finish. Paste the redirect URL to continue…");
|
||||
params.runtime.log(
|
||||
`OpenAI Codex OAuth callback did not arrive within ${localManualFallbackDelayMs}ms; switching to manual entry (callback_timeout).`,
|
||||
);
|
||||
params.stopProgress("Manual OAuth entry required");
|
||||
return await params.onPrompt({
|
||||
message: manualInputPromptMessage,
|
||||
});
|
||||
};
|
||||
|
||||
return async () => {
|
||||
manualFallbackPromise ??= runLocalManualFallback();
|
||||
return await manualFallbackPromise;
|
||||
};
|
||||
}
|
||||
|
||||
export async function loginOpenAICodexOAuth(params: {
|
||||
@@ -166,6 +177,18 @@ export async function loginOpenAICodexOAuth(params: {
|
||||
);
|
||||
|
||||
const spin = prompter.progress("Starting OAuth flow…");
|
||||
let progressActive = true;
|
||||
const updateProgress = (message: string) => {
|
||||
if (progressActive) {
|
||||
spin.update(message);
|
||||
}
|
||||
};
|
||||
const stopProgress = (message?: string) => {
|
||||
if (progressActive) {
|
||||
progressActive = false;
|
||||
spin.stop(message);
|
||||
}
|
||||
};
|
||||
let browserAuthStarted = false;
|
||||
let markLoginSettled!: () => void;
|
||||
const waitForLoginToSettle = new Promise<void>((resolve) => {
|
||||
@@ -194,16 +217,17 @@ export async function loginOpenAICodexOAuth(params: {
|
||||
isRemote,
|
||||
onPrompt,
|
||||
runtime,
|
||||
spin,
|
||||
updateProgress,
|
||||
stopProgress,
|
||||
waitForLoginToSettle,
|
||||
hasBrowserAuthStarted: () => browserAuthStarted,
|
||||
}),
|
||||
onProgress: (msg: string) => spin.update(msg),
|
||||
onProgress: (msg: string) => updateProgress(msg),
|
||||
});
|
||||
spin.stop("OpenAI OAuth complete");
|
||||
stopProgress("OpenAI OAuth complete");
|
||||
return creds ?? null;
|
||||
} catch (err) {
|
||||
spin.stop("OpenAI OAuth failed");
|
||||
stopProgress("OpenAI OAuth failed");
|
||||
const rewrittenError = rewriteOpenAICodexOAuthError(err);
|
||||
runtime.error(String(rewrittenError));
|
||||
await prompter.note("Trouble with OAuth? See https://docs.openclaw.ai/start/faq", "OAuth help");
|
||||
|
||||
Reference in New Issue
Block a user