mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
refactor(doctor): unify legacy auth migration ownership (#117361)
This commit is contained in:
committed by
GitHub
parent
7bd7d7d2a1
commit
3777f009d0
@@ -1,337 +1,152 @@
|
||||
// Doctor auth alias tests cover canonical API-key profile repair and auth-profile store migration.
|
||||
// Historical API-key aliases migrate directly into the receipted SQLite auth owner.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadPersistedAuthProfileStore } from "../agents/auth-profiles/persisted.js";
|
||||
import { clearRuntimeAuthProfileStoreSnapshots } from "../agents/auth-profiles/store.js";
|
||||
import { closeOpenClawAgentDatabasesForTest } from "../state/openclaw-agent-db.js";
|
||||
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
||||
import {
|
||||
createOpenClawTestState,
|
||||
type OpenClawTestState,
|
||||
} from "../test-utils/openclaw-test-state.js";
|
||||
import { maybeRepairCanonicalApiKeyFieldAlias } from "./doctor-auth-flat-profiles.js";
|
||||
import type { DoctorPrompter } from "./doctor-prompter.js";
|
||||
import { maybeMigrateAuthProfileJsonStoresToSqlite } from "./doctor-auth-flat-profiles.js";
|
||||
|
||||
const states: OpenClawTestState[] = [];
|
||||
|
||||
function makePrompter(shouldRepair: boolean): DoctorPrompter {
|
||||
return {
|
||||
confirm: vi.fn(async () => shouldRepair),
|
||||
confirmAutoFix: vi.fn(async () => shouldRepair),
|
||||
confirmAggressiveAutoFix: vi.fn(async () => shouldRepair),
|
||||
confirmRuntimeRepair: vi.fn(async () => shouldRepair),
|
||||
select: vi.fn(async (_params, fallback) => fallback),
|
||||
shouldRepair,
|
||||
shouldForce: false,
|
||||
repairMode: {
|
||||
shouldRepair,
|
||||
shouldForce: false,
|
||||
nonInteractive: false,
|
||||
canPrompt: true,
|
||||
updateInProgress: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
const secretRef = { source: "env", provider: "default", id: "MY_PROVIDER_API_KEY" } as const;
|
||||
|
||||
async function makeTestState(): Promise<OpenClawTestState> {
|
||||
const state = await createOpenClawTestState({
|
||||
layout: "state-only",
|
||||
prefix: "openclaw-doctor-canonical-api-key-",
|
||||
env: {
|
||||
OPENCLAW_AGENT_DIR: undefined,
|
||||
},
|
||||
env: { OPENCLAW_AGENT_DIR: undefined, PI_CODING_AGENT_DIR: undefined },
|
||||
});
|
||||
states.push(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
async function writeLegacyAuthProfilesJson(
|
||||
async function writeProfiles(
|
||||
state: OpenClawTestState,
|
||||
value: unknown,
|
||||
profile: Record<string, unknown>,
|
||||
options: { agentDir?: string; order?: boolean } = {},
|
||||
): Promise<string> {
|
||||
return await state.writeText(
|
||||
"agents/main/agent/auth-profiles.json",
|
||||
`${JSON.stringify(value, null, 2)}\n`,
|
||||
const authPath = path.join(options.agentDir ?? state.agentDir(), "auth-profiles.json");
|
||||
fs.mkdirSync(path.dirname(authPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
`${JSON.stringify({
|
||||
version: 1,
|
||||
profiles: { "my-key": { type: "api_key", provider: "my-provider", ...profile } },
|
||||
...(options.order ? { order: { "my-provider": ["my-key"] } } : {}),
|
||||
})}\n`,
|
||||
);
|
||||
return authPath;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
closeOpenClawAgentDatabasesForTest();
|
||||
closeOpenClawStateDatabaseForTest();
|
||||
for (const state of states.splice(0)) {
|
||||
await state.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
describe("maybeRepairCanonicalApiKeyFieldAlias", () => {
|
||||
it('rewrites the non-canonical "api_key" field to "key" with a backup (57389)', async () => {
|
||||
describe("canonical SQLite migration for historical API-key aliases", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "inline snake-case key",
|
||||
profile: { api_key: "fake-snake-case-key" },
|
||||
expected: { key: "fake-snake-case-key" },
|
||||
},
|
||||
{
|
||||
name: "snake-case SecretRef",
|
||||
profile: { api_key: secretRef },
|
||||
expected: { keyRef: secretRef },
|
||||
},
|
||||
{
|
||||
name: "canonical inline key wins over the stale alias",
|
||||
profile: { key: "fake-canonical-key", api_key: "fake-stale-key" },
|
||||
expected: { key: "fake-canonical-key" },
|
||||
},
|
||||
{
|
||||
name: "canonical keyRef wins over the stale alias",
|
||||
profile: { keyRef: secretRef, api_key: "fake-stale-key" },
|
||||
expected: { keyRef: secretRef },
|
||||
},
|
||||
{
|
||||
name: "canonical inline SecretRef wins over the stale alias",
|
||||
profile: { key: secretRef, api_key: "fake-stale-key" },
|
||||
expected: { keyRef: secretRef },
|
||||
},
|
||||
])("preserves $name and archives untouched source bytes", async ({ profile, expected }) => {
|
||||
const state = await makeTestState();
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
api_key: "sk-snake-case-key",
|
||||
},
|
||||
},
|
||||
order: {
|
||||
"my-provider": ["my-key"],
|
||||
},
|
||||
};
|
||||
const authPath = await writeLegacyAuthProfilesJson(state, canonical);
|
||||
const authPath = await writeProfiles(state, profile, { order: true });
|
||||
const original = fs.readFileSync(authPath);
|
||||
const prompter = { confirmAutoFix: vi.fn(async () => true) };
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
const result = await maybeMigrateAuthProfileJsonStoresToSqlite({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
prompter,
|
||||
env: state.env,
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(result.changes).toStrictEqual([
|
||||
`Rewrote 1 "api_key" field(s) to "key" in ${authPath} (backup: ${authPath}.api-key-alias.123.bak).`,
|
||||
]);
|
||||
expect(result.warnings).toStrictEqual([]);
|
||||
// After the fix: api_key is aliased to the canonical key, other fields untouched.
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
key: "sk-snake-case-key",
|
||||
},
|
||||
},
|
||||
order: {
|
||||
"my-provider": ["my-key"],
|
||||
},
|
||||
expect(result.warnings).toEqual([]);
|
||||
expect(loadPersistedAuthProfileStore(state.agentDir())).toMatchObject({
|
||||
profiles: { "my-key": { type: "api_key", provider: "my-provider", ...expected } },
|
||||
order: { "my-provider": ["my-key"] },
|
||||
});
|
||||
// The backup preserves the original non-canonical shape.
|
||||
expect(JSON.parse(fs.readFileSync(`${authPath}.api-key-alias.123.bak`, "utf8"))).toEqual(
|
||||
canonical,
|
||||
);
|
||||
expect(fs.existsSync(authPath)).toBe(false);
|
||||
const archive = fs
|
||||
.readdirSync(path.dirname(authPath))
|
||||
.find((entry) => entry.startsWith(`${path.basename(authPath)}.migrated-`));
|
||||
expect(archive).toBeDefined();
|
||||
expect(fs.readFileSync(path.join(path.dirname(authPath), archive!))).toEqual(original);
|
||||
|
||||
const rerun = await maybeMigrateAuthProfileJsonStoresToSqlite({
|
||||
cfg: {},
|
||||
prompter,
|
||||
env: state.env,
|
||||
});
|
||||
expect(rerun).toEqual({ detected: [], changes: [], warnings: [] });
|
||||
expect(prompter.confirmAutoFix).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('rewrites non-canonical SecretRef "api_key" fields to canonical "key"', async () => {
|
||||
it.each(["OPENCLAW_AGENT_DIR", "PI_CODING_AGENT_DIR"] as const)(
|
||||
"migrates aliases from the shipped %s agent override",
|
||||
async (agentDirVariable) => {
|
||||
const state = await makeTestState();
|
||||
const agentDir = state.path(`external-${agentDirVariable.toLowerCase()}`);
|
||||
const authPath = await writeProfiles(state, { api_key: "fake-external-key" }, { agentDir });
|
||||
|
||||
const result = await maybeMigrateAuthProfileJsonStoresToSqlite({
|
||||
cfg: {},
|
||||
prompter: { confirmAutoFix: async () => true },
|
||||
env: { ...state.env, [agentDirVariable]: agentDir },
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(loadPersistedAuthProfileStore(agentDir)?.profiles["my-key"]).toMatchObject({
|
||||
key: "fake-external-key",
|
||||
});
|
||||
expect(fs.existsSync(authPath)).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("leaves original credentials untouched when interactive repair is declined", async () => {
|
||||
const state = await makeTestState();
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
api_key: { source: "env", provider: "default", id: "MY_PROVIDER_API_KEY" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const authPath = await writeLegacyAuthProfilesJson(state, canonical);
|
||||
const authPath = await writeProfiles(state, { api_key: "fake-declined-key" });
|
||||
const original = fs.readFileSync(authPath);
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
const result = await maybeMigrateAuthProfileJsonStoresToSqlite({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
prompter: { confirmAutoFix: async () => false },
|
||||
env: state.env,
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(result.changes).toStrictEqual([
|
||||
`Rewrote 1 "api_key" field(s) to "key" in ${authPath} (backup: ${authPath}.api-key-alias.123.bak).`,
|
||||
]);
|
||||
expect(result.warnings).toStrictEqual([]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
key: { source: "env", provider: "default", id: "MY_PROVIDER_API_KEY" },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(JSON.parse(fs.readFileSync(`${authPath}.api-key-alias.123.bak`, "utf8"))).toEqual(
|
||||
canonical,
|
||||
);
|
||||
});
|
||||
|
||||
it("repairs auth profiles from OPENCLAW_AGENT_DIR", async () => {
|
||||
const state = await makeTestState();
|
||||
const agentDir = state.path("external-agent");
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
api_key: "sk-snake-case-key",
|
||||
},
|
||||
},
|
||||
};
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
fs.writeFileSync(authPath, `${JSON.stringify(canonical, null, 2)}\n`, "utf8");
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
env: {
|
||||
...state.env,
|
||||
OPENCLAW_AGENT_DIR: agentDir,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(result.changes).toStrictEqual([
|
||||
`Rewrote 1 "api_key" field(s) to "key" in ${authPath} (backup: ${authPath}.api-key-alias.123.bak).`,
|
||||
]);
|
||||
expect(result.warnings).toStrictEqual([]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
key: "sk-snake-case-key",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("repairs auth profiles from PI_CODING_AGENT_DIR", async () => {
|
||||
const state = await makeTestState();
|
||||
const agentDir = state.path("legacy-external-agent");
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
api_key: "sk-snake-case-key",
|
||||
},
|
||||
},
|
||||
};
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
fs.writeFileSync(authPath, `${JSON.stringify(canonical, null, 2)}\n`, "utf8");
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
env: {
|
||||
...state.env,
|
||||
OPENCLAW_AGENT_DIR: undefined,
|
||||
PI_CODING_AGENT_DIR: agentDir,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8")).profiles["my-key"]).toEqual({
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
key: "sk-snake-case-key",
|
||||
});
|
||||
});
|
||||
|
||||
it('does not touch profiles that already have the canonical "key" field', async () => {
|
||||
const state = await makeTestState();
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"good-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
key: "sk-already-canonical",
|
||||
},
|
||||
},
|
||||
};
|
||||
const authPath = await writeLegacyAuthProfilesJson(state, canonical);
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
});
|
||||
|
||||
expect(result.detected).toStrictEqual([]);
|
||||
expect(result.changes).toStrictEqual([]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual(canonical);
|
||||
expect(fs.existsSync(`${authPath}.api-key-alias.123.bak`)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not replace canonical "keyRef" credentials with stale "api_key" fields', async () => {
|
||||
const state = await makeTestState();
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"ref-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
keyRef: { source: "env", provider: "default", id: "MY_PROVIDER_API_KEY" },
|
||||
api_key: "stale-inline-key",
|
||||
},
|
||||
},
|
||||
};
|
||||
const authPath = await writeLegacyAuthProfilesJson(state, canonical);
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
});
|
||||
|
||||
expect(result.detected).toStrictEqual([]);
|
||||
expect(result.changes).toStrictEqual([]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual(canonical);
|
||||
expect(fs.existsSync(`${authPath}.api-key-alias.123.bak`)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not replace inline canonical SecretRef "key" credentials with stale "api_key" fields', async () => {
|
||||
const state = await makeTestState();
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"inline-ref-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
key: { source: "env", provider: "default", id: "MY_PROVIDER_API_KEY" },
|
||||
api_key: "stale-inline-key",
|
||||
},
|
||||
},
|
||||
};
|
||||
const authPath = await writeLegacyAuthProfilesJson(state, canonical);
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
});
|
||||
|
||||
expect(result.detected).toStrictEqual([]);
|
||||
expect(result.changes).toStrictEqual([]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual(canonical);
|
||||
expect(fs.existsSync(`${authPath}.api-key-alias.123.bak`)).toBe(false);
|
||||
});
|
||||
|
||||
it('reports the non-canonical "api_key" field without rewriting when repair is declined', async () => {
|
||||
const state = await makeTestState();
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
api_key: "sk-snake-case-key",
|
||||
},
|
||||
},
|
||||
};
|
||||
const authPath = await writeLegacyAuthProfilesJson(state, canonical);
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: {},
|
||||
prompter: makePrompter(false),
|
||||
now: () => 123,
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(result.changes).toStrictEqual([]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual(canonical);
|
||||
expect(fs.existsSync(`${authPath}.api-key-alias.123.bak`)).toBe(false);
|
||||
expect(result).toEqual({ detected: [authPath], changes: [], warnings: [] });
|
||||
expect(fs.readFileSync(authPath)).toEqual(original);
|
||||
expect(loadPersistedAuthProfileStore(state.agentDir())).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,9 +26,7 @@ import {
|
||||
import {
|
||||
collectOpenAICodexAuthProfileStoreIdMap,
|
||||
maybeMigrateAuthProfileJsonStoresToSqlite,
|
||||
maybeRepairLegacyFlatAuthProfileStores,
|
||||
maybeRepairOpenAICodexAuthConfig,
|
||||
maybeRepairOpenAICodexAuthProfileStores,
|
||||
} from "./doctor-auth-flat-profiles.js";
|
||||
import type { DoctorPrompter } from "./doctor-prompter.js";
|
||||
|
||||
@@ -1308,8 +1306,8 @@ describe("maybeMigrateAuthProfileJsonStoresToSqlite", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("maybeRepairLegacyFlatAuthProfileStores", () => {
|
||||
it("migrates legacy flat auth-profiles.json stores with a backup", async () => {
|
||||
describe("legacy flat profiles through the canonical auth migration owner", () => {
|
||||
it("migrates legacy flat auth-profiles.json stores with a receipted archive", async () => {
|
||||
const state = await makeTestState();
|
||||
const legacy = {
|
||||
"ollama-windows": {
|
||||
@@ -1319,16 +1317,14 @@ describe("maybeRepairLegacyFlatAuthProfileStores", () => {
|
||||
};
|
||||
const authPath = await writeLegacyAuthProfilesJson(state, legacy);
|
||||
|
||||
const result = await maybeRepairLegacyFlatAuthProfileStores({
|
||||
const result = await maybeMigrateAuthProfileJsonStoresToSqlite({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(result.changes).toStrictEqual([
|
||||
`Migrated ${authPath} to the SQLite auth profile store (backup: ${authPath}.legacy-flat.123.bak).`,
|
||||
]);
|
||||
expect(result.changes).toEqual([expect.stringContaining("Migrated auth profile JSON")]);
|
||||
expect(result.warnings).toStrictEqual([]);
|
||||
expect(loadPersistedAuthProfileStore(state.agentDir())).toEqual({
|
||||
version: 1,
|
||||
@@ -1341,7 +1337,8 @@ describe("maybeRepairLegacyFlatAuthProfileStores", () => {
|
||||
},
|
||||
});
|
||||
expect(fs.existsSync(authPath)).toBe(false);
|
||||
expect(JSON.parse(fs.readFileSync(`${authPath}.legacy-flat.123.bak`, "utf8"))).toEqual(legacy);
|
||||
const [archive] = listMigratedArchives(authPath);
|
||||
expect(JSON.parse(fs.readFileSync(archive!, "utf8"))).toEqual(legacy);
|
||||
});
|
||||
|
||||
it("preserves existing SQLite auth profiles when migrating a legacy flat store", async () => {
|
||||
@@ -1364,16 +1361,14 @@ describe("maybeRepairLegacyFlatAuthProfileStores", () => {
|
||||
const legacy = { openai: { apiKey: "sk-openai-flat" } };
|
||||
const authPath = await writeLegacyAuthProfilesJson(state, legacy);
|
||||
|
||||
const result = await maybeRepairLegacyFlatAuthProfileStores({
|
||||
const result = await maybeMigrateAuthProfileJsonStoresToSqlite({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
});
|
||||
|
||||
expect(result.warnings).toStrictEqual([]);
|
||||
expect(result.changes).toStrictEqual([
|
||||
`Migrated ${authPath} to the SQLite auth profile store (backup: ${authPath}.legacy-flat.123.bak).`,
|
||||
]);
|
||||
expect(result.changes).toEqual([expect.stringContaining("Migrated auth profile JSON")]);
|
||||
expect(loadPersistedAuthProfileStore(state.agentDir())?.profiles).toEqual({
|
||||
"anthropic:default": {
|
||||
type: "oauth",
|
||||
@@ -1400,7 +1395,7 @@ describe("maybeRepairLegacyFlatAuthProfileStores", () => {
|
||||
};
|
||||
const authPath = await writeLegacyAuthProfilesJson(state, legacy);
|
||||
|
||||
const result = await maybeRepairLegacyFlatAuthProfileStores({
|
||||
const result = await maybeMigrateAuthProfileJsonStoresToSqlite({
|
||||
cfg: {},
|
||||
prompter: makePrompter(false),
|
||||
});
|
||||
@@ -1430,15 +1425,16 @@ describe("maybeRepairLegacyFlatAuthProfileStores", () => {
|
||||
const authPath = await writeLegacyAuthProfilesJson(state, legacy);
|
||||
const cfg = {};
|
||||
|
||||
const result = await maybeRepairLegacyFlatAuthProfileStores({
|
||||
const result = await maybeMigrateAuthProfileJsonStoresToSqlite({
|
||||
cfg,
|
||||
prompter: makePrompter(true),
|
||||
now: () => 456,
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(result.changes).toStrictEqual([
|
||||
`Moved aws-sdk profile metadata from ${authPath} to auth.profiles (backup: ${authPath}.aws-sdk-profile.456.bak).`,
|
||||
expect(result.changes).toEqual([
|
||||
expect.stringContaining("Migrated auth profile JSON"),
|
||||
expect.stringContaining("Moved aws-sdk profile metadata"),
|
||||
]);
|
||||
expect(result.warnings).toStrictEqual([]);
|
||||
expect(cfg).toEqual({
|
||||
@@ -1451,19 +1447,16 @@ describe("maybeRepairLegacyFlatAuthProfileStores", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openrouter:default": {
|
||||
type: "api_key",
|
||||
provider: "openrouter",
|
||||
key: "sk-openrouter",
|
||||
},
|
||||
expect(loadPersistedAuthProfileStore(state.agentDir())?.profiles).toEqual({
|
||||
"openrouter:default": {
|
||||
type: "api_key",
|
||||
provider: "openrouter",
|
||||
key: "sk-openrouter",
|
||||
},
|
||||
});
|
||||
expect(JSON.parse(fs.readFileSync(`${authPath}.aws-sdk-profile.456.bak`, "utf8"))).toEqual(
|
||||
legacy,
|
||||
);
|
||||
expect(fs.existsSync(authPath)).toBe(false);
|
||||
const [archive] = listMigratedArchives(authPath);
|
||||
expect(JSON.parse(fs.readFileSync(archive!, "utf8"))).toEqual(legacy);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1780,7 +1773,7 @@ describe("maybeRepairOpenAICodexAuthConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("maybeRepairOpenAICodexAuthProfileStores", () => {
|
||||
describe("legacy OpenAI auth profiles through the canonical migration owner", () => {
|
||||
it("collects the store-derived legacy OpenAI Codex profile id map", async () => {
|
||||
const state = await makeTestState();
|
||||
await writeLegacyAuthProfilesJson(state, {
|
||||
@@ -1811,7 +1804,7 @@ describe("maybeRepairOpenAICodexAuthProfileStores", () => {
|
||||
).toEqual([["openai-codex:default", "openai:chatgpt-default"]]);
|
||||
});
|
||||
|
||||
it("renames legacy OpenAI Codex auth store profiles with a backup", async () => {
|
||||
it("renames legacy OpenAI Codex auth profiles while archiving the untouched source", async () => {
|
||||
const state = await makeTestState();
|
||||
const legacy = {
|
||||
version: 1,
|
||||
@@ -1839,18 +1832,20 @@ describe("maybeRepairOpenAICodexAuthProfileStores", () => {
|
||||
};
|
||||
const authPath = await writeLegacyAuthProfilesJson(state, legacy);
|
||||
|
||||
const result = await maybeRepairOpenAICodexAuthProfileStores({
|
||||
const result = await maybeMigrateAuthProfileJsonStoresToSqlite({
|
||||
cfg: {},
|
||||
env: state.env,
|
||||
prompter: makePrompter(true),
|
||||
now: () => 789,
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(result.changes).toStrictEqual([
|
||||
`Migrated 1 OpenAI Codex auth profile(s) in ${authPath} to provider "openai" (backup: ${authPath}.openai-provider-unification.789.bak).`,
|
||||
expect(result.changes).toEqual([
|
||||
expect.stringContaining("Migrated auth profile JSON"),
|
||||
`Migrated 1 OpenAI Codex auth profile(s) in ${authPath} to provider "openai".`,
|
||||
]);
|
||||
expect(result.warnings).toStrictEqual([]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual({
|
||||
expect(loadPersistedAuthProfileStore(state.agentDir())).toEqual({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:work": {
|
||||
@@ -1874,9 +1869,9 @@ describe("maybeRepairOpenAICodexAuthProfileStores", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
JSON.parse(fs.readFileSync(`${authPath}.openai-provider-unification.789.bak`, "utf8")),
|
||||
).toEqual(legacy);
|
||||
expect(fs.existsSync(authPath)).toBe(false);
|
||||
const [archive] = listMigratedArchives(authPath);
|
||||
expect(JSON.parse(fs.readFileSync(archive!, "utf8"))).toEqual(legacy);
|
||||
});
|
||||
|
||||
it("canonicalizes a mixed Codex store before importing it into SQLite", async () => {
|
||||
@@ -1904,13 +1899,6 @@ describe("maybeRepairOpenAICodexAuthProfileStores", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const providerRepair = await maybeRepairOpenAICodexAuthProfileStores({
|
||||
cfg: {},
|
||||
env: state.env,
|
||||
now: () => 790,
|
||||
});
|
||||
expect(providerRepair.warnings).toStrictEqual([]);
|
||||
|
||||
const sqliteMigration = await maybeMigrateAuthProfileJsonStoresToSqlite({
|
||||
cfg: {},
|
||||
env: state.env,
|
||||
|
||||
@@ -69,12 +69,6 @@ type AuthProfileRepairCandidate = {
|
||||
authPath: string;
|
||||
};
|
||||
|
||||
type LegacyFlatAuthProfileStore = {
|
||||
agentDir?: string;
|
||||
authPath: string;
|
||||
store: AuthProfileStore;
|
||||
};
|
||||
|
||||
type AuthProfileSqliteMigrationCandidate = AuthProfileRepairCandidate & {
|
||||
statePath: string;
|
||||
legacyPath: string;
|
||||
@@ -334,16 +328,12 @@ function listAuthProfileSqliteMigrationCandidates(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): AuthProfileSqliteMigrationCandidate[] {
|
||||
const candidates: AuthProfileSqliteMigrationCandidate[] = [];
|
||||
for (const candidate of listAuthProfileRepairCandidates(cfg, env)) {
|
||||
candidates.push({
|
||||
agentDir: candidate.agentDir,
|
||||
authPath: candidate.authPath,
|
||||
statePath: resolveAuthStatePath(candidate.agentDir),
|
||||
legacyPath: resolveLegacyAuthStorePath(candidate.agentDir),
|
||||
});
|
||||
}
|
||||
return candidates;
|
||||
return listAuthProfileRepairCandidates(cfg, env).map((candidate) => ({
|
||||
agentDir: candidate.agentDir,
|
||||
authPath: candidate.authPath,
|
||||
statePath: resolveAuthStatePath(candidate.agentDir),
|
||||
legacyPath: resolveLegacyAuthStorePath(candidate.agentDir),
|
||||
}));
|
||||
}
|
||||
|
||||
function hasAuthProfileState(state: AuthProfileState): boolean {
|
||||
@@ -363,7 +353,9 @@ function normalizeLegacyApiKeyAliasesForImport(raw: unknown): void {
|
||||
continue;
|
||||
}
|
||||
const hasCanonicalCredential =
|
||||
readNonEmptyString(profile.key) !== undefined || coerceSecretRef(profile.keyRef) !== null;
|
||||
readNonEmptyString(profile.key) !== undefined ||
|
||||
coerceSecretRef(profile.key) !== null ||
|
||||
coerceSecretRef(profile.keyRef) !== null;
|
||||
if (hasCanonicalCredential || profile["api_key"] === undefined) {
|
||||
continue;
|
||||
}
|
||||
@@ -372,19 +364,13 @@ function normalizeLegacyApiKeyAliasesForImport(raw: unknown): void {
|
||||
}
|
||||
|
||||
function collectAuthProfileStateProfileIds(state: AuthProfileState): string[] {
|
||||
const profileIds = new Set<string>();
|
||||
for (const entries of Object.values(state.order ?? {})) {
|
||||
for (const profileId of entries) {
|
||||
profileIds.add(profileId);
|
||||
}
|
||||
}
|
||||
for (const profileId of Object.values(state.lastGood ?? {})) {
|
||||
profileIds.add(profileId);
|
||||
}
|
||||
for (const profileId of Object.keys(state.usageStats ?? {})) {
|
||||
profileIds.add(profileId);
|
||||
}
|
||||
return [...profileIds];
|
||||
return [
|
||||
...new Set([
|
||||
...Object.values(state.order ?? {}).flat(),
|
||||
...Object.values(state.lastGood ?? {}),
|
||||
...Object.keys(state.usageStats ?? {}),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
function inferLegacyConfigAuthProfileMode(
|
||||
@@ -569,45 +555,23 @@ function mergeImportedAuthProfileState(params: {
|
||||
existingState: AuthProfileState;
|
||||
}): AuthProfileStore {
|
||||
// Preserve current SQLite state over imported JSON state; old files are backup-only after import.
|
||||
return {
|
||||
...params.store,
|
||||
...(params.state.order
|
||||
? {
|
||||
order: {
|
||||
...params.store.order,
|
||||
...Object.fromEntries(
|
||||
Object.entries(params.state.order).filter(
|
||||
([provider]) => !params.existingState.order?.[provider],
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(params.state.lastGood
|
||||
? {
|
||||
lastGood: {
|
||||
...params.store.lastGood,
|
||||
...Object.fromEntries(
|
||||
Object.entries(params.state.lastGood).filter(
|
||||
([provider]) => !params.existingState.lastGood?.[provider],
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(params.state.usageStats
|
||||
? {
|
||||
usageStats: {
|
||||
...params.store.usageStats,
|
||||
...Object.fromEntries(
|
||||
Object.entries(params.state.usageStats).filter(
|
||||
([profileId]) => !params.existingState.usageStats?.[profileId],
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const next = { ...params.store };
|
||||
for (const field of ["order", "lastGood", "usageStats"] as const) {
|
||||
const incoming = params.state[field];
|
||||
if (!incoming) {
|
||||
continue;
|
||||
}
|
||||
const existing = params.existingState[field] ?? {};
|
||||
Object.assign(next, {
|
||||
[field]: {
|
||||
...params.store[field],
|
||||
...Object.fromEntries(
|
||||
Object.entries(incoming).filter(([key]) => !Object.hasOwn(existing, key)),
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function formatMissingAuthProfileSqliteVerification(params: {
|
||||
@@ -1089,6 +1053,7 @@ export async function maybeMigrateAuthProfileJsonStoresToSqlite(params: {
|
||||
return result;
|
||||
}
|
||||
|
||||
const openAIProfileIdMap = collectOpenAICodexAuthProfileStoreIdMap({ cfg: params.cfg, env });
|
||||
for (const candidate of detected) {
|
||||
let releaseSources: (() => void) | undefined;
|
||||
try {
|
||||
@@ -1098,41 +1063,16 @@ export async function maybeMigrateAuthProfileJsonStoresToSqlite(params: {
|
||||
}
|
||||
releaseSources = acquireAuthProfileMigrationSourceLocks(candidateSourcePaths);
|
||||
const targetDatabasePath = resolveAuthProfileDatabasePath(candidate.agentDir);
|
||||
let sourceReceipts = [
|
||||
...(fs.existsSync(candidate.authPath)
|
||||
? [
|
||||
prepareAuthProfileSourceReceipt({
|
||||
pathname: candidate.authPath,
|
||||
targetDatabasePath,
|
||||
targetTable: "auth_profile_store",
|
||||
now,
|
||||
env,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(fs.existsSync(candidate.statePath)
|
||||
? [
|
||||
prepareAuthProfileSourceReceipt({
|
||||
pathname: candidate.statePath,
|
||||
targetDatabasePath,
|
||||
targetTable: "auth_profile_state",
|
||||
now,
|
||||
env,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(fs.existsSync(candidate.legacyPath)
|
||||
? [
|
||||
prepareAuthProfileSourceReceipt({
|
||||
pathname: candidate.legacyPath,
|
||||
targetDatabasePath,
|
||||
targetTable: "auth_profile_store",
|
||||
now,
|
||||
env,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
];
|
||||
let sourceReceipts = candidateSourcePaths.filter(fs.existsSync).map((pathname) =>
|
||||
prepareAuthProfileSourceReceipt({
|
||||
pathname,
|
||||
targetDatabasePath,
|
||||
targetTable:
|
||||
pathname === candidate.statePath ? "auth_profile_state" : "auth_profile_store",
|
||||
now,
|
||||
env,
|
||||
}),
|
||||
);
|
||||
sourceReceipts = sourceReceipts.filter(
|
||||
(receipt) => !archivePreviouslyMigratedAuthProfileSource(receipt, result),
|
||||
);
|
||||
@@ -1146,6 +1086,7 @@ export async function maybeMigrateAuthProfileJsonStoresToSqlite(params: {
|
||||
const rawStore = parseAuthProfileMigrationSource(
|
||||
receiptByPath.get(path.resolve(candidate.authPath)),
|
||||
);
|
||||
const openAIProviderRepair = canonicalizeLegacyOpenAIAuthStore(rawStore, openAIProfileIdMap);
|
||||
const unresolvedSidecarProfileIds = new Set(
|
||||
collectUnresolvedLegacyOAuthSidecarProfileIds(rawStore),
|
||||
);
|
||||
@@ -1406,6 +1347,11 @@ export async function maybeMigrateAuthProfileJsonStoresToSqlite(params: {
|
||||
result.changes.push(
|
||||
`Migrated auth profile JSON for ${shortenHomePath(candidate.authPath)} into SQLite (${archiveText}).`,
|
||||
);
|
||||
if (openAIProviderRepair !== null) {
|
||||
result.changes.push(
|
||||
`Migrated ${openAIProviderRepair} OpenAI Codex auth profile(s) in ${shortenHomePath(candidate.authPath)} to provider "openai".`,
|
||||
);
|
||||
}
|
||||
if (awsSdkMarkerStore) {
|
||||
result.changes.push(
|
||||
`Moved aws-sdk profile metadata from ${shortenHomePath(candidate.authPath)} to auth.profiles before removing the legacy auth profile JSON.`,
|
||||
@@ -1448,38 +1394,6 @@ export async function maybeMigrateAuthProfileJsonStoresToSqlite(params: {
|
||||
return result;
|
||||
}
|
||||
|
||||
function resolveLegacyFlatStore(
|
||||
candidate: AuthProfileRepairCandidate,
|
||||
): LegacyFlatAuthProfileStore | null {
|
||||
if (!fs.existsSync(candidate.authPath)) {
|
||||
return null;
|
||||
}
|
||||
const raw = loadJsonFile(candidate.authPath);
|
||||
if (!raw || typeof raw !== "object" || "profiles" in raw) {
|
||||
return null;
|
||||
}
|
||||
const store = coerceLegacyFlatAuthProfileStore(raw);
|
||||
if (!store || Object.keys(store.profiles).length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...candidate,
|
||||
store,
|
||||
};
|
||||
}
|
||||
|
||||
function backupAuthProfileStore(authPath: string, now: () => number): string {
|
||||
const backupPath = `${authPath}.legacy-flat.${now()}.bak`;
|
||||
fs.copyFileSync(authPath, backupPath);
|
||||
return backupPath;
|
||||
}
|
||||
|
||||
function backupAwsSdkProfileMarkerStore(authPath: string, now: () => number): string {
|
||||
const backupPath = `${authPath}.aws-sdk-profile.${now()}.bak`;
|
||||
fs.copyFileSync(authPath, backupPath);
|
||||
return backupPath;
|
||||
}
|
||||
|
||||
function resolveAwsSdkAuthProfileMarkerStore(
|
||||
candidate: AuthProfileRepairCandidate,
|
||||
): AwsSdkAuthProfileMarkerStore | null {
|
||||
@@ -1542,254 +1456,6 @@ function removeAwsSdkProfileMarkers(raw: Record<string, unknown>, profileIds: st
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites pre-versioned flat auth profile JSON into canonical profile stores.
|
||||
*
|
||||
* Also lifts aws-sdk profile markers into config because those entries are routing metadata, not
|
||||
* credentials, and the runtime no longer treats them as stored secrets.
|
||||
*/
|
||||
export async function maybeRepairLegacyFlatAuthProfileStores(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: DoctorPrompter;
|
||||
now?: () => number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<LegacyFlatAuthProfileRepairResult> {
|
||||
const now = params.now ?? Date.now;
|
||||
const env = params.env ?? process.env;
|
||||
const legacyStores = listAuthProfileRepairCandidates(params.cfg, env)
|
||||
.map(resolveLegacyFlatStore)
|
||||
.filter((entry): entry is LegacyFlatAuthProfileStore => entry !== null);
|
||||
const awsSdkMarkerStores = listAuthProfileRepairCandidates(params.cfg, env)
|
||||
.map(resolveAwsSdkAuthProfileMarkerStore)
|
||||
.filter((entry): entry is AwsSdkAuthProfileMarkerStore => entry !== null);
|
||||
|
||||
const result: LegacyFlatAuthProfileRepairResult = {
|
||||
detected: [
|
||||
...legacyStores.map((entry) => entry.authPath),
|
||||
...awsSdkMarkerStores.map((entry) => entry.authPath),
|
||||
],
|
||||
changes: [],
|
||||
warnings: [],
|
||||
};
|
||||
if (legacyStores.length === 0 && awsSdkMarkerStores.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const noteLines = [
|
||||
...legacyStores.map(
|
||||
(entry) => `- ${shortenHomePath(entry.authPath)} uses the legacy flat auth profile format.`,
|
||||
),
|
||||
...awsSdkMarkerStores.map(
|
||||
(entry) =>
|
||||
`- ${shortenHomePath(entry.authPath)} contains aws-sdk profile markers that belong in openclaw.json auth.profiles.`,
|
||||
),
|
||||
];
|
||||
if (legacyStores.length > 0) {
|
||||
noteLines.push(
|
||||
`- The gateway expects the canonical version/profiles store; ${formatCliCommand("openclaw doctor --fix")} rewrites this legacy shape with a backup.`,
|
||||
);
|
||||
}
|
||||
if (awsSdkMarkerStores.length > 0) {
|
||||
noteLines.push(
|
||||
`- AWS SDK profile markers are routing metadata, not stored credentials; ${formatCliCommand("openclaw doctor --fix")} moves them to config with a backup.`,
|
||||
);
|
||||
}
|
||||
note(noteLines.join("\n"), "Auth profiles");
|
||||
|
||||
const shouldRepair = await params.prompter.confirmAutoFix({
|
||||
message: "Repair legacy auth-profiles.json files now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!shouldRepair) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const entry of legacyStores) {
|
||||
try {
|
||||
const existing = loadPersistedAuthProfileStore(entry.agentDir) ?? {
|
||||
version: AUTH_STORE_VERSION,
|
||||
profiles: {},
|
||||
};
|
||||
const importedProfileIds = new Set(Object.keys(entry.store.profiles));
|
||||
const merged = mergeImportedAuthProfiles({
|
||||
store: { ...existing, version: Math.max(existing.version, entry.store.version) },
|
||||
profiles: entry.store.profiles,
|
||||
existingProfileIds: new Set(Object.keys(existing.profiles)),
|
||||
});
|
||||
const backupPath = backupAuthProfileStore(entry.authPath, now);
|
||||
saveAuthProfileStore(merged, entry.agentDir, { syncExternalCli: false });
|
||||
const verificationFailure = formatMissingAuthProfileSqliteVerification({
|
||||
expected: merged,
|
||||
importedProfileIds,
|
||||
loaded: loadPersistedAuthProfileStore(entry.agentDir),
|
||||
});
|
||||
if (verificationFailure) {
|
||||
result.warnings.push(
|
||||
`Left auth profile JSON in place for ${shortenHomePath(entry.authPath)} because SQLite verification did not find ${verificationFailure}.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
fs.unlinkSync(entry.authPath);
|
||||
result.changes.push(
|
||||
`Migrated ${shortenHomePath(entry.authPath)} to the SQLite auth profile store (backup: ${shortenHomePath(backupPath)}).`,
|
||||
);
|
||||
} catch (err) {
|
||||
result.warnings.push(`Failed to rewrite ${shortenHomePath(entry.authPath)}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
for (const entry of awsSdkMarkerStores) {
|
||||
try {
|
||||
const backupPath = backupAwsSdkProfileMarkerStore(entry.authPath, now);
|
||||
const configProfiles = ensureConfigAuthProfiles(params.cfg);
|
||||
for (const marker of entry.profiles) {
|
||||
configProfiles[marker.profileId] = {
|
||||
provider: marker.provider,
|
||||
mode: "aws-sdk",
|
||||
...(marker.email ? { email: marker.email } : {}),
|
||||
...(marker.displayName ? { displayName: marker.displayName } : {}),
|
||||
};
|
||||
}
|
||||
removeAwsSdkProfileMarkers(
|
||||
entry.raw,
|
||||
entry.profiles.map((profile) => profile.profileId),
|
||||
);
|
||||
fs.writeFileSync(entry.authPath, `${JSON.stringify(entry.raw, null, 2)}\n`);
|
||||
result.changes.push(
|
||||
`Moved aws-sdk profile metadata from ${shortenHomePath(entry.authPath)} to auth.profiles (backup: ${shortenHomePath(backupPath)}).`,
|
||||
);
|
||||
} catch (err) {
|
||||
result.warnings.push(
|
||||
`Failed to migrate aws-sdk profile markers from ${shortenHomePath(entry.authPath)}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
if (result.changes.length > 0) {
|
||||
note(result.changes.map((change) => `- ${change}`).join("\n"), "Doctor changes");
|
||||
}
|
||||
if (result.warnings.length > 0) {
|
||||
note(result.warnings.map((warning) => `- ${warning}`).join("\n"), "Doctor warnings");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
type CanonicalApiKeyAliasRepair = {
|
||||
authPath: string;
|
||||
raw: Record<string, unknown>;
|
||||
profileIds: string[];
|
||||
};
|
||||
|
||||
function resolveCanonicalApiKeyAliasRepair(
|
||||
candidate: AuthProfileRepairCandidate,
|
||||
): CanonicalApiKeyAliasRepair | null {
|
||||
if (!fs.existsSync(candidate.authPath)) {
|
||||
return null;
|
||||
}
|
||||
const raw = loadJsonFile(candidate.authPath);
|
||||
if (!isRecord(raw) || !isRecord(raw.profiles)) {
|
||||
return null;
|
||||
}
|
||||
const profileIds: string[] = [];
|
||||
for (const [profileId, value] of Object.entries(raw.profiles)) {
|
||||
if (!isRecord(value)) {
|
||||
continue;
|
||||
}
|
||||
const type = readNonEmptyString(value.type) ?? readNonEmptyString(value.mode);
|
||||
const hasApiKeyField =
|
||||
readNonEmptyString(value["api_key"]) !== undefined ||
|
||||
coerceSecretRef(value["api_key"]) !== null;
|
||||
const hasCanonicalKey =
|
||||
readNonEmptyString(value.key) !== undefined || coerceSecretRef(value.key) !== null;
|
||||
const hasCanonicalKeyRef = coerceSecretRef(value.keyRef) !== null;
|
||||
if (type === "api_key" && hasApiKeyField && !hasCanonicalKey && !hasCanonicalKeyRef) {
|
||||
profileIds.push(profileId);
|
||||
}
|
||||
}
|
||||
return profileIds.length > 0 ? { authPath: candidate.authPath, raw, profileIds } : null;
|
||||
}
|
||||
|
||||
function backupCanonicalApiKeyAlias(authPath: string, now: () => number): string {
|
||||
const backupPath = `${authPath}.api-key-alias.${now()}.bak`;
|
||||
fs.copyFileSync(authPath, backupPath);
|
||||
return backupPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repairs auth profile JSON that used the historical "api_key" credential field.
|
||||
*
|
||||
* Runtime parsing reads "key" or "keyRef"; doctor preserves the original file as a backup before
|
||||
* moving the alias into the canonical key slot.
|
||||
*/
|
||||
export async function maybeRepairCanonicalApiKeyFieldAlias(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: DoctorPrompter;
|
||||
now?: () => number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<LegacyFlatAuthProfileRepairResult> {
|
||||
const now = params.now ?? Date.now;
|
||||
const env = params.env ?? process.env;
|
||||
const repairs = listAuthProfileRepairCandidates(params.cfg, env)
|
||||
.map(resolveCanonicalApiKeyAliasRepair)
|
||||
.filter((entry): entry is CanonicalApiKeyAliasRepair => entry !== null);
|
||||
|
||||
const result: LegacyFlatAuthProfileRepairResult = {
|
||||
detected: repairs.map((entry) => entry.authPath),
|
||||
changes: [],
|
||||
warnings: [],
|
||||
};
|
||||
if (repairs.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const noteLines = repairs.map(
|
||||
(entry) =>
|
||||
`- ${shortenHomePath(entry.authPath)} has ${entry.profileIds.length} profile(s) using the non-canonical "api_key" field; the canonical field is "key".`,
|
||||
);
|
||||
noteLines.push(
|
||||
`- Runtime auth parsing only reads canonical "key" and "keyRef" fields, so these profiles are silently skipped; ${formatCliCommand("openclaw doctor --fix")} rewrites "api_key" to "key" with a backup.`,
|
||||
);
|
||||
note(noteLines.join("\n"), "Auth profiles");
|
||||
|
||||
const shouldRepair = await params.prompter.confirmAutoFix({
|
||||
message: 'Rewrite non-canonical "api_key" fields to "key" now?',
|
||||
initialValue: true,
|
||||
});
|
||||
if (!shouldRepair) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const entry of repairs) {
|
||||
try {
|
||||
const backupPath = backupCanonicalApiKeyAlias(entry.authPath, now);
|
||||
const profiles = entry.raw.profiles as Record<string, Record<string, unknown>>;
|
||||
for (const profileId of entry.profileIds) {
|
||||
const profile = profiles[profileId];
|
||||
if (!isRecord(profile)) {
|
||||
continue;
|
||||
}
|
||||
profile.key = profile["api_key"];
|
||||
delete profile["api_key"];
|
||||
}
|
||||
fs.writeFileSync(entry.authPath, `${JSON.stringify(entry.raw, null, 2)}\n`);
|
||||
result.changes.push(
|
||||
`Rewrote ${entry.profileIds.length} "api_key" field(s) to "key" in ${shortenHomePath(entry.authPath)} (backup: ${shortenHomePath(backupPath)}).`,
|
||||
);
|
||||
} catch (err) {
|
||||
result.warnings.push(
|
||||
`Failed to rewrite "api_key" fields in ${shortenHomePath(entry.authPath)}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
if (result.changes.length > 0) {
|
||||
note(result.changes.map((change) => `- ${change}`).join("\n"), "Doctor changes");
|
||||
}
|
||||
if (result.warnings.length > 0) {
|
||||
note(result.warnings.map((warning) => `- ${warning}`).join("\n"), "Doctor warnings");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const LEGACY_OPENAI_CODEX_PROVIDER_ID = "openai-codex";
|
||||
const OPENAI_PROVIDER_ID = "openai";
|
||||
|
||||
@@ -2072,21 +1738,10 @@ export function maybeRepairOpenAICodexAuthConfig(
|
||||
};
|
||||
}
|
||||
|
||||
type OpenAICodexAuthStoreRepair = {
|
||||
authPath: string;
|
||||
raw: Record<string, unknown>;
|
||||
profileIdMap: Map<string, string>;
|
||||
changed: boolean;
|
||||
};
|
||||
|
||||
function resolveOpenAICodexAuthStoreRepair(
|
||||
candidate: AuthProfileRepairCandidate,
|
||||
profileIdMap?: ReadonlyMap<string, string>,
|
||||
): OpenAICodexAuthStoreRepair | null {
|
||||
if (!fs.existsSync(candidate.authPath)) {
|
||||
return null;
|
||||
}
|
||||
const raw = loadJsonFile(candidate.authPath);
|
||||
function canonicalizeLegacyOpenAIAuthStore(
|
||||
raw: unknown,
|
||||
profileIdMap: ReadonlyMap<string, string>,
|
||||
): number | null {
|
||||
if (!isRecord(raw) || !isRecord(raw.profiles)) {
|
||||
return null;
|
||||
}
|
||||
@@ -2101,14 +1756,8 @@ function resolveOpenAICodexAuthStoreRepair(
|
||||
if (rewrite.profileIdMap.size > 0) {
|
||||
replaceMappedProfileId(raw, rewrite.profileIdMap);
|
||||
}
|
||||
const changed = rewrite.changed || orderChanged || usageChanged || lastGoodChanged;
|
||||
return changed
|
||||
? {
|
||||
authPath: candidate.authPath,
|
||||
raw,
|
||||
profileIdMap: rewrite.profileIdMap,
|
||||
changed,
|
||||
}
|
||||
return rewrite.changed || orderChanged || usageChanged || lastGoodChanged
|
||||
? rewrite.profileIdMap.size
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -2143,49 +1792,4 @@ export function collectOpenAICodexAuthProfileStoreIdMap(params: {
|
||||
return profileIdMap;
|
||||
}
|
||||
|
||||
function backupOpenAIProviderUnification(authPath: string, now: () => number): string {
|
||||
const backupPath = `${authPath}.openai-provider-unification.${now()}.bak`;
|
||||
fs.copyFileSync(authPath, backupPath);
|
||||
return backupPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites legacy OpenAI Codex auth profiles in JSON stores to the canonical OpenAI provider id.
|
||||
*/
|
||||
export async function maybeRepairOpenAICodexAuthProfileStores(params: {
|
||||
cfg: OpenClawConfig;
|
||||
now?: () => number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<LegacyFlatAuthProfileRepairResult> {
|
||||
const now = params.now ?? Date.now;
|
||||
const env = params.env ?? process.env;
|
||||
const profileIdMap = collectOpenAICodexAuthProfileStoreIdMap({ cfg: params.cfg, env });
|
||||
const repairs = listAuthProfileRepairCandidates(params.cfg, env)
|
||||
.map((candidate) => resolveOpenAICodexAuthStoreRepair(candidate, profileIdMap))
|
||||
.filter((entry): entry is OpenAICodexAuthStoreRepair => entry !== null);
|
||||
const result: LegacyFlatAuthProfileRepairResult = {
|
||||
detected: repairs.map((entry) => entry.authPath),
|
||||
changes: [],
|
||||
warnings: [],
|
||||
};
|
||||
if (repairs.length === 0) {
|
||||
return result;
|
||||
}
|
||||
for (const entry of repairs) {
|
||||
try {
|
||||
const backupPath = backupOpenAIProviderUnification(entry.authPath, now);
|
||||
fs.writeFileSync(entry.authPath, `${JSON.stringify(entry.raw, null, 2)}\n`);
|
||||
const movedCount = entry.profileIdMap.size;
|
||||
result.changes.push(
|
||||
`Migrated ${movedCount} OpenAI Codex auth profile(s) in ${shortenHomePath(entry.authPath)} to provider "openai" (backup: ${shortenHomePath(backupPath)}).`,
|
||||
);
|
||||
} catch (err) {
|
||||
result.warnings.push(
|
||||
`Failed to migrate OpenAI Codex auth profiles in ${shortenHomePath(entry.authPath)}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
return result;
|
||||
}
|
||||
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|
||||
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
getNodeSqliteKysely,
|
||||
} from "../infra/kysely-sync.js";
|
||||
import { openNodeSqliteDatabase } from "../infra/node-sqlite.js";
|
||||
import {
|
||||
recordLegacyMigrationRun,
|
||||
recordLegacyMigrationSource,
|
||||
} from "../infra/state-migrations.receipts.js";
|
||||
import type { DB as OpenClawAgentKyselyDatabase } from "../state/openclaw-agent-db.generated.js";
|
||||
import type { DB as OpenClawStateDatabase } from "../state/openclaw-state-db.generated.js";
|
||||
import {
|
||||
@@ -112,47 +116,29 @@ function recordAuthProfileMigrationImported(
|
||||
`auth profile migration source already owned by ${existing.status} receipt`,
|
||||
);
|
||||
}
|
||||
executeSqliteQuerySync(
|
||||
db,
|
||||
kysely
|
||||
.insertInto("migration_runs")
|
||||
.values({
|
||||
id: receipt.runId,
|
||||
started_at: now,
|
||||
finished_at: null,
|
||||
status: "imported",
|
||||
report_json: reportJson(receipt),
|
||||
})
|
||||
.onConflict((conflict) => conflict.column("id").doNothing()),
|
||||
);
|
||||
executeSqliteQuerySync(
|
||||
db,
|
||||
kysely
|
||||
.insertInto("migration_sources")
|
||||
.values({
|
||||
source_key: receipt.sourceKey,
|
||||
migration_kind: MIGRATION_KIND,
|
||||
source_path: receipt.sourcePath,
|
||||
target_table: receipt.targetTable,
|
||||
source_sha256: receipt.sourceSha256,
|
||||
source_size_bytes: receipt.sourceSizeBytes,
|
||||
source_record_count: receipt.sourceRecordCount,
|
||||
last_run_id: receipt.runId,
|
||||
status: "imported",
|
||||
imported_at: now,
|
||||
removed_source: 0,
|
||||
report_json: reportJson(receipt),
|
||||
})
|
||||
.onConflict((conflict) =>
|
||||
conflict.column("source_key").doUpdateSet({
|
||||
last_run_id: receipt.runId,
|
||||
status: "imported",
|
||||
imported_at: now,
|
||||
removed_source: 0,
|
||||
report_json: reportJson(receipt),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const report = reportJson(receipt);
|
||||
recordLegacyMigrationRun(db, {
|
||||
runId: receipt.runId,
|
||||
startedAt: now,
|
||||
finishedAt: null,
|
||||
status: "imported",
|
||||
reportJson: report,
|
||||
upsert: true,
|
||||
});
|
||||
recordLegacyMigrationSource(db, {
|
||||
sourceKey: receipt.sourceKey,
|
||||
migrationKind: MIGRATION_KIND,
|
||||
sourcePath: receipt.sourcePath,
|
||||
targetTable: receipt.targetTable,
|
||||
sourceSha256: receipt.sourceSha256,
|
||||
sourceSizeBytes: receipt.sourceSizeBytes,
|
||||
sourceRecordCount: receipt.sourceRecordCount,
|
||||
runId: receipt.runId,
|
||||
status: "imported",
|
||||
importedAt: now,
|
||||
reportJson: report,
|
||||
upsert: true,
|
||||
});
|
||||
},
|
||||
{ env: receipt.env },
|
||||
);
|
||||
|
||||
@@ -10,14 +10,11 @@ vi.mock("./doctor-bootstrap-size.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./doctor-auth-flat-profiles.js", () => ({
|
||||
maybeRepairCanonicalApiKeyFieldAlias: vi.fn(async (params: { cfg: unknown }) => params.cfg),
|
||||
maybeMigrateAuthProfileJsonStoresToSqlite: vi.fn().mockResolvedValue({
|
||||
changes: [],
|
||||
warnings: [],
|
||||
}),
|
||||
maybeRepairLegacyFlatAuthProfileStores: vi.fn().mockResolvedValue(undefined),
|
||||
maybeRepairOpenAICodexAuthConfig: vi.fn((cfg: unknown) => cfg),
|
||||
maybeRepairOpenAICodexAuthProfileStores: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("./doctor-auth-legacy-oauth.js", () => ({
|
||||
|
||||
@@ -18,7 +18,6 @@ const mocks = vi.hoisted(() => ({
|
||||
migrateLegacyOnboardingRecommendationsScope: vi.fn(),
|
||||
maybeMigrateAuthProfileJsonStoresToSqlite: vi.fn(),
|
||||
maybeRepairOpenAICodexAuthConfig: vi.fn(),
|
||||
maybeRepairOpenAICodexAuthProfileStores: vi.fn(),
|
||||
maybeRepairOpenPolicyAllowFrom: vi.fn(),
|
||||
maybeRepairStaleManagedNpmBundledPlugins: vi.fn(),
|
||||
maybeRepairStaleConfiguredAuthOrders: vi.fn(),
|
||||
@@ -52,7 +51,6 @@ vi.mock("../doctor-auth-flat-profiles.js", () => ({
|
||||
collectOpenAICodexAuthProfileStoreIdMap: vi.fn(() => new Map()),
|
||||
maybeMigrateAuthProfileJsonStoresToSqlite: mocks.maybeMigrateAuthProfileJsonStoresToSqlite,
|
||||
maybeRepairOpenAICodexAuthConfig: mocks.maybeRepairOpenAICodexAuthConfig,
|
||||
maybeRepairOpenAICodexAuthProfileStores: mocks.maybeRepairOpenAICodexAuthProfileStores,
|
||||
}));
|
||||
|
||||
vi.mock("./shared/missing-configured-plugin-install.js", () => ({
|
||||
@@ -267,11 +265,6 @@ describe("doctor repair sequencing", () => {
|
||||
config: cfg,
|
||||
warnings: [],
|
||||
}));
|
||||
mocks.maybeRepairOpenAICodexAuthProfileStores.mockResolvedValue({
|
||||
detected: [],
|
||||
changes: [],
|
||||
warnings: [],
|
||||
});
|
||||
mocks.maybeRepairOpenPolicyAllowFrom.mockImplementation((cfg: OpenClawConfig) => ({
|
||||
config: cfg,
|
||||
changes: [],
|
||||
@@ -565,8 +558,8 @@ describe("doctor repair sequencing", () => {
|
||||
expect(result.authProfilesRepaired).toBe(true);
|
||||
});
|
||||
|
||||
it("reports auth profiles repaired after OpenAI Codex auth-provider migration", async () => {
|
||||
mocks.maybeRepairOpenAICodexAuthProfileStores.mockResolvedValueOnce({
|
||||
it("reports receipt-owned OpenAI auth-provider migration as an auth repair", async () => {
|
||||
mocks.maybeMigrateAuthProfileJsonStoresToSqlite.mockResolvedValueOnce({
|
||||
changes: ["Migrated OpenAI Codex auth-provider profile openai-codex."],
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
collectOpenAICodexAuthProfileStoreIdMap,
|
||||
maybeMigrateAuthProfileJsonStoresToSqlite,
|
||||
maybeRepairOpenAICodexAuthConfig,
|
||||
maybeRepairOpenAICodexAuthProfileStores,
|
||||
} from "../doctor-auth-flat-profiles.js";
|
||||
import { maybeRepairLegacyOAuthSidecarProfiles } from "../doctor-auth-oauth-sidecar.js";
|
||||
import {
|
||||
@@ -244,11 +243,6 @@ export async function runDoctorRepairSequence(params: {
|
||||
env,
|
||||
});
|
||||
appendRepairNotes(legacyOAuthSidecarRepair);
|
||||
const openAIAuthProviderRepair = await maybeRepairOpenAICodexAuthProfileStores({
|
||||
cfg: state.candidate,
|
||||
env,
|
||||
});
|
||||
appendRepairNotes(openAIAuthProviderRepair);
|
||||
const staleOAuthShadowRepair = await repairStaleOAuthProfileShadows({
|
||||
cfg: state.candidate,
|
||||
env,
|
||||
@@ -277,7 +271,6 @@ export async function runDoctorRepairSequence(params: {
|
||||
applyMutation(staleAuthOrderRepair);
|
||||
const authProfilesRepaired =
|
||||
legacyOAuthSidecarRepair.changes.length > 0 ||
|
||||
openAIAuthProviderRepair.changes.length > 0 ||
|
||||
staleOAuthShadowRepair.changes.length > 0 ||
|
||||
authProfileSqliteMigration.changes.length > 0;
|
||||
|
||||
|
||||
@@ -22,8 +22,7 @@ const mocks = vi.hoisted(() => ({
|
||||
maybeRunConfiguredPluginInstallReleaseStep: vi.fn(),
|
||||
registerBundledHealthChecks: vi.fn(),
|
||||
runDoctorHealthRepairs: vi.fn(),
|
||||
maybeRepairLegacyFlatAuthProfileStores: vi.fn().mockResolvedValue(undefined),
|
||||
maybeRepairCanonicalApiKeyFieldAlias: vi.fn().mockResolvedValue(undefined),
|
||||
maybeMigrateAuthProfileJsonStoresToSqlite: vi.fn().mockResolvedValue(undefined),
|
||||
maybeMigrateLegacyPluginModelCatalogs: vi.fn().mockResolvedValue({
|
||||
detected: 0,
|
||||
migrated: 0,
|
||||
@@ -219,8 +218,7 @@ vi.mock("../commands/doctor-gateway-services.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../commands/doctor-auth-flat-profiles.js", () => ({
|
||||
maybeRepairLegacyFlatAuthProfileStores: mocks.maybeRepairLegacyFlatAuthProfileStores,
|
||||
maybeRepairCanonicalApiKeyFieldAlias: mocks.maybeRepairCanonicalApiKeyFieldAlias,
|
||||
maybeMigrateAuthProfileJsonStoresToSqlite: mocks.maybeMigrateAuthProfileJsonStoresToSqlite,
|
||||
}));
|
||||
|
||||
vi.mock("../commands/doctor-plugin-model-catalog.js", () => ({
|
||||
@@ -566,10 +564,8 @@ describe("doctor health contributions", () => {
|
||||
mocks.maybeRunConfiguredPluginInstallReleaseStep.mockReset();
|
||||
mocks.registerBundledHealthChecks.mockReset();
|
||||
mocks.runDoctorHealthRepairs.mockReset();
|
||||
mocks.maybeRepairLegacyFlatAuthProfileStores.mockClear();
|
||||
mocks.maybeRepairLegacyFlatAuthProfileStores.mockResolvedValue(undefined);
|
||||
mocks.maybeRepairCanonicalApiKeyFieldAlias.mockClear();
|
||||
mocks.maybeRepairCanonicalApiKeyFieldAlias.mockResolvedValue(undefined);
|
||||
mocks.maybeMigrateAuthProfileJsonStoresToSqlite.mockClear();
|
||||
mocks.maybeMigrateAuthProfileJsonStoresToSqlite.mockResolvedValue(undefined);
|
||||
mocks.maybeMigrateLegacyPluginModelCatalogs.mockClear();
|
||||
mocks.maybeMigrateLegacyPluginModelCatalogs.mockResolvedValue({
|
||||
detected: 0,
|
||||
@@ -1845,7 +1841,7 @@ describe("doctor health contributions", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps canonical api_key alias repair wired through auth profile health", async () => {
|
||||
it("runs the receipted auth migration after repairing OAuth sidecars", async () => {
|
||||
const contribution = requireDoctorContribution("doctor:auth-profiles");
|
||||
const ctx = {
|
||||
cfg: {},
|
||||
@@ -1857,14 +1853,17 @@ describe("doctor health contributions", () => {
|
||||
|
||||
await contribution.run(ctx);
|
||||
|
||||
expect(mocks.maybeRepairLegacyFlatAuthProfileStores).toHaveBeenCalledWith({
|
||||
expect(mocks.maybeRepairLegacyOAuthSidecarProfiles).toHaveBeenCalledWith({
|
||||
cfg: ctx.cfg,
|
||||
prompter: ctx.prompter,
|
||||
});
|
||||
expect(mocks.maybeRepairCanonicalApiKeyFieldAlias).toHaveBeenCalledWith({
|
||||
expect(mocks.maybeMigrateAuthProfileJsonStoresToSqlite).toHaveBeenCalledWith({
|
||||
cfg: ctx.cfg,
|
||||
prompter: ctx.prompter,
|
||||
});
|
||||
expect(mocks.maybeRepairLegacyOAuthSidecarProfiles.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mocks.maybeMigrateAuthProfileJsonStoresToSqlite.mock.invocationCallOrder[0]!,
|
||||
);
|
||||
expect(mocks.maybeMigrateLegacyPluginModelCatalogs).toHaveBeenCalledWith({
|
||||
cfg: ctx.cfg,
|
||||
prompter: ctx.prompter,
|
||||
|
||||
@@ -54,7 +54,7 @@ async function runGatewayConfigHealth(ctx: DoctorHealthFlowContext): Promise<voi
|
||||
}
|
||||
|
||||
async function runAuthProfileHealth(ctx: DoctorHealthFlowContext): Promise<void> {
|
||||
const { maybeRepairLegacyFlatAuthProfileStores, maybeRepairCanonicalApiKeyFieldAlias } =
|
||||
const { maybeMigrateAuthProfileJsonStoresToSqlite } =
|
||||
await import("../commands/doctor-auth-flat-profiles.js");
|
||||
const { maybeRepairLegacyOAuthProfileIds } =
|
||||
await import("../commands/doctor-auth-legacy-oauth.js");
|
||||
@@ -66,18 +66,15 @@ async function runAuthProfileHealth(ctx: DoctorHealthFlowContext): Promise<void>
|
||||
await import("../commands/doctor-auth.js");
|
||||
const { buildGatewayConnectionDetails } = await import("../gateway/call.js");
|
||||
const { note } = await loadNoteModule();
|
||||
await maybeRepairLegacyFlatAuthProfileStores({
|
||||
cfg: ctx.cfg,
|
||||
prompter: ctx.prompter,
|
||||
});
|
||||
await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: ctx.cfg,
|
||||
prompter: ctx.prompter,
|
||||
});
|
||||
await maybeRepairLegacyOAuthSidecarProfiles({
|
||||
cfg: ctx.cfg,
|
||||
prompter: ctx.prompter,
|
||||
});
|
||||
await maybeMigrateAuthProfileJsonStoresToSqlite({
|
||||
cfg: ctx.cfg,
|
||||
prompter: ctx.prompter,
|
||||
...(ctx.env ? { env: ctx.env } : {}),
|
||||
});
|
||||
await maybeMigrateLegacyPluginModelCatalogs({
|
||||
cfg: ctx.cfg,
|
||||
...(ctx.env ? { env: ctx.env } : {}),
|
||||
|
||||
Reference in New Issue
Block a user