mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 04:20:24 +00:00
refactor: move oauth profile repair metadata into providers
This commit is contained in:
@@ -3,11 +3,21 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import { maybeRemoveDeprecatedCliAuthProfiles } from "./doctor-auth.js";
|
||||
import {
|
||||
maybeRemoveDeprecatedCliAuthProfiles,
|
||||
maybeRepairLegacyOAuthProfileIds,
|
||||
} from "./doctor-auth.js";
|
||||
import type { DoctorPrompter } from "./doctor-prompter.js";
|
||||
import type { DoctorRepairMode } from "./doctor-repair-mode.js";
|
||||
|
||||
const resolvePluginProvidersMock = vi.fn<() => ProviderPlugin[]>(() => []);
|
||||
|
||||
vi.mock("../plugins/providers.runtime.js", () => ({
|
||||
resolvePluginProviders: () => resolvePluginProvidersMock(),
|
||||
}));
|
||||
|
||||
let envSnapshot: ReturnType<typeof captureEnv>;
|
||||
let tempAgentDir: string | undefined;
|
||||
|
||||
@@ -36,6 +46,8 @@ beforeEach(() => {
|
||||
tempAgentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||
process.env.OPENCLAW_AGENT_DIR = tempAgentDir;
|
||||
process.env.PI_CODING_AGENT_DIR = tempAgentDir;
|
||||
resolvePluginProvidersMock.mockReset();
|
||||
resolvePluginProvidersMock.mockReturnValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -87,6 +99,21 @@ describe("maybeRemoveDeprecatedCliAuthProfiles", () => {
|
||||
"utf8",
|
||||
);
|
||||
|
||||
resolvePluginProvidersMock.mockReturnValue([
|
||||
{
|
||||
id: "anthropic",
|
||||
label: "Anthropic",
|
||||
auth: [],
|
||||
deprecatedProfileIds: ["anthropic:claude-cli"],
|
||||
},
|
||||
{
|
||||
id: "openai-codex",
|
||||
label: "OpenAI Codex",
|
||||
auth: [],
|
||||
deprecatedProfileIds: ["openai-codex:codex-cli"],
|
||||
},
|
||||
]);
|
||||
|
||||
const cfg = {
|
||||
auth: {
|
||||
profiles: {
|
||||
@@ -120,3 +147,67 @@ describe("maybeRemoveDeprecatedCliAuthProfiles", () => {
|
||||
expect(next.auth?.order?.["openai-codex"]).toEqual(["openai-codex:default"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maybeRepairLegacyOAuthProfileIds", () => {
|
||||
it("repairs provider-owned legacy OAuth profile ids", async () => {
|
||||
if (!tempAgentDir) {
|
||||
throw new Error("Missing temp agent dir");
|
||||
}
|
||||
const authPath = path.join(tempAgentDir, "auth-profiles.json");
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:user@example.com": {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "token-a",
|
||||
refresh: "token-r",
|
||||
expires: Date.now() + 60_000,
|
||||
email: "user@example.com",
|
||||
},
|
||||
},
|
||||
lastGood: {
|
||||
anthropic: "anthropic:user@example.com",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
resolvePluginProvidersMock.mockReturnValue([
|
||||
{
|
||||
id: "anthropic",
|
||||
label: "Anthropic",
|
||||
auth: [],
|
||||
oauthProfileIdRepairs: [{ legacyProfileId: "anthropic:default" }],
|
||||
},
|
||||
]);
|
||||
|
||||
const next = await maybeRepairLegacyOAuthProfileIds(
|
||||
{
|
||||
auth: {
|
||||
profiles: {
|
||||
"anthropic:default": { provider: "anthropic", mode: "oauth" },
|
||||
},
|
||||
order: {
|
||||
anthropic: ["anthropic:default"],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
makePrompter(true),
|
||||
);
|
||||
|
||||
expect(next.auth?.profiles?.["anthropic:default"]).toBeUndefined();
|
||||
expect(next.auth?.profiles?.["anthropic:user@example.com"]).toMatchObject({
|
||||
provider: "anthropic",
|
||||
mode: "oauth",
|
||||
email: "user@example.com",
|
||||
});
|
||||
expect(next.auth?.order?.anthropic).toEqual(["anthropic:user@example.com"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,30 +22,42 @@ import {
|
||||
resolveProviderAuthLoginCommand,
|
||||
} from "./provider-auth-guidance.js";
|
||||
|
||||
export async function maybeRepairAnthropicOAuthProfileId(
|
||||
export async function maybeRepairLegacyOAuthProfileIds(
|
||||
cfg: OpenClawConfig,
|
||||
prompter: DoctorPrompter,
|
||||
): Promise<OpenClawConfig> {
|
||||
const store = ensureAuthProfileStore();
|
||||
const repair = repairOAuthProfileIdMismatch({
|
||||
cfg,
|
||||
store,
|
||||
provider: "anthropic",
|
||||
legacyProfileId: "anthropic:default",
|
||||
let nextCfg = cfg;
|
||||
const providers = resolvePluginProviders({
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
});
|
||||
if (!repair.migrated || repair.changes.length === 0) {
|
||||
return cfg;
|
||||
}
|
||||
for (const provider of providers) {
|
||||
for (const repairSpec of provider.oauthProfileIdRepairs ?? []) {
|
||||
const repair = repairOAuthProfileIdMismatch({
|
||||
cfg: nextCfg,
|
||||
store,
|
||||
provider: provider.id,
|
||||
legacyProfileId: repairSpec.legacyProfileId,
|
||||
});
|
||||
if (!repair.migrated || repair.changes.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
note(repair.changes.map((c) => `- ${c}`).join("\n"), "Auth profiles");
|
||||
const apply = await prompter.confirm({
|
||||
message: "Update Anthropic OAuth profile id in config now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!apply) {
|
||||
return cfg;
|
||||
note(repair.changes.map((c) => `- ${c}`).join("\n"), "Auth profiles");
|
||||
const apply = await prompter.confirm({
|
||||
message: `Update ${repairSpec.promptLabel ?? provider.label} OAuth profile id in config now?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (!apply) {
|
||||
continue;
|
||||
}
|
||||
nextCfg = repair.config;
|
||||
}
|
||||
}
|
||||
return repair.config;
|
||||
return nextCfg;
|
||||
}
|
||||
|
||||
function pruneAuthOrder(
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import {
|
||||
maybeRemoveDeprecatedCliAuthProfiles,
|
||||
maybeRepairAnthropicOAuthProfileId,
|
||||
maybeRepairLegacyOAuthProfileIds,
|
||||
noteAuthProfileHealth,
|
||||
} from "../commands/doctor-auth.js";
|
||||
import { noteBootstrapFileSize } from "../commands/doctor-bootstrap-size.js";
|
||||
@@ -136,7 +136,7 @@ async function runGatewayConfigHealth(ctx: DoctorHealthFlowContext): Promise<voi
|
||||
}
|
||||
|
||||
async function runAuthProfileHealth(ctx: DoctorHealthFlowContext): Promise<void> {
|
||||
ctx.cfg = await maybeRepairAnthropicOAuthProfileId(ctx.cfg, ctx.prompter);
|
||||
ctx.cfg = await maybeRepairLegacyOAuthProfileIds(ctx.cfg, ctx.prompter);
|
||||
ctx.cfg = await maybeRemoveDeprecatedCliAuthProfiles(ctx.cfg, ctx.prompter);
|
||||
await noteAuthProfileHealth({
|
||||
cfg: ctx.cfg,
|
||||
|
||||
@@ -41,6 +41,28 @@ function normalizeOnboardingScopes(
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeProviderOAuthProfileIdRepairs(
|
||||
values: ProviderPlugin["oauthProfileIdRepairs"],
|
||||
): ProviderPlugin["oauthProfileIdRepairs"] {
|
||||
if (!Array.isArray(values)) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = values
|
||||
.map((value) => {
|
||||
const legacyProfileId = normalizeText(value?.legacyProfileId);
|
||||
const promptLabel = normalizeText(value?.promptLabel);
|
||||
if (!legacyProfileId && !promptLabel) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...(legacyProfileId ? { legacyProfileId } : {}),
|
||||
...(promptLabel ? { promptLabel } : {}),
|
||||
};
|
||||
})
|
||||
.filter((value): value is NonNullable<typeof value> => value !== null);
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeProviderWizardSetup(params: {
|
||||
providerId: string;
|
||||
pluginId: string;
|
||||
@@ -273,6 +295,9 @@ export function normalizeRegisteredProvider(params: {
|
||||
const docsPath = normalizeText(params.provider.docsPath);
|
||||
const aliases = normalizeTextList(params.provider.aliases);
|
||||
const deprecatedProfileIds = normalizeTextList(params.provider.deprecatedProfileIds);
|
||||
const oauthProfileIdRepairs = normalizeProviderOAuthProfileIdRepairs(
|
||||
params.provider.oauthProfileIdRepairs,
|
||||
);
|
||||
const envVars = normalizeTextList(params.provider.envVars);
|
||||
const wizard = normalizeProviderWizard({
|
||||
providerId: id,
|
||||
@@ -309,6 +334,7 @@ export function normalizeRegisteredProvider(params: {
|
||||
...(docsPath ? { docsPath } : {}),
|
||||
...(aliases ? { aliases } : {}),
|
||||
...(deprecatedProfileIds ? { deprecatedProfileIds } : {}),
|
||||
...(oauthProfileIdRepairs ? { oauthProfileIdRepairs } : {}),
|
||||
...(envVars ? { envVars } : {}),
|
||||
auth,
|
||||
...(catalog ? { catalog } : {}),
|
||||
|
||||
@@ -733,6 +733,21 @@ export type ProviderPluginWizard = {
|
||||
modelPicker?: ProviderPluginWizardModelPicker;
|
||||
};
|
||||
|
||||
export type ProviderOAuthProfileIdRepair = {
|
||||
/**
|
||||
* Legacy OAuth profile id to migrate away from.
|
||||
*
|
||||
* When omitted, OpenClaw falls back to `<provider>:default`.
|
||||
*/
|
||||
legacyProfileId?: string;
|
||||
/**
|
||||
* Optional custom doctor prompt label.
|
||||
*
|
||||
* Defaults to the provider label when omitted.
|
||||
*/
|
||||
promptLabel?: string;
|
||||
};
|
||||
|
||||
export type ProviderModelSelectedContext = {
|
||||
config: OpenClawConfig;
|
||||
model: string;
|
||||
@@ -989,6 +1004,14 @@ export type ProviderPlugin = {
|
||||
* in hardcoded doctor tables.
|
||||
*/
|
||||
deprecatedProfileIds?: string[];
|
||||
/**
|
||||
* Legacy OAuth profile-id migrations that `openclaw doctor` should offer.
|
||||
*
|
||||
* Use this when a provider moved from a legacy default OAuth profile id to a
|
||||
* newer identity-based id and wants doctor to own the config rewrite without
|
||||
* another core-specific migration branch.
|
||||
*/
|
||||
oauthProfileIdRepairs?: ProviderOAuthProfileIdRepair[];
|
||||
/**
|
||||
* Provider-owned OAuth refresh.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user