refactor: move legacy auth choice aliases into plugin manifests

This commit is contained in:
Peter Steinberger 2026-03-27 17:22:05 +00:00
parent e25f634d50
commit 2d26f2d876
14 changed files with 174 additions and 56 deletions

View File

@ -11,6 +11,7 @@
"provider": "anthropic",
"method": "cli",
"choiceId": "anthropic-cli",
"deprecatedChoiceIds": ["claude-cli"],
"choiceLabel": "Anthropic Claude CLI",
"choiceHint": "Reuse a local Claude CLI login on this host",
"groupId": "anthropic",

View File

@ -11,6 +11,7 @@
"provider": "openai-codex",
"method": "oauth",
"choiceId": "openai-codex",
"deprecatedChoiceIds": ["codex-cli"],
"choiceLabel": "OpenAI Codex (ChatGPT OAuth)",
"choiceHint": "Browser sign-in",
"groupId": "openai",

View File

@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import {
resolveLegacyAuthChoiceAliasesForCli,
formatDeprecatedNonInteractiveAuthChoiceError,
normalizeLegacyOnboardAuthChoice,
resolveDeprecatedAuthChoiceReplacement,
@ -16,4 +17,13 @@ describe("auth choice legacy aliases", () => {
'Auth choice "claude-cli" is deprecated.\nUse "--auth-choice anthropic-cli".',
);
});
it("sources deprecated cli aliases from plugin manifests", () => {
expect(resolveLegacyAuthChoiceAliasesForCli()).toEqual([
"setup-token",
"oauth",
"claude-cli",
"codex-cli",
]);
});
});

View File

@ -1,53 +1,114 @@
import type { OpenClawConfig } from "../config/config.js";
import {
resolveManifestDeprecatedProviderAuthChoice,
resolveManifestProviderAuthChoices,
} from "../plugins/provider-auth-choices.js";
import type { AuthChoice } from "./onboard-types.js";
export const AUTH_CHOICE_LEGACY_ALIASES_FOR_CLI: ReadonlyArray<AuthChoice> = [
"setup-token",
"oauth",
"claude-cli",
"codex-cli",
];
function resolveLegacyCliBackendChoice(
choice: string,
params?: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
},
) {
if (!choice.endsWith("-cli")) {
return undefined;
}
return resolveManifestDeprecatedProviderAuthChoice(choice, params);
}
function resolveReplacementLabel(choiceLabel: string): string {
return choiceLabel.trim() || "the replacement auth choice";
}
export function resolveLegacyAuthChoiceAliasesForCli(params?: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
}): ReadonlyArray<AuthChoice> {
const manifestCliAliases = resolveManifestProviderAuthChoices(params)
.flatMap((choice) => choice.deprecatedChoiceIds ?? [])
.filter((choice): choice is AuthChoice => choice.endsWith("-cli"))
.toSorted((left, right) => left.localeCompare(right));
return ["setup-token", "oauth", ...manifestCliAliases];
}
export function normalizeLegacyOnboardAuthChoice(
authChoice: AuthChoice | undefined,
params?: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
},
): AuthChoice | undefined {
if (authChoice === "oauth") {
return "setup-token";
}
if (authChoice === "claude-cli") {
return "anthropic-cli";
}
if (authChoice === "codex-cli") {
return "openai-codex";
if (typeof authChoice === "string") {
const deprecatedChoice = resolveLegacyCliBackendChoice(authChoice, params);
if (deprecatedChoice) {
return deprecatedChoice.choiceId as AuthChoice;
}
}
return authChoice;
}
export function isDeprecatedAuthChoice(
authChoice: AuthChoice | undefined,
): authChoice is "claude-cli" | "codex-cli" {
return authChoice === "claude-cli" || authChoice === "codex-cli";
params?: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
},
): authChoice is AuthChoice {
return (
typeof authChoice === "string" && Boolean(resolveLegacyCliBackendChoice(authChoice, params))
);
}
export function resolveDeprecatedAuthChoiceReplacement(authChoice: "claude-cli" | "codex-cli"): {
normalized: AuthChoice;
message: string;
} {
if (authChoice === "claude-cli") {
return {
normalized: "anthropic-cli",
message: 'Auth choice "claude-cli" is deprecated; using Anthropic Claude CLI setup instead.',
};
export function resolveDeprecatedAuthChoiceReplacement(
authChoice: AuthChoice,
params?: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
},
):
| {
normalized: AuthChoice;
message: string;
}
| undefined {
if (typeof authChoice !== "string") {
return undefined;
}
const deprecatedChoice = resolveLegacyCliBackendChoice(authChoice, params);
if (!deprecatedChoice) {
return undefined;
}
const replacementLabel = resolveReplacementLabel(deprecatedChoice.choiceLabel);
return {
normalized: "openai-codex",
message: 'Auth choice "codex-cli" is deprecated; using OpenAI Codex OAuth instead.',
normalized: deprecatedChoice.choiceId as AuthChoice,
message: `Auth choice "${authChoice}" is deprecated; using ${replacementLabel} setup instead.`,
};
}
export function formatDeprecatedNonInteractiveAuthChoiceError(
authChoice: "claude-cli" | "codex-cli",
): string {
const replacement =
authChoice === "claude-cli" ? '"--auth-choice anthropic-cli"' : '"--auth-choice openai-codex"';
return [`Auth choice "${authChoice}" is deprecated.`, `Use ${replacement}.`].join("\n");
authChoice: AuthChoice,
params?: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
},
): string | undefined {
const replacement = resolveDeprecatedAuthChoiceReplacement(authChoice, params);
if (!replacement) {
return undefined;
}
return [
`Auth choice "${authChoice}" is deprecated.`,
`Use "--auth-choice ${replacement.normalized}".`,
].join("\n");
}

