mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-29 09:02:02 +03:00
Auth: land codex oauth onboarding flow (#15406)
This commit is contained in:
@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Auto-reply/Threading: auto-inject implicit reply threading so `replyToMode` works without requiring model-emitted `[[reply_to_current]]`, while preserving `replyToMode: "off"` behavior for implicit Slack replies and keeping block-streaming chunk coalescing stable under `replyToMode: "first"`. (#14976) Thanks @Diaspar4u.
|
- Auto-reply/Threading: auto-inject implicit reply threading so `replyToMode` works without requiring model-emitted `[[reply_to_current]]`, while preserving `replyToMode: "off"` behavior for implicit Slack replies and keeping block-streaming chunk coalescing stable under `replyToMode: "first"`. (#14976) Thanks @Diaspar4u.
|
||||||
- Sandbox: pass configured `sandbox.docker.env` variables to sandbox containers at `docker create` time. (#15138) Thanks @stevebot-alive.
|
- Sandbox: pass configured `sandbox.docker.env` variables to sandbox containers at `docker create` time. (#15138) Thanks @stevebot-alive.
|
||||||
- Onboarding/CLI: restore terminal state without resuming paused `stdin`, so onboarding exits cleanly after choosing Web UI and the installer returns instead of appearing stuck.
|
- Onboarding/CLI: restore terminal state without resuming paused `stdin`, so onboarding exits cleanly after choosing Web UI and the installer returns instead of appearing stuck.
|
||||||
|
- Auth/OpenAI Codex: share OAuth login handling across onboarding and `models auth login --provider openai-codex`, keep onboarding alive when OAuth fails, and surface a direct OAuth help note instead of terminating the wizard. (#15406, follow-up to #14552) Thanks @zhiluo20.
|
||||||
- Onboarding/Providers: add vLLM as an onboarding provider with model discovery, auth profile wiring, and non-interactive auth-choice validation. (#12577) Thanks @gejifeng.
|
- Onboarding/Providers: add vLLM as an onboarding provider with model discovery, auth profile wiring, and non-interactive auth-choice validation. (#12577) Thanks @gejifeng.
|
||||||
- Onboarding/Providers: preserve Hugging Face auth intent in auth-choice remapping (`tokenProvider=huggingface` with `authChoice=apiKey`) and skip env-override prompts when an explicit token is provided. (#13472) Thanks @Josephrp.
|
- Onboarding/Providers: preserve Hugging Face auth intent in auth-choice remapping (`tokenProvider=huggingface` with `authChoice=apiKey`) and skip env-override prompts when an explicit token is provided. (#13472) Thanks @Josephrp.
|
||||||
- OpenAI Codex/Spark: implement end-to-end `gpt-5.3-codex-spark` support across fallback/thinking/model resolution and `models list` forward-compat visibility. (#14990, #15174) Thanks @L-U-C-K-Y, @loiie45e.
|
- OpenAI Codex/Spark: implement end-to-end `gpt-5.3-codex-spark` support across fallback/thinking/model resolution and `models list` forward-compat visibility. (#14990, #15174) Thanks @L-U-C-K-Y, @loiie45e.
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { loginOpenAICodex } from "@mariozechner/pi-ai";
|
|
||||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||||
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
||||||
import { upsertSharedEnvVar } from "../infra/env-file.js";
|
import { upsertSharedEnvVar } from "../infra/env-file.js";
|
||||||
@@ -9,13 +8,13 @@ import {
|
|||||||
} from "./auth-choice.api-key.js";
|
} from "./auth-choice.api-key.js";
|
||||||
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
|
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
|
||||||
import { isRemoteEnvironment } from "./oauth-env.js";
|
import { isRemoteEnvironment } from "./oauth-env.js";
|
||||||
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
|
|
||||||
import { applyAuthProfileConfig, writeOAuthCredentials } from "./onboard-auth.js";
|
import { applyAuthProfileConfig, writeOAuthCredentials } from "./onboard-auth.js";
|
||||||
import { openUrl } from "./onboard-helpers.js";
|
import { openUrl } from "./onboard-helpers.js";
|
||||||
import {
|
import {
|
||||||
applyOpenAICodexModelDefault,
|
applyOpenAICodexModelDefault,
|
||||||
OPENAI_CODEX_DEFAULT_MODEL,
|
OPENAI_CODEX_DEFAULT_MODEL,
|
||||||
} from "./openai-codex-model-default.js";
|
} from "./openai-codex-model-default.js";
|
||||||
|
import { loginOpenAICodexOAuth } from "./openai-codex-oauth.js";
|
||||||
import {
|
import {
|
||||||
applyOpenAIConfig,
|
applyOpenAIConfig,
|
||||||
applyOpenAIProviderConfig,
|
applyOpenAIProviderConfig,
|
||||||
@@ -125,66 +124,42 @@ export async function applyAuthChoiceOpenAI(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRemote = isRemoteEnvironment();
|
let creds;
|
||||||
await params.prompter.note(
|
|
||||||
isRemote
|
|
||||||
? [
|
|
||||||
"You are running in a remote/VPS environment.",
|
|
||||||
"A URL will be shown for you to open in your LOCAL browser.",
|
|
||||||
"After signing in, paste the redirect URL back here.",
|
|
||||||
].join("\n")
|
|
||||||
: [
|
|
||||||
"Browser will open for OpenAI authentication.",
|
|
||||||
"If the callback doesn't auto-complete, paste the redirect URL.",
|
|
||||||
"OpenAI OAuth uses localhost:1455 for the callback.",
|
|
||||||
].join("\n"),
|
|
||||||
"OpenAI Codex OAuth",
|
|
||||||
);
|
|
||||||
const spin = params.prompter.progress("Starting OAuth flow…");
|
|
||||||
try {
|
try {
|
||||||
const { onAuth, onPrompt } = createVpsAwareOAuthHandlers({
|
creds = await loginOpenAICodexOAuth({
|
||||||
isRemote,
|
|
||||||
prompter: params.prompter,
|
prompter: params.prompter,
|
||||||
runtime: params.runtime,
|
runtime: params.runtime,
|
||||||
spin,
|
isRemote: isRemoteEnvironment(),
|
||||||
openUrl,
|
openUrl: async (url) => {
|
||||||
|
await openUrl(url);
|
||||||
|
},
|
||||||
localBrowserMessage: "Complete sign-in in browser…",
|
localBrowserMessage: "Complete sign-in in browser…",
|
||||||
});
|
});
|
||||||
|
} catch {
|
||||||
const creds = await loginOpenAICodex({
|
// The helper already surfaces the error to the user.
|
||||||
onAuth,
|
// Keep onboarding flow alive and return unchanged config.
|
||||||
onPrompt,
|
return { config: nextConfig, agentModelOverride };
|
||||||
onProgress: (msg) => spin.update(msg),
|
}
|
||||||
|
if (creds) {
|
||||||
|
await writeOAuthCredentials("openai-codex", creds, params.agentDir);
|
||||||
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||||
|
profileId: "openai-codex:default",
|
||||||
|
provider: "openai-codex",
|
||||||
|
mode: "oauth",
|
||||||
});
|
});
|
||||||
spin.stop("OpenAI OAuth complete");
|
if (params.setDefaultModel) {
|
||||||
if (creds) {
|
const applied = applyOpenAICodexModelDefault(nextConfig);
|
||||||
await writeOAuthCredentials("openai-codex", creds, params.agentDir);
|
nextConfig = applied.next;
|
||||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
if (applied.changed) {
|
||||||
profileId: "openai-codex:default",
|
await params.prompter.note(
|
||||||
provider: "openai-codex",
|
`Default model set to ${OPENAI_CODEX_DEFAULT_MODEL}`,
|
||||||
mode: "oauth",
|
"Model configured",
|
||||||
});
|
);
|
||||||
if (params.setDefaultModel) {
|
|
||||||
const applied = applyOpenAICodexModelDefault(nextConfig);
|
|
||||||
nextConfig = applied.next;
|
|
||||||
if (applied.changed) {
|
|
||||||
await params.prompter.note(
|
|
||||||
`Default model set to ${OPENAI_CODEX_DEFAULT_MODEL}`,
|
|
||||||
"Model configured",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
agentModelOverride = OPENAI_CODEX_DEFAULT_MODEL;
|
|
||||||
await noteAgentModel(OPENAI_CODEX_DEFAULT_MODEL);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
agentModelOverride = OPENAI_CODEX_DEFAULT_MODEL;
|
||||||
|
await noteAgentModel(OPENAI_CODEX_DEFAULT_MODEL);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
spin.stop("OpenAI OAuth failed");
|
|
||||||
params.runtime.error(String(err));
|
|
||||||
await params.prompter.note(
|
|
||||||
"Trouble with OAuth? See https://docs.openclaw.ai/start/faq",
|
|
||||||
"OAuth help",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return { config: nextConfig, agentModelOverride };
|
return { config: nextConfig, agentModelOverride };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ vi.mock("../providers/github-copilot-auth.js", () => ({
|
|||||||
githubCopilotLoginCommand: vi.fn(async () => {}),
|
githubCopilotLoginCommand: vi.fn(async () => {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const loginOpenAICodexOAuth = vi.hoisted(() => vi.fn(async () => null));
|
||||||
|
vi.mock("./openai-codex-oauth.js", () => ({
|
||||||
|
loginOpenAICodexOAuth,
|
||||||
|
}));
|
||||||
|
|
||||||
const resolvePluginProviders = vi.hoisted(() => vi.fn(() => []));
|
const resolvePluginProviders = vi.hoisted(() => vi.fn(() => []));
|
||||||
vi.mock("../plugins/providers.js", () => ({
|
vi.mock("../plugins/providers.js", () => ({
|
||||||
resolvePluginProviders,
|
resolvePluginProviders,
|
||||||
@@ -46,6 +51,8 @@ describe("applyAuthChoice", () => {
|
|||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
resolvePluginProviders.mockReset();
|
resolvePluginProviders.mockReset();
|
||||||
|
loginOpenAICodexOAuth.mockReset();
|
||||||
|
loginOpenAICodexOAuth.mockResolvedValue(null);
|
||||||
if (tempStateDir) {
|
if (tempStateDir) {
|
||||||
await fs.rm(tempStateDir, { recursive: true, force: true });
|
await fs.rm(tempStateDir, { recursive: true, force: true });
|
||||||
tempStateDir = null;
|
tempStateDir = null;
|
||||||
@@ -112,6 +119,43 @@ describe("applyAuthChoice", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not throw when openai-codex oauth fails", async () => {
|
||||||
|
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||||
|
process.env.OPENCLAW_STATE_DIR = tempStateDir;
|
||||||
|
process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||||
|
process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR;
|
||||||
|
|
||||||
|
loginOpenAICodexOAuth.mockRejectedValueOnce(new Error("oauth failed"));
|
||||||
|
|
||||||
|
const prompter: WizardPrompter = {
|
||||||
|
intro: vi.fn(noopAsync),
|
||||||
|
outro: vi.fn(noopAsync),
|
||||||
|
note: vi.fn(noopAsync),
|
||||||
|
select: vi.fn(async () => "" as never),
|
||||||
|
multiselect: vi.fn(async () => []),
|
||||||
|
text: vi.fn(async () => ""),
|
||||||
|
confirm: vi.fn(async () => false),
|
||||||
|
progress: vi.fn(() => ({ update: noop, stop: noop })),
|
||||||
|
};
|
||||||
|
const runtime: RuntimeEnv = {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: vi.fn((code: number) => {
|
||||||
|
throw new Error(`exit:${code}`);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
applyAuthChoice({
|
||||||
|
authChoice: "openai-codex",
|
||||||
|
config: {},
|
||||||
|
prompter,
|
||||||
|
runtime,
|
||||||
|
setDefaultModel: false,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual({ config: {} });
|
||||||
|
});
|
||||||
|
|
||||||
it("prompts and writes MiniMax API key when selecting minimax-api", async () => {
|
it("prompts and writes MiniMax API key when selecting minimax-api", async () => {
|
||||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
|
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||||
process.env.OPENCLAW_STATE_DIR = tempStateDir;
|
process.env.OPENCLAW_STATE_DIR = tempStateDir;
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import { isRemoteEnvironment } from "../oauth-env.js";
|
|||||||
import { createVpsAwareOAuthHandlers } from "../oauth-flow.js";
|
import { createVpsAwareOAuthHandlers } from "../oauth-flow.js";
|
||||||
import { applyAuthProfileConfig } from "../onboard-auth.js";
|
import { applyAuthProfileConfig } from "../onboard-auth.js";
|
||||||
import { openUrl } from "../onboard-helpers.js";
|
import { openUrl } from "../onboard-helpers.js";
|
||||||
|
import { OPENAI_CODEX_DEFAULT_MODEL } from "../openai-codex-model-default.js";
|
||||||
|
import { loginOpenAICodexOAuth } from "../openai-codex-oauth.js";
|
||||||
import { updateConfig } from "./shared.js";
|
import { updateConfig } from "./shared.js";
|
||||||
|
|
||||||
const confirm = (params: Parameters<typeof clackConfirm>[0]) =>
|
const confirm = (params: Parameters<typeof clackConfirm>[0]) =>
|
||||||
@@ -342,6 +344,59 @@ export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: Runtim
|
|||||||
const workspaceDir =
|
const workspaceDir =
|
||||||
resolveAgentWorkspaceDir(config, defaultAgentId) ?? resolveDefaultAgentWorkspaceDir();
|
resolveAgentWorkspaceDir(config, defaultAgentId) ?? resolveDefaultAgentWorkspaceDir();
|
||||||
|
|
||||||
|
const prompter = createClackPrompter();
|
||||||
|
const requestedProvider = opts.provider ? normalizeProviderId(opts.provider) : null;
|
||||||
|
if (requestedProvider === "openai-codex") {
|
||||||
|
const method = opts.method?.trim().toLowerCase();
|
||||||
|
if (method && method !== "oauth") {
|
||||||
|
throw new Error('OpenAI Codex auth only supports --method "oauth".');
|
||||||
|
}
|
||||||
|
|
||||||
|
const creds = await loginOpenAICodexOAuth({
|
||||||
|
prompter,
|
||||||
|
runtime,
|
||||||
|
isRemote: isRemoteEnvironment(),
|
||||||
|
openUrl: async (url) => {
|
||||||
|
await openUrl(url);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!creds) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileId = "openai-codex:default";
|
||||||
|
upsertAuthProfile({
|
||||||
|
profileId,
|
||||||
|
credential: {
|
||||||
|
type: "oauth",
|
||||||
|
provider: "openai-codex",
|
||||||
|
...creds,
|
||||||
|
},
|
||||||
|
agentDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateConfig((cfg) => {
|
||||||
|
let next = applyAuthProfileConfig(cfg, {
|
||||||
|
profileId,
|
||||||
|
provider: "openai-codex",
|
||||||
|
mode: "oauth",
|
||||||
|
});
|
||||||
|
if (opts.setDefault) {
|
||||||
|
next = applyDefaultModel(next, OPENAI_CODEX_DEFAULT_MODEL);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
logConfigUpdated(runtime);
|
||||||
|
runtime.log(`Auth profile: ${profileId} (openai-codex/oauth)`);
|
||||||
|
runtime.log(
|
||||||
|
opts.setDefault
|
||||||
|
? `Default model set to ${OPENAI_CODEX_DEFAULT_MODEL}`
|
||||||
|
: `Default model available: ${OPENAI_CODEX_DEFAULT_MODEL} (use --set-default to apply)`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const providers = resolvePluginProviders({ config, workspaceDir });
|
const providers = resolvePluginProviders({ config, workspaceDir });
|
||||||
if (providers.length === 0) {
|
if (providers.length === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -349,7 +404,6 @@ export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: Runtim
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompter = createClackPrompter();
|
|
||||||
const selectedProvider =
|
const selectedProvider =
|
||||||
resolveProviderMatch(providers, opts.provider) ??
|
resolveProviderMatch(providers, opts.provider) ??
|
||||||
(await prompter
|
(await prompter
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
loginOpenAICodex: vi.fn(),
|
||||||
|
createVpsAwareOAuthHandlers: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@mariozechner/pi-ai", () => ({
|
||||||
|
loginOpenAICodex: mocks.loginOpenAICodex,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./oauth-flow.js", () => ({
|
||||||
|
createVpsAwareOAuthHandlers: mocks.createVpsAwareOAuthHandlers,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { loginOpenAICodexOAuth } from "./openai-codex-oauth.js";
|
||||||
|
|
||||||
|
function createPrompter() {
|
||||||
|
const spin = { update: vi.fn(), stop: vi.fn() };
|
||||||
|
const prompter: Pick<WizardPrompter, "note" | "progress"> = {
|
||||||
|
note: vi.fn(async () => {}),
|
||||||
|
progress: vi.fn(() => spin),
|
||||||
|
};
|
||||||
|
return { prompter: prompter as unknown as WizardPrompter, spin };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRuntime(): RuntimeEnv {
|
||||||
|
return {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: vi.fn((code: number) => {
|
||||||
|
throw new Error(`exit:${code}`);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("loginOpenAICodexOAuth", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns credentials on successful oauth login", async () => {
|
||||||
|
const creds = {
|
||||||
|
provider: "openai-codex" as const,
|
||||||
|
access: "access-token",
|
||||||
|
refresh: "refresh-token",
|
||||||
|
expires: Date.now() + 60_000,
|
||||||
|
email: "user@example.com",
|
||||||
|
};
|
||||||
|
mocks.createVpsAwareOAuthHandlers.mockReturnValue({
|
||||||
|
onAuth: vi.fn(),
|
||||||
|
onPrompt: vi.fn(),
|
||||||
|
});
|
||||||
|
mocks.loginOpenAICodex.mockResolvedValue(creds);
|
||||||
|
|
||||||
|
const { prompter, spin } = createPrompter();
|
||||||
|
const runtime = createRuntime();
|
||||||
|
const result = await loginOpenAICodexOAuth({
|
||||||
|
prompter,
|
||||||
|
runtime,
|
||||||
|
isRemote: false,
|
||||||
|
openUrl: async () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(creds);
|
||||||
|
expect(mocks.loginOpenAICodex).toHaveBeenCalledOnce();
|
||||||
|
expect(spin.stop).toHaveBeenCalledWith("OpenAI OAuth complete");
|
||||||
|
expect(runtime.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports oauth errors and rethrows", async () => {
|
||||||
|
mocks.createVpsAwareOAuthHandlers.mockReturnValue({
|
||||||
|
onAuth: vi.fn(),
|
||||||
|
onPrompt: vi.fn(),
|
||||||
|
});
|
||||||
|
mocks.loginOpenAICodex.mockRejectedValue(new Error("oauth failed"));
|
||||||
|
|
||||||
|
const { prompter, spin } = createPrompter();
|
||||||
|
const runtime = createRuntime();
|
||||||
|
await expect(
|
||||||
|
loginOpenAICodexOAuth({
|
||||||
|
prompter,
|
||||||
|
runtime,
|
||||||
|
isRemote: true,
|
||||||
|
openUrl: async () => {},
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("oauth failed");
|
||||||
|
|
||||||
|
expect(spin.stop).toHaveBeenCalledWith("OpenAI OAuth failed");
|
||||||
|
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("oauth failed"));
|
||||||
|
expect(prompter.note).toHaveBeenCalledWith(
|
||||||
|
"Trouble with OAuth? See https://docs.openclaw.ai/start/faq",
|
||||||
|
"OAuth help",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
||||||
|
import { loginOpenAICodex } from "@mariozechner/pi-ai";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
|
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
|
||||||
|
|
||||||
|
export async function loginOpenAICodexOAuth(params: {
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
runtime: RuntimeEnv;
|
||||||
|
isRemote: boolean;
|
||||||
|
openUrl: (url: string) => Promise<void>;
|
||||||
|
localBrowserMessage?: string;
|
||||||
|
}): Promise<OAuthCredentials | null> {
|
||||||
|
const { prompter, runtime, isRemote, openUrl, localBrowserMessage } = params;
|
||||||
|
|
||||||
|
await prompter.note(
|
||||||
|
isRemote
|
||||||
|
? [
|
||||||
|
"You are running in a remote/VPS environment.",
|
||||||
|
"A URL will be shown for you to open in your LOCAL browser.",
|
||||||
|
"After signing in, paste the redirect URL back here.",
|
||||||
|
].join("\n")
|
||||||
|
: [
|
||||||
|
"Browser will open for OpenAI authentication.",
|
||||||
|
"If the callback doesn't auto-complete, paste the redirect URL.",
|
||||||
|
"OpenAI OAuth uses localhost:1455 for the callback.",
|
||||||
|
].join("\n"),
|
||||||
|
"OpenAI Codex OAuth",
|
||||||
|
);
|
||||||
|
|
||||||
|
const spin = prompter.progress("Starting OAuth flow…");
|
||||||
|
try {
|
||||||
|
const { onAuth, onPrompt } = createVpsAwareOAuthHandlers({
|
||||||
|
isRemote,
|
||||||
|
prompter,
|
||||||
|
runtime,
|
||||||
|
spin,
|
||||||
|
openUrl,
|
||||||
|
localBrowserMessage: localBrowserMessage ?? "Complete sign-in in browser…",
|
||||||
|
});
|
||||||
|
|
||||||
|
const creds = await loginOpenAICodex({
|
||||||
|
onAuth,
|
||||||
|
onPrompt,
|
||||||
|
onProgress: (msg) => spin.update(msg),
|
||||||
|
});
|
||||||
|
spin.stop("OpenAI OAuth complete");
|
||||||
|
return creds ?? null;
|
||||||
|
} catch (err) {
|
||||||
|
spin.stop("OpenAI OAuth failed");
|
||||||
|
runtime.error(String(err));
|
||||||
|
await prompter.note("Trouble with OAuth? See https://docs.openclaw.ai/start/faq", "OAuth help");
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user