fix(auth): doctor-migrate anthropic oauth profiles

This commit is contained in:
Peter Steinberger
2026-01-07 06:29:43 +00:00
parent ff79db0a99
commit 2937c4861f
8 changed files with 353 additions and 5 deletions

View File

@@ -304,6 +304,7 @@ async function promptAuthConfig(
profileId,
provider: "anthropic",
mode: "oauth",
email: oauthCreds.email ?? undefined,
});
}
} catch (err) {

View File

@@ -40,6 +40,10 @@ const runCommandWithTimeout = vi.fn().mockResolvedValue({
killed: false,
});
const ensureAuthProfileStore = vi
.fn()
.mockReturnValue({ version: 1, profiles: {} });
const legacyReadConfigFileSnapshot = vi.fn().mockResolvedValue({
path: "/tmp/clawdis.json",
exists: false,
@@ -103,6 +107,14 @@ vi.mock("../process/exec.js", () => ({
runCommandWithTimeout,
}));
vi.mock("../agents/auth-profiles.js", async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
ensureAuthProfileStore,
};
});
vi.mock("../daemon/service.js", () => ({
resolveGatewayService: () => ({
label: "LaunchAgent",
@@ -614,4 +626,52 @@ describe("doctor", () => {
expect(serviceRestart).not.toHaveBeenCalled();
expect(confirm).not.toHaveBeenCalled();
});
it("migrates anthropic oauth config profile id when only email profile exists", async () => {
readConfigFileSnapshot.mockResolvedValue({
path: "/tmp/clawdbot.json",
exists: true,
raw: "{}",
parsed: {},
valid: true,
config: {
auth: {
profiles: {
"anthropic:default": { provider: "anthropic", mode: "oauth" },
},
},
},
issues: [],
legacyIssues: [],
});
ensureAuthProfileStore.mockReturnValueOnce({
version: 1,
profiles: {
"anthropic:me@example.com": {
type: "oauth",
provider: "anthropic",
access: "access",
refresh: "refresh",
expires: Date.now() + 60_000,
email: "me@example.com",
},
},
});
const { doctorCommand } = await import("./doctor.js");
await doctorCommand(
{ log: vi.fn(), error: vi.fn(), exit: vi.fn() },
{ yes: true },
);
const written = writeConfigFile.mock.calls.at(-1)?.[0] as Record<
string,
unknown
>;
const profiles = (written.auth as { profiles: Record<string, unknown> })
.profiles;
expect(profiles["anthropic:me@example.com"]).toBeTruthy();
expect(profiles["anthropic:default"]).toBeUndefined();
});
});

View File

@@ -3,7 +3,10 @@ import os from "node:os";
import path from "node:path";
import { confirm, intro, note, outro, select } from "@clack/prompts";
import {
ensureAuthProfileStore,
repairOAuthProfileIdMismatch,
} from "../agents/auth-profiles.js";
import {
DEFAULT_SANDBOX_BROWSER_IMAGE,
DEFAULT_SANDBOX_COMMON_IMAGE,
@@ -386,6 +389,28 @@ function createDoctorPrompter(params: {
};
}
async function maybeRepairAnthropicOAuthProfileId(
cfg: ClawdbotConfig,
prompter: DoctorPrompter,
): Promise<ClawdbotConfig> {
const store = ensureAuthProfileStore();
const repair = repairOAuthProfileIdMismatch({
cfg,
store,
provider: "anthropic",
legacyProfileId: "anthropic:default",
});
if (!repair.migrated || repair.changes.length === 0) return cfg;
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;
return repair.config;
}
const MEMORY_SYSTEM_PROMPT = [
"Memory system not found in workspace.",
"Paste this into your agent:",
@@ -889,6 +914,8 @@ export async function doctorCommand(
cfg = normalized.config;
}
cfg = await maybeRepairAnthropicOAuthProfileId(cfg, prompter);
const legacyState = await detectLegacyStateMigrations({ cfg });
if (legacyState.preview.length > 0) {
note(legacyState.preview.join("\n"), "Legacy state detected");