fix(auth): doctor-migrate anthropic oauth profiles
This commit is contained in:
@@ -51,6 +51,7 @@
|
||||
- Commands: unify native + text chat commands behind `commands.*` config (Discord/Slack/Telegram). Thanks @thewilloftheshadow for PR #275.
|
||||
- Auto-reply: treat steer during compaction as a follow-up, queued until compaction completes.
|
||||
- Auth: lock auth profile refreshes to avoid multi-instance OAuth logouts; keep credentials on refresh failure.
|
||||
- Auth/Doctor: migrate Anthropic OAuth configs from `anthropic:default` → `anthropic:<email>` and surface a doctor hint on refresh failures. Thanks @RandyVentures for PR #361. (#363)
|
||||
- Gateway/CLI: stop forcing localhost URL in remote mode so remote gateway config works. Thanks @oswalpalash for PR #293.
|
||||
- Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins.
|
||||
- Configure: add OpenAI Codex (ChatGPT OAuth) auth choice (align with onboarding).
|
||||
|
||||
@@ -453,5 +453,5 @@ Thanks to all clawtributors:
|
||||
<a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a>
|
||||
<a href="https://github.com/adamgall"><img src="https://avatars.githubusercontent.com/u/706929?v=4&s=48" width="48" height="48" alt="adamgall" title="adamgall"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/regenrek"><img src="https://avatars.githubusercontent.com/u/5182020?v=4&s=48" width="48" height="48" alt="regenrek" title="regenrek"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="tobiasbischoff" title="tobiasbischoff"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a>
|
||||
<a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="Iamadig" title="Iamadig"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a>
|
||||
<a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="ManuelHettich" title="ManuelHettich"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="AbhisekBasu1" title="AbhisekBasu1"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a>
|
||||
<a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="ManuelHettich" title="ManuelHettich"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="AbhisekBasu1" title="AbhisekBasu1"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a>
|
||||
</p>
|
||||
|
||||
@@ -119,11 +119,11 @@ rotation order used for failover.
|
||||
{
|
||||
auth: {
|
||||
profiles: {
|
||||
"anthropic:default": { provider: "anthropic", mode: "oauth", email: "me@example.com" },
|
||||
"anthropic:me@example.com": { provider: "anthropic", mode: "oauth", email: "me@example.com" },
|
||||
"anthropic:work": { provider: "anthropic", mode: "api_key" }
|
||||
},
|
||||
order: {
|
||||
anthropic: ["anthropic:default", "anthropic:work"]
|
||||
anthropic: ["anthropic:me@example.com", "anthropic:work"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import lockfile from "proper-lockfile";
|
||||
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { resolveOAuthPath } from "../config/paths.js";
|
||||
import type { AuthProfileConfig } from "../config/types.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { resolveClawdbotAgentDir } from "./agent-paths.js";
|
||||
import { normalizeProviderId } from "./model-selection.js";
|
||||
@@ -694,10 +695,36 @@ export async function resolveApiKeyForProfile(params: {
|
||||
email: refreshed.email ?? cred.email,
|
||||
};
|
||||
}
|
||||
const fallbackProfileId = suggestOAuthProfileIdForLegacyDefault({
|
||||
cfg,
|
||||
store: refreshedStore,
|
||||
provider: cred.provider,
|
||||
legacyProfileId: profileId,
|
||||
});
|
||||
if (fallbackProfileId && fallbackProfileId !== profileId) {
|
||||
try {
|
||||
const fallbackResolved = await tryResolveOAuthProfile({
|
||||
cfg,
|
||||
store: refreshedStore,
|
||||
profileId: fallbackProfileId,
|
||||
agentDir: params.agentDir,
|
||||
});
|
||||
if (fallbackResolved) return fallbackResolved;
|
||||
} catch {
|
||||
// keep original error
|
||||
}
|
||||
}
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const hint = formatAuthDoctorHint({
|
||||
cfg,
|
||||
store: refreshedStore,
|
||||
provider: cred.provider,
|
||||
profileId,
|
||||
});
|
||||
throw new Error(
|
||||
`OAuth token refresh failed for ${cred.provider}: ${message}. ` +
|
||||
"Please try again or re-authenticate.",
|
||||
"Please try again or re-authenticate." +
|
||||
(hint ? `\n\n${hint}` : ""),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -745,3 +772,234 @@ export function resolveAuthProfileDisplayLabel(params: {
|
||||
if (email) return `${profileId} (${email})`;
|
||||
return profileId;
|
||||
}
|
||||
|
||||
async function tryResolveOAuthProfile(params: {
|
||||
cfg?: ClawdbotConfig;
|
||||
store: AuthProfileStore;
|
||||
profileId: string;
|
||||
agentDir?: string;
|
||||
}): Promise<{ apiKey: string; provider: string; email?: string } | null> {
|
||||
const { cfg, store, profileId } = params;
|
||||
const cred = store.profiles[profileId];
|
||||
if (!cred || cred.type !== "oauth") return null;
|
||||
const profileConfig = cfg?.auth?.profiles?.[profileId];
|
||||
if (profileConfig && profileConfig.provider !== cred.provider) return null;
|
||||
if (profileConfig && profileConfig.mode !== cred.type) return null;
|
||||
|
||||
if (Date.now() < cred.expires) {
|
||||
return {
|
||||
apiKey: buildOAuthApiKey(cred.provider, cred),
|
||||
provider: cred.provider,
|
||||
email: cred.email,
|
||||
};
|
||||
}
|
||||
|
||||
const refreshed = await refreshOAuthTokenWithLock({
|
||||
profileId,
|
||||
provider: cred.provider,
|
||||
agentDir: params.agentDir,
|
||||
});
|
||||
if (!refreshed) return null;
|
||||
return {
|
||||
apiKey: refreshed.apiKey,
|
||||
provider: cred.provider,
|
||||
email: cred.email,
|
||||
};
|
||||
}
|
||||
|
||||
function getProfileSuffix(profileId: string): string {
|
||||
const idx = profileId.indexOf(":");
|
||||
if (idx < 0) return "";
|
||||
return profileId.slice(idx + 1);
|
||||
}
|
||||
|
||||
function isEmailLike(value: string): boolean {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return false;
|
||||
return trimmed.includes("@") && trimmed.includes(".");
|
||||
}
|
||||
|
||||
export function suggestOAuthProfileIdForLegacyDefault(params: {
|
||||
cfg?: ClawdbotConfig;
|
||||
store: AuthProfileStore;
|
||||
provider: string;
|
||||
legacyProfileId: string;
|
||||
}): string | null {
|
||||
const providerKey = normalizeProviderId(params.provider);
|
||||
const legacySuffix = getProfileSuffix(params.legacyProfileId);
|
||||
if (legacySuffix !== "default") return null;
|
||||
|
||||
const legacyCfg = params.cfg?.auth?.profiles?.[params.legacyProfileId];
|
||||
if (
|
||||
legacyCfg &&
|
||||
normalizeProviderId(legacyCfg.provider) === providerKey &&
|
||||
legacyCfg.mode !== "oauth"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const oauthProfiles = listProfilesForProvider(
|
||||
params.store,
|
||||
providerKey,
|
||||
).filter((id) => params.store.profiles[id]?.type === "oauth");
|
||||
if (oauthProfiles.length === 0) return null;
|
||||
|
||||
const configuredEmail = legacyCfg?.email?.trim();
|
||||
if (configuredEmail) {
|
||||
const byEmail = oauthProfiles.find((id) => {
|
||||
const cred = params.store.profiles[id];
|
||||
if (!cred || cred.type !== "oauth") return false;
|
||||
const email = cred.email?.trim();
|
||||
return (
|
||||
email === configuredEmail || id === `${providerKey}:${configuredEmail}`
|
||||
);
|
||||
});
|
||||
if (byEmail) return byEmail;
|
||||
}
|
||||
|
||||
const lastGood =
|
||||
params.store.lastGood?.[providerKey] ??
|
||||
params.store.lastGood?.[params.provider];
|
||||
if (lastGood && oauthProfiles.includes(lastGood)) return lastGood;
|
||||
|
||||
const nonLegacy = oauthProfiles.filter((id) => id !== params.legacyProfileId);
|
||||
if (nonLegacy.length === 1) return nonLegacy[0] ?? null;
|
||||
|
||||
const emailLike = nonLegacy.filter((id) => isEmailLike(getProfileSuffix(id)));
|
||||
if (emailLike.length === 1) return emailLike[0] ?? null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export type AuthProfileIdRepairResult = {
|
||||
config: ClawdbotConfig;
|
||||
changes: string[];
|
||||
migrated: boolean;
|
||||
fromProfileId?: string;
|
||||
toProfileId?: string;
|
||||
};
|
||||
|
||||
export function repairOAuthProfileIdMismatch(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
store: AuthProfileStore;
|
||||
provider: string;
|
||||
legacyProfileId?: string;
|
||||
}): AuthProfileIdRepairResult {
|
||||
const legacyProfileId =
|
||||
params.legacyProfileId ?? `${normalizeProviderId(params.provider)}:default`;
|
||||
const legacyCfg = params.cfg.auth?.profiles?.[legacyProfileId];
|
||||
if (!legacyCfg) {
|
||||
return { config: params.cfg, changes: [], migrated: false };
|
||||
}
|
||||
if (legacyCfg.mode !== "oauth") {
|
||||
return { config: params.cfg, changes: [], migrated: false };
|
||||
}
|
||||
if (
|
||||
normalizeProviderId(legacyCfg.provider) !==
|
||||
normalizeProviderId(params.provider)
|
||||
) {
|
||||
return { config: params.cfg, changes: [], migrated: false };
|
||||
}
|
||||
|
||||
const toProfileId = suggestOAuthProfileIdForLegacyDefault({
|
||||
cfg: params.cfg,
|
||||
store: params.store,
|
||||
provider: params.provider,
|
||||
legacyProfileId,
|
||||
});
|
||||
if (!toProfileId || toProfileId === legacyProfileId) {
|
||||
return { config: params.cfg, changes: [], migrated: false };
|
||||
}
|
||||
|
||||
const toCred = params.store.profiles[toProfileId];
|
||||
const toEmail = toCred?.type === "oauth" ? toCred.email?.trim() : undefined;
|
||||
|
||||
const nextProfiles = {
|
||||
...(params.cfg.auth?.profiles as
|
||||
| Record<string, AuthProfileConfig>
|
||||
| undefined),
|
||||
} as Record<string, AuthProfileConfig>;
|
||||
delete nextProfiles[legacyProfileId];
|
||||
nextProfiles[toProfileId] = {
|
||||
...legacyCfg,
|
||||
...(toEmail ? { email: toEmail } : {}),
|
||||
};
|
||||
|
||||
const providerKey = normalizeProviderId(params.provider);
|
||||
const nextOrder = (() => {
|
||||
const order = params.cfg.auth?.order;
|
||||
if (!order) return undefined;
|
||||
const resolvedKey = Object.keys(order).find(
|
||||
(key) => normalizeProviderId(key) === providerKey,
|
||||
);
|
||||
if (!resolvedKey) return order;
|
||||
const existing = order[resolvedKey];
|
||||
if (!Array.isArray(existing)) return order;
|
||||
const replaced = existing
|
||||
.map((id) => (id === legacyProfileId ? toProfileId : id))
|
||||
.filter(
|
||||
(id): id is string => typeof id === "string" && id.trim().length > 0,
|
||||
);
|
||||
const deduped: string[] = [];
|
||||
for (const entry of replaced) {
|
||||
if (!deduped.includes(entry)) deduped.push(entry);
|
||||
}
|
||||
return { ...order, [resolvedKey]: deduped };
|
||||
})();
|
||||
|
||||
const nextCfg: ClawdbotConfig = {
|
||||
...params.cfg,
|
||||
auth: {
|
||||
...params.cfg.auth,
|
||||
profiles: nextProfiles,
|
||||
...(nextOrder ? { order: nextOrder } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
const changes = [
|
||||
`Auth: migrate ${legacyProfileId} → ${toProfileId} (OAuth profile id)`,
|
||||
];
|
||||
|
||||
return {
|
||||
config: nextCfg,
|
||||
changes,
|
||||
migrated: true,
|
||||
fromProfileId: legacyProfileId,
|
||||
toProfileId,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatAuthDoctorHint(params: {
|
||||
cfg?: ClawdbotConfig;
|
||||
store: AuthProfileStore;
|
||||
provider: string;
|
||||
profileId?: string;
|
||||
}): string {
|
||||
const providerKey = normalizeProviderId(params.provider);
|
||||
if (providerKey !== "anthropic") return "";
|
||||
|
||||
const legacyProfileId = params.profileId ?? "anthropic:default";
|
||||
const suggested = suggestOAuthProfileIdForLegacyDefault({
|
||||
cfg: params.cfg,
|
||||
store: params.store,
|
||||
provider: providerKey,
|
||||
legacyProfileId,
|
||||
});
|
||||
if (!suggested || suggested === legacyProfileId) return "";
|
||||
|
||||
const storeOauthProfiles = listProfilesForProvider(params.store, providerKey)
|
||||
.filter((id) => params.store.profiles[id]?.type === "oauth")
|
||||
.join(", ");
|
||||
|
||||
const cfgMode = params.cfg?.auth?.profiles?.[legacyProfileId]?.mode;
|
||||
const cfgProvider = params.cfg?.auth?.profiles?.[legacyProfileId]?.provider;
|
||||
|
||||
return [
|
||||
"Doctor hint (for GitHub issue):",
|
||||
`- provider: ${providerKey}`,
|
||||
`- config: ${legacyProfileId}${cfgProvider || cfgMode ? ` (provider=${cfgProvider ?? "?"}, mode=${cfgMode ?? "?"})` : ""}`,
|
||||
`- auth store oauth profiles: ${storeOauthProfiles || "(none)"}`,
|
||||
`- suggested profile: ${suggested}`,
|
||||
'Fix: run "clawdbot doctor --yes"',
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -304,6 +304,7 @@ async function promptAuthConfig(
|
||||
profileId,
|
||||
provider: "anthropic",
|
||||
mode: "oauth",
|
||||
email: oauthCreds.email ?? undefined,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -300,6 +300,7 @@ export async function runOnboardingWizard(
|
||||
profileId,
|
||||
provider: "anthropic",
|
||||
mode: "oauth",
|
||||
email: oauthCreds.email ?? undefined,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user