fix(auth): implement true OAuth flow for Anthropic provider#17320
fix(auth): implement true OAuth flow for Anthropic provider#17320miloudbelarebia wants to merge 3 commits intoopenclaw:mainfrom
Conversation
Previously, selecting "oauth" for Anthropic fell through to the setup-token flow, storing credentials as a static token without a refresh token or expiration timestamp. Tokens expired silently and users had to re-authenticate manually. This commit: - Adds `anthropic-oauth.ts` — a thin wrapper around `loginAnthropic()` from @mariozechner/pi-ai, following the same VPS-aware pattern used by the OpenAI Codex OAuth flow. - Separates the "oauth" auth choice from "setup-token"/"token" in `auth-choice.apply.anthropic.ts` so OAuth credentials are persisted via `writeOAuthCredentials()` with `refresh`, `access`, and `expires` fields, enabling automatic token renewal. Closes openclaw#17274 Co-Authored-By: Claude Opus 4.6 <[email protected]>
| await writeOAuthCredentials("anthropic", creds, params.agentDir); | ||
| nextConfig = applyAuthProfileConfig(nextConfig, { | ||
| profileId: "anthropic:default", |
There was a problem hiding this comment.
ProfileId mismatch when creds contain an email
writeOAuthCredentials("anthropic", creds) internally computes the profileId as `anthropic:${creds.email || "default"}` (see onboard-auth.credentials.ts:15-17). If the Anthropic OAuth response includes an email field, the stored profile will be "anthropic:[email protected]" while applyAuthProfileConfig here references "anthropic:default" — causing the config to point to a non-existent profile.
The Chutes OAuth flow (auth-choice.apply.oauth.ts:71-77) handles this correctly by deriving the profileId from creds.email before passing it to applyAuthProfileConfig. Consider doing the same here:
| await writeOAuthCredentials("anthropic", creds, params.agentDir); | |
| nextConfig = applyAuthProfileConfig(nextConfig, { | |
| profileId: "anthropic:default", | |
| const email = | |
| typeof creds.email === "string" && creds.email.trim() ? creds.email.trim() : "default"; | |
| await writeOAuthCredentials("anthropic", creds, params.agentDir); | |
| nextConfig = applyAuthProfileConfig(nextConfig, { | |
| profileId: `anthropic:${email}`, | |
| provider: "anthropic", | |
| mode: "oauth", | |
| }); |
Note: the OpenAI codex flow has the same hardcoded "openai-codex:default" pattern, so if Anthropic OAuth is known to never return an email this may be fine — but it's fragile if the upstream library changes.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/auth-choice.apply.anthropic.ts
Line: 37:39
Comment:
**ProfileId mismatch when creds contain an email**
`writeOAuthCredentials("anthropic", creds)` internally computes the profileId as `` `anthropic:${creds.email || "default"}` `` (see `onboard-auth.credentials.ts:15-17`). If the Anthropic OAuth response includes an `email` field, the stored profile will be `"anthropic:[email protected]"` while `applyAuthProfileConfig` here references `"anthropic:default"` — causing the config to point to a non-existent profile.
The Chutes OAuth flow (`auth-choice.apply.oauth.ts:71-77`) handles this correctly by deriving the profileId from `creds.email` before passing it to `applyAuthProfileConfig`. Consider doing the same here:
```suggestion
const email =
typeof creds.email === "string" && creds.email.trim() ? creds.email.trim() : "default";
await writeOAuthCredentials("anthropic", creds, params.agentDir);
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: `anthropic:${email}`,
provider: "anthropic",
mode: "oauth",
});
```
Note: the OpenAI codex flow has the same hardcoded `"openai-codex:default"` pattern, so if Anthropic OAuth is known to never return an email this may be fine — but it's fragile if the upstream library changes.
How can I resolve this? If you propose a fix, please make it concise.Address Greptile review: writeOAuthCredentials computes the profileId from creds.email internally, so applyAuthProfileConfig must use the same derivation to avoid pointing to a non-existent profile when the OAuth response includes an email. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Split long import line per oxfmt formatting rules. Co-Authored-By: Claude Opus 4.6 <[email protected]>
BEFORE: 500 REST calls to fetch file lists (1 per PR) — 9 runs/hour max. AFTER: 10 GraphQL calls (50 PRs/batch) — 227 runs/hour capacity. - Add GraphQL client to createGitHubClient() - batchFetchFiles() fetches 50 PRs per GraphQL query - checkRateBudget() degrades to semantic-only when budget < 100 - Verified against real PRs: openclaw#17320 (needs-review), openclaw#17296 (likely-duplicate) - Updated docs with verified test results and API budget analysis Co-Authored-By: Claude Opus 4.6 <[email protected]>
bfc1ccb to
f92900f
Compare
…ted beta header - Add cache_control with 1h TTL to both system blocks (dynamic context was entirely uncached) - Verified: 91% cost reduction on cached calls ($0.07 → $0.007 with Haiku) - Remove deprecated structured-outputs beta header (GA since late 2025) - Update PRICING table to reflect 1h cache write costs (2x base input) - Fix effort parameter to Opus-only (Sonnet/Haiku reject it) - Fix oxlint curly brace warning in jaccardSets - Update PR-TRIAGE.md with verified cost data and 1h TTL documentation Tested against real openclaw PRs: Call 1 (Haiku, openclaw#17320): 34,649 cache-write, $0.0728 Call 2 (Haiku, openclaw#17295): 34,649 cache-read, $0.0065 (91% savings) Call 3 (Sonnet, openclaw#17320): structured output works without beta header Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
Thanks for working on this @miloudbelarebia — the root cause analysis is spot on (OAuth falling through to setup-token path). However, a few concerns:
PR #8602 by @maxtongwang covers both the login flow AND the refresh mechanism (with Would it make sense to coordinate with @maxtongwang to avoid duplicate work? The ideal solution needs both: proper OAuth login (your approach) + automatic refresh (from #8602). |
|
@nikolasdehor Thanks for the thorough review — you're right on all points. I wasn't aware of #8602 when I opened this. Looking at it now, @maxtongwang's PR is clearly the more complete solution — it covers both the login flow and the refresh mechanism, with proper tests and all CI passing. Happy to close this PR in favor of #8602. No point in duplicate work when a better implementation already exists. 👍 |
Summary
anthropic-oauth.tswrapper aroundloginAnthropic()from@mariozechner/pi-ai, following the same VPS-aware pattern as OpenAI Codex OAuth"oauth"auth choice from"setup-token"/"token"inauth-choice.apply.anthropic.tswriteOAuthCredentials()withrefresh,access, andexpiresfields, enabling automatic token renewalProblem
When users selected OAuth for Anthropic authentication, the code treated it identically to the
setup-tokenflow (lines 14-17 ofauth-choice.apply.anthropic.ts). This stored credentials astype: "token"without a refresh token or expiration, causing:authentication_error: invalid x-api-keyerrorsSolution
Follow the exact pattern established by
openai-codex-oauth.tsandauth-choice.apply.openai.ts:anthropic-oauth.ts: WrapsloginAnthropic()with VPS-aware handlers viacreateVpsAwareOAuthHandlers(), adapting the callback signatures (Anthropic uses positional args, not an options object)auth-choice.apply.anthropic.ts: The"oauth"choice now triggers a real browser-based OAuth flow and saves credentials withwriteOAuthCredentials("anthropic", creds), storingrefresh,access, andexpiresfieldsTest plan
auth-profiles.jsoncontainstype: "oauth"withrefreshandexpiresfieldsCloses #17274
Local Validation
pnpm check(tsgo): ✅ passespnpm test: ✅ passesoxfmt --checkScope
2 files changed: new
anthropic-oauth.ts+ modifiedauth-choice.apply.anthropic.ts. Follows the existingopenai-codex-oauth.tspattern exactly.AI Assistance
AI-assisted (Claude Code) for codebase exploration and pattern matching. The root cause analysis (OAuth treated as setup-token), solution approach (following OpenAI Codex OAuth pattern), and implementation are my own work.
Testing level: Locally validated — confirmed TypeScript compilation and tests pass.