fix: restore claude cli guidance and doctor behavior

This commit is contained in:
Peter Steinberger
2026-04-06 14:20:51 +01:00
parent 445133b865
commit d378a504ac
23 changed files with 108 additions and 446 deletions

View File

@@ -479,11 +479,11 @@ export function buildAgentSystemPrompt(params: {
// For "none" mode, return just the basic identity line
if (promptMode === "none") {
return "You are a personal assistant running inside OpenClaw.";
return "You are a personal assistant operating inside OpenClaw.";
}
const lines = [
"You are a personal assistant running inside OpenClaw.",
"You are a personal assistant operating inside OpenClaw.",
"",
"## Tooling",
"Structured tool definitions are the source of truth for tool names, descriptions, and parameters.",

View File

@@ -5,10 +5,7 @@ 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,
maybeRepairLegacyOAuthProfileIds,
} from "./doctor-auth.js";
import { maybeRepairLegacyOAuthProfileIds } from "./doctor-auth.js";
import type { DoctorPrompter } from "./doctor-prompter.js";
import type { DoctorRepairMode } from "./doctor-repair-mode.js";
@@ -58,96 +55,6 @@ afterEach(() => {
}
});
describe("maybeRemoveDeprecatedCliAuthProfiles", () => {
it("removes deprecated CLI auth profiles from store + config", 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:claude-cli": {
type: "oauth",
provider: "anthropic",
access: "token-a",
refresh: "token-r",
expires: Date.now() + 60_000,
},
"openai-codex:codex-cli": {
type: "oauth",
provider: "openai-codex",
access: "token-b",
refresh: "token-r2",
expires: Date.now() + 60_000,
},
"openai-codex:default": {
type: "oauth",
provider: "openai-codex",
access: "token-c",
refresh: "token-r3",
expires: Date.now() + 60_000,
},
},
},
null,
2,
)}\n`,
"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: {
"anthropic:claude-cli": { provider: "anthropic", mode: "oauth" },
"openai-codex:codex-cli": { provider: "openai-codex", mode: "oauth" },
"openai-codex:default": { provider: "openai-codex", mode: "oauth" },
},
order: {
anthropic: ["anthropic:claude-cli"],
"openai-codex": ["openai-codex:codex-cli", "openai-codex:default"],
},
},
} as const;
const next = await maybeRemoveDeprecatedCliAuthProfiles(
cfg as unknown as OpenClawConfig,
makePrompter(true),
);
const raw = JSON.parse(fs.readFileSync(authPath, "utf8")) as {
profiles?: Record<string, unknown>;
};
expect(raw.profiles?.["anthropic:claude-cli"]).toBeUndefined();
expect(raw.profiles?.["openai-codex:codex-cli"]).toBeUndefined();
expect(raw.profiles?.["openai-codex:default"]).toBeDefined();
expect(next.auth?.profiles?.["anthropic:claude-cli"]).toBeUndefined();
expect(next.auth?.profiles?.["openai-codex:codex-cli"]).toBeUndefined();
expect(next.auth?.profiles?.["openai-codex:default"]).toBeDefined();
expect(next.auth?.order?.anthropic).toBeUndefined();
expect(next.auth?.order?.["openai-codex"]).toEqual(["openai-codex:default"]);
});
});
describe("maybeRepairLegacyOAuthProfileIds", () => {
it("repairs provider-owned legacy OAuth profile ids", async () => {
if (!tempAgentDir) {

View File

@@ -11,16 +11,11 @@ import {
resolveProfileUnusableUntilForDisplay,
} from "../agents/auth-profiles.js";
import { formatAuthDoctorHint } from "../agents/auth-profiles/doctor.js";
import { updateAuthProfileStoreWithLock } from "../agents/auth-profiles/store.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolvePluginProviders } from "../plugins/providers.runtime.js";
import { note } from "../terminal/note.js";
import type { DoctorPrompter } from "./doctor-prompter.js";
import {
buildProviderAuthRecoveryHint,
resolveProviderAuthLoginCommand,
} from "./provider-auth-guidance.js";
import { buildProviderAuthRecoveryHint } from "./provider-auth-guidance.js";
export async function maybeRepairLegacyOAuthProfileIds(
cfg: OpenClawConfig,
@@ -59,167 +54,6 @@ export async function maybeRepairLegacyOAuthProfileIds(
return nextCfg;
}
function pruneAuthOrder(
order: Record<string, string[]> | undefined,
profileIds: Set<string>,
): { next: Record<string, string[]> | undefined; changed: boolean } {
if (!order) {
return { next: order, changed: false };
}
let changed = false;
const next: Record<string, string[]> = {};
for (const [provider, list] of Object.entries(order)) {
const filtered = list.filter((id) => !profileIds.has(id));
if (filtered.length !== list.length) {
changed = true;
}
if (filtered.length > 0) {
next[provider] = filtered;
}
}
return { next: Object.keys(next).length > 0 ? next : undefined, changed };
}
function pruneAuthProfiles(
cfg: OpenClawConfig,
profileIds: Set<string>,
): { next: OpenClawConfig; changed: boolean } {
const profiles = cfg.auth?.profiles;
const order = cfg.auth?.order;
const nextProfiles = profiles ? { ...profiles } : undefined;
let changed = false;
if (nextProfiles) {
for (const id of profileIds) {
if (id in nextProfiles) {
delete nextProfiles[id];
changed = true;
}
}
}
const prunedOrder = pruneAuthOrder(order, profileIds);
if (prunedOrder.changed) {
changed = true;
}
if (!changed) {
return { next: cfg, changed: false };
}
const nextAuth =
nextProfiles || prunedOrder.next
? {
...cfg.auth,
profiles: nextProfiles && Object.keys(nextProfiles).length > 0 ? nextProfiles : undefined,
order: prunedOrder.next,
}
: undefined;
return {
next: {
...cfg,
auth: nextAuth,
},
changed: true,
};
}
export async function maybeRemoveDeprecatedCliAuthProfiles(
cfg: OpenClawConfig,
prompter: DoctorPrompter,
): Promise<OpenClawConfig> {
const store = ensureAuthProfileStore(undefined, { allowKeychainPrompt: false });
const providers = resolvePluginProviders({
config: cfg,
env: process.env,
mode: "setup",
});
const deprecatedEntries = providers.flatMap((provider) =>
(provider.deprecatedProfileIds ?? [])
.filter((profileId) => store.profiles[profileId] || cfg.auth?.profiles?.[profileId])
.map((profileId) => ({
profileId,
providerId: provider.id,
providerLabel: provider.label,
})),
);
const deprecated = new Set(deprecatedEntries.map((entry) => entry.profileId));
if (deprecated.size === 0) {
return cfg;
}
const lines = ["Deprecated external CLI auth profiles detected (no longer supported):"];
for (const entry of deprecatedEntries) {
const authCommand =
resolveProviderAuthLoginCommand({
provider: entry.providerId,
config: cfg,
env: process.env,
}) ?? formatCliCommand("openclaw configure");
lines.push(`- ${entry.profileId} (${entry.providerLabel}): use ${authCommand}`);
}
note(lines.join("\n"), "Auth profiles");
const shouldRemove = await prompter.confirmAutoFix({
message: "Remove deprecated CLI auth profiles now?",
initialValue: true,
});
if (!shouldRemove) {
return cfg;
}
await updateAuthProfileStoreWithLock({
updater: (nextStore) => {
let mutated = false;
for (const id of deprecated) {
if (nextStore.profiles[id]) {
delete nextStore.profiles[id];
mutated = true;
}
if (nextStore.usageStats?.[id]) {
delete nextStore.usageStats[id];
mutated = true;
}
}
if (nextStore.order) {
for (const [provider, list] of Object.entries(nextStore.order)) {
const filtered = list.filter((id) => !deprecated.has(id));
if (filtered.length !== list.length) {
mutated = true;
if (filtered.length > 0) {
nextStore.order[provider] = filtered;
} else {
delete nextStore.order[provider];
}
}
}
}
if (nextStore.lastGood) {
for (const [provider, profileId] of Object.entries(nextStore.lastGood)) {
if (deprecated.has(profileId)) {
delete nextStore.lastGood[provider];
mutated = true;
}
}
}
return mutated;
},
});
const pruned = pruneAuthProfiles(cfg, deprecated);
if (pruned.changed) {
note(
Array.from(deprecated.values())
.map((id) => `- removed ${id} from config`)
.join("\n"),
"Doctor changes",
);
}
return pruned.next;
}
type AuthIssue = {
profileId: string;
provider: string;

View File

@@ -598,7 +598,7 @@ describe("modelsAuthLoginCommand", () => {
"Anthropic setup-token auth is a legacy/manual path in OpenClaw.",
);
expect(runtime.log).toHaveBeenCalledWith(
"Anthropic told OpenClaw users this path requires Extra Usage on the Claude account.",
"Anthropic staff told us this OpenClaw path is allowed again.",
);
});

View File

@@ -421,9 +421,7 @@ export async function modelsAuthPasteTokenCommand(
runtime.log(`Auth profile: ${profileId} (${provider}/token)`);
if (provider === "anthropic") {
runtime.log("Anthropic setup-token auth is a legacy/manual path in OpenClaw.");
runtime.log(
"Anthropic told OpenClaw users this path requires Extra Usage on the Claude account.",
);
runtime.log("Anthropic staff told us this OpenClaw path is allowed again.");
}
}

View File

@@ -10,7 +10,6 @@ import {
import { runChannelPluginStartupMaintenance } from "../channels/plugins/lifecycle-startup.js";
import { formatCliCommand } from "../cli/command-format.js";
import {
maybeRemoveDeprecatedCliAuthProfiles,
maybeRepairLegacyOAuthProfileIds,
noteAuthProfileHealth,
} from "../commands/doctor-auth.js";
@@ -143,7 +142,6 @@ async function runGatewayConfigHealth(ctx: DoctorHealthFlowContext): Promise<voi
async function runAuthProfileHealth(ctx: DoctorHealthFlowContext): Promise<void> {
ctx.cfg = await maybeRepairLegacyOAuthProfileIds(ctx.cfg, ctx.prompter);
ctx.cfg = await maybeRemoveDeprecatedCliAuthProfiles(ctx.cfg, ctx.prompter);
await noteAuthProfileHealth({
cfg: ctx.cfg,
prompter: ctx.prompter,