View File

@ -1,4 +1,4 @@
import { AUTH_CHOICE_LEGACY_ALIASES_FOR_CLI } from "./auth-choice-legacy.js";
import { resolveLegacyAuthChoiceAliasesForCli } from "./auth-choice-legacy.js";
import type { AuthChoice, AuthChoiceGroupId } from "./onboard-types.js";
export type { AuthChoiceGroupId };
@ -33,6 +33,9 @@ export const CORE_AUTH_CHOICE_OPTIONS: ReadonlyArray<AuthChoiceOption> = [
export function formatStaticAuthChoiceChoicesForCli(params?: {
includeSkip?: boolean;
includeLegacyAliases?: boolean;
config?: import("../config/config.js").OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
}): string {
const includeSkip = params?.includeSkip ?? true;
const includeLegacyAliases = params?.includeLegacyAliases ?? false;
@ -42,7 +45,7 @@ export function formatStaticAuthChoiceChoicesForCli(params?: {
values.push("skip");
}
if (includeLegacyAliases) {
values.push(...AUTH_CHOICE_LEGACY_ALIASES_FOR_CLI);
values.push(...resolveLegacyAuthChoiceAliasesForCli(params));
}
return values.join("|");

View File

@ -271,6 +271,25 @@ describe("buildAuthChoiceOptions", () => {
});
it("can include legacy aliases in cli help choices", () => {
resolveManifestProviderAuthChoices.mockReturnValue([
{
pluginId: "anthropic",
providerId: "anthropic",
methodId: "cli",
choiceId: "anthropic-cli",
choiceLabel: "Anthropic Claude CLI",
deprecatedChoiceIds: ["claude-cli"],
},
{
pluginId: "openai",
providerId: "openai-codex",
methodId: "oauth",
choiceId: "openai-codex",
choiceLabel: "OpenAI Codex (ChatGPT OAuth)",
deprecatedChoiceIds: ["codex-cli"],
},
]);
const cliChoices = formatAuthChoiceChoicesForCli({
includeLegacyAliases: true,
includeSkip: true,

View File

@ -28,7 +28,10 @@ export async function applyAuthChoice(
params: ApplyAuthChoiceParams,
): Promise<ApplyAuthChoiceResult> {
const normalizedAuthChoice =
normalizeLegacyOnboardAuthChoice(params.authChoice) ?? params.authChoice;
normalizeLegacyOnboardAuthChoice(params.authChoice, {
config: params.config,
env: process.env,
}) ?? params.authChoice;
const normalizedProviderAuthChoice = normalizeApiKeyTokenProviderAuthChoice({
authChoice: normalizedAuthChoice,
tokenProvider: params.opts?.tokenProvider,

View File

@ -1,11 +1,15 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const resolveManifestProviderAuthChoice = vi.hoisted(() => vi.fn());
const resolveManifestDeprecatedProviderAuthChoice = vi.hoisted(() => vi.fn());
const resolveManifestProviderAuthChoices = vi.hoisted(() => vi.fn(() => []));
const resolveProviderPluginChoice = vi.hoisted(() => vi.fn());
const resolvePluginProviders = vi.hoisted(() => vi.fn(() => []));
vi.mock("../plugins/provider-auth-choices.js", () => ({
resolveManifestProviderAuthChoice,
resolveManifestDeprecatedProviderAuthChoice,
resolveManifestProviderAuthChoices,
}));
vi.mock("../plugins/provider-wizard.js", () => ({
@ -22,6 +26,8 @@ describe("resolvePreferredProviderForAuthChoice", () => {
beforeEach(() => {
vi.clearAllMocks();
resolveManifestProviderAuthChoice.mockReturnValue(undefined);
resolveManifestDeprecatedProviderAuthChoice.mockReturnValue(undefined);
resolveManifestProviderAuthChoices.mockReturnValue([]);
resolvePluginProviders.mockReturnValue([]);
resolveProviderPluginChoice.mockReturnValue(null);
});
@ -42,25 +48,23 @@ describe("resolvePreferredProviderForAuthChoice", () => {
});
it("normalizes legacy auth choices before plugin lookup", async () => {
resolveProviderPluginChoice.mockReturnValue({
provider: { id: "anthropic", label: "Anthropic", auth: [] },
method: { id: "setup-token", label: "setup-token", kind: "token" },
resolveManifestDeprecatedProviderAuthChoice.mockReturnValue({
choiceId: "anthropic-cli",
choiceLabel: "Anthropic Claude CLI",
});
resolveManifestProviderAuthChoice.mockReturnValue({
pluginId: "anthropic",
providerId: "anthropic",
methodId: "cli",
choiceId: "anthropic-cli",
choiceLabel: "Anthropic Claude CLI",
});
await expect(resolvePreferredProviderForAuthChoice({ choice: "claude-cli" })).resolves.toBe(
"anthropic",
);
expect(resolveProviderPluginChoice).toHaveBeenCalledWith(
expect.objectContaining({
choice: "setup-token",
}),
);
expect(resolvePluginProviders).toHaveBeenCalledWith(
expect.objectContaining({
bundledProviderAllowlistCompat: true,
bundledProviderVitestCompat: true,
}),
);
expect(resolveProviderPluginChoice).not.toHaveBeenCalled();
expect(resolvePluginProviders).not.toHaveBeenCalled();
});
it("falls back to static core choices when no provider plugin claims the choice", async () => {

View File

@ -13,8 +13,10 @@ vi.mock("../api-keys.js", () => ({
}));
const resolveManifestDeprecatedProviderAuthChoice = vi.hoisted(() => vi.fn(() => undefined));
const resolveManifestProviderAuthChoices = vi.hoisted(() => vi.fn(() => []));
vi.mock("../../../plugins/provider-auth-choices.js", () => ({
resolveManifestDeprecatedProviderAuthChoice,
resolveManifestProviderAuthChoices,
}));
beforeEach(() => {

View File

@ -116,8 +116,13 @@ export async function applyNonInteractiveAuthChoice(params: {
...(params.metadata ? { metadata: params.metadata } : {}),
};
};
if (isDeprecatedAuthChoice(authChoice)) {
runtime.error(formatDeprecatedNonInteractiveAuthChoiceError(authChoice));
if (isDeprecatedAuthChoice(authChoice, { config: nextConfig, env: process.env })) {
runtime.error(
formatDeprecatedNonInteractiveAuthChoiceError(authChoice, {
config: nextConfig,
env: process.env,
})!,
);
runtime.exit(1);
return null;
}

View File

@ -7,7 +7,6 @@ export type BuiltInAuthChoice =
// Legacy alias for `setup-token` (kept for backwards CLI compatibility).
| "oauth"
| "setup-token"
| "claude-cli"
| "token"
| "chutes"
| "deepseek-api-key"
@ -25,7 +24,6 @@ export type BuiltInAuthChoice =
| "venice-api-key"
| "together-api-key"
| "huggingface-api-key"
| "codex-cli"
| "apiKey"
| "gemini-api-key"
| "google-gemini-cli"

View File

@ -23,14 +23,22 @@ export async function setupWizardCommand(
) {
assertSupportedRuntime(runtime);
const originalAuthChoice = opts.authChoice;
const normalizedAuthChoice = normalizeLegacyOnboardAuthChoice(originalAuthChoice);
if (opts.nonInteractive && isDeprecatedAuthChoice(originalAuthChoice)) {
runtime.error(formatDeprecatedNonInteractiveAuthChoiceError(originalAuthChoice));
const normalizedAuthChoice = normalizeLegacyOnboardAuthChoice(originalAuthChoice, {
env: process.env,
});
if (opts.nonInteractive && isDeprecatedAuthChoice(originalAuthChoice, { env: process.env })) {
runtime.error(
formatDeprecatedNonInteractiveAuthChoiceError(originalAuthChoice, {
env: process.env,
})!,
);
runtime.exit(1);
return;
}
if (isDeprecatedAuthChoice(originalAuthChoice)) {
runtime.log(resolveDeprecatedAuthChoiceReplacement(originalAuthChoice).message);
if (isDeprecatedAuthChoice(originalAuthChoice, { env: process.env })) {
runtime.log(
resolveDeprecatedAuthChoiceReplacement(originalAuthChoice, { env: process.env })!.message,
);
}
const flow = opts.flow === "manual" ? ("advanced" as const) : opts.flow;
const normalizedOpts =

View File

@ -192,6 +192,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
provider: "anthropic",
method: "cli",
choiceId: "anthropic-cli",
deprecatedChoiceIds: ["claude-cli"],
choiceLabel: "Anthropic Claude CLI",
choiceHint: "Reuse a local Claude CLI login on this host",
groupId: "anthropic",
@ -11374,6 +11375,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
provider: "openai-codex",
method: "oauth",
choiceId: "openai-codex",
deprecatedChoiceIds: ["codex-cli"],
choiceLabel: "OpenAI Codex (ChatGPT OAuth)",
choiceHint: "Browser sign-in",
groupId: "openai",

View File

@ -3,11 +3,12 @@ import type { OpenClawConfig } from "../config/config.js";
import { resolveManifestProviderAuthChoice } from "./provider-auth-choices.js";
const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<string, string>> = {
chutes: "chutes",
"custom-api-key": "custom",
};
function normalizeLegacyAuthChoice(choice: string): string {
return normalizeLegacyOnboardAuthChoice(choice) ?? choice;
return normalizeLegacyOnboardAuthChoice(choice, { env: process.env }) ?? choice;
}
export async function resolvePreferredProviderForAuthChoice(params: {