diff --git a/CHANGELOG.md b/CHANGELOG.md index f2271eb46..0ae254e4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Fixes - Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06. -- Slack: respect `channels.slack.requireMention` default when resolving channel mention gating. (#850) — thanks @evalexpr. +- Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight. ## 2026.1.14 diff --git a/src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.test.ts b/src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.test.ts index dbb4355a9..692b67a01 100644 --- a/src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.test.ts +++ b/src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.test.ts @@ -154,4 +154,81 @@ describe("resolveAuthProfileOrder", () => { }); expect(order).toEqual(["anthropic:work", "anthropic:default"]); }); + + it("mode: oauth config accepts both oauth and token credentials (issue #559)", () => { + const now = Date.now(); + const storeWithBothTypes: AuthProfileStore = { + version: 1, + profiles: { + "anthropic:oauth-cred": { + type: "oauth", + provider: "anthropic", + access: "access-token", + refresh: "refresh-token", + expires: now + 60_000, + }, + "anthropic:token-cred": { + type: "token", + provider: "anthropic", + token: "just-a-token", + expires: now + 60_000, + }, + }, + }; + + const orderOauthCred = resolveAuthProfileOrder({ + store: storeWithBothTypes, + provider: "anthropic", + cfg: { + auth: { + profiles: { + "anthropic:oauth-cred": { provider: "anthropic", mode: "oauth" }, + }, + }, + }, + }); + expect(orderOauthCred).toContain("anthropic:oauth-cred"); + + const orderTokenCred = resolveAuthProfileOrder({ + store: storeWithBothTypes, + provider: "anthropic", + cfg: { + auth: { + profiles: { + "anthropic:token-cred": { provider: "anthropic", mode: "oauth" }, + }, + }, + }, + }); + expect(orderTokenCred).toContain("anthropic:token-cred"); + }); + + it("mode: token config rejects oauth credentials (issue #559 root cause)", () => { + const now = Date.now(); + const storeWithOauth: AuthProfileStore = { + version: 1, + profiles: { + "anthropic:oauth-cred": { + type: "oauth", + provider: "anthropic", + access: "access-token", + refresh: "refresh-token", + expires: now + 60_000, + }, + }, + }; + + const order = resolveAuthProfileOrder({ + store: storeWithOauth, + provider: "anthropic", + cfg: { + auth: { + profiles: { + "anthropic:oauth-cred": { provider: "anthropic", mode: "token" }, + }, + }, + }, + }); + expect(order).not.toContain("anthropic:oauth-cred"); + }); }); diff --git a/src/commands/auth-choice.apply.anthropic.ts b/src/commands/auth-choice.apply.anthropic.ts index b85b4bb9c..64678e51c 100644 --- a/src/commands/auth-choice.apply.anthropic.ts +++ b/src/commands/auth-choice.apply.anthropic.ts @@ -84,7 +84,7 @@ export async function applyAuthChoiceAnthropic( nextConfig = applyAuthProfileConfig(nextConfig, { profileId: CLAUDE_CLI_PROFILE_ID, provider: "anthropic", - mode: "token", + mode: "oauth", }); return { config: nextConfig }; } @@ -146,7 +146,7 @@ export async function applyAuthChoiceAnthropic( nextConfig = applyAuthProfileConfig(nextConfig, { profileId: CLAUDE_CLI_PROFILE_ID, provider: "anthropic", - mode: "token", + mode: "oauth", }); return { config: nextConfig }; } diff --git a/src/commands/models/auth.ts b/src/commands/models/auth.ts index ca0830a09..0b11f1e68 100644 --- a/src/commands/models/auth.ts +++ b/src/commands/models/auth.ts @@ -91,12 +91,12 @@ export async function modelsAuthSetupTokenCommand( applyAuthProfileConfig(cfg, { profileId: CLAUDE_CLI_PROFILE_ID, provider: "anthropic", - mode: "token", + mode: "oauth", }), ); runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`); - runtime.log(`Auth profile: ${CLAUDE_CLI_PROFILE_ID} (anthropic/token)`); + runtime.log(`Auth profile: ${CLAUDE_CLI_PROFILE_ID} (anthropic/oauth)`); } export async function modelsAuthPasteTokenCommand( diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 3c2786264..65cf83bec 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -270,7 +270,7 @@ export async function applyNonInteractiveAuthChoice(params: { return applyAuthProfileConfig(nextConfig, { profileId: CLAUDE_CLI_PROFILE_ID, provider: "anthropic", - mode: "token", + mode: "oauth", }); } diff --git a/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts b/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts index 9379fba72..eb6c8ce2f 100644 --- a/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts +++ b/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts @@ -200,6 +200,46 @@ describe("legacy config detection", () => { } }); }); + it("auto-migrates claude-cli auth profile mode to oauth", async () => { + await withTempHome(async (home) => { + const configPath = path.join(home, ".clawdbot", "clawdbot.json"); + await fs.mkdir(path.dirname(configPath), { recursive: true }); + await fs.writeFile( + configPath, + JSON.stringify( + { + auth: { + profiles: { + "anthropic:claude-cli": { provider: "anthropic", mode: "token" }, + }, + }, + }, + null, + 2, + ), + "utf-8", + ); + + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + vi.resetModules(); + try { + const { loadConfig } = await import("./config.js"); + const cfg = loadConfig(); + expect(cfg.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("oauth"); + + const raw = await fs.readFile(configPath, "utf-8"); + const parsed = JSON.parse(raw) as { + auth?: { profiles?: Record }; + }; + expect(parsed.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("oauth"); + expect( + warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")), + ).toBe(true); + } finally { + warnSpy.mockRestore(); + } + }); + }); it("auto-migrates legacy provider sections on load and writes back", async () => { await withTempHome(async (home) => { const configPath = path.join(home, ".clawdbot", "clawdbot.json"); diff --git a/src/config/legacy.migrations.part-3.ts b/src/config/legacy.migrations.part-3.ts index 6831f8335..1f38fe42e 100644 --- a/src/config/legacy.migrations.part-3.ts +++ b/src/config/legacy.migrations.part-3.ts @@ -10,6 +10,20 @@ import { } from "./legacy.shared.js"; export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [ + { + id: "auth.anthropic-claude-cli-mode-oauth", + describe: "Switch anthropic:claude-cli auth profile mode to oauth", + apply: (raw, changes) => { + const auth = getRecord(raw.auth); + const profiles = getRecord(auth?.profiles); + if (!profiles) return; + const claudeCli = getRecord(profiles["anthropic:claude-cli"]); + if (!claudeCli) return; + if (claudeCli.mode !== "token") return; + claudeCli.mode = "oauth"; + changes.push('Updated auth.profiles["anthropic:claude-cli"].mode → "oauth".'); + }, + }, { id: "agent.defaults-v2", describe: "Move agent config to agents.defaults and tools",