Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions scripts/setup_anthropic_cred.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""
Two-step Anthropic OAuth credential setup.

Step 1 (no args): Generate auth URL + save verifier
python scripts/setup_anthropic_cred.py

Step 2 (with code): Exchange code for tokens
python scripts/setup_anthropic_cred.py "CODE_FROM_BROWSER"
"""
import sys
import os
import json
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

import asyncio
from pathlib import Path
from rotator_library.providers.anthropic_auth_base import (
_generate_pkce, _build_authorize_url, AnthropicAuthBase
)

STATE_FILE = Path(__file__).parent / ".anthropic_pkce_state.json"
OAUTH_DIR = Path(__file__).parent / ".." / "oauth_creds"

async def exchange_code(auth_code: str):
if not STATE_FILE.exists():
print("Error: PKCE state file not found. Please run Step 1 first.")
sys.exit(1)
state = json.loads(STATE_FILE.read_text())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will raise a FileNotFoundError if Step 2 is run before Step 1. Consider adding a check:

if not STATE_FILE.exists():
    print("Error: PKCE state file not found. Please run Step 1 first.")
    sys.exit(1)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed in 7aa20d4. Added the existence check before reading.

verifier = state["verifier"]

auth = AnthropicAuthBase()
tokens = await auth._exchange_code(auth_code.strip(), verifier)

import time
creds = {
**tokens,
"email": "anthropic-oauth-user",
"_proxy_metadata": {
"email": "anthropic-oauth-user",
"last_check_timestamp": time.time(),
"credential_type": "oauth",
},
}

oauth_dir = OAUTH_DIR.resolve()
oauth_dir.mkdir(parents=True, exist_ok=True)
existing = sorted(oauth_dir.glob("anthropic_oauth_*.json"))
next_num = len(existing) + 1
file_path = oauth_dir / f"anthropic_oauth_{next_num}.json"

file_path.write_text(json.dumps(creds, indent=2))
os.chmod(file_path, 0o600)
STATE_FILE.unlink(missing_ok=True)

print(f"Credential saved to: {file_path}")
print(f"Access token prefix: {tokens['access_token'][:20]}...")

def step1():
verifier, challenge = _generate_pkce()
url = _build_authorize_url(verifier, challenge)
STATE_FILE.write_text(json.dumps({"verifier": verifier, "challenge": challenge}))
print("Open this URL in your browser, authorize, then copy the code:\n")
print(url)
print(f"\nThen run: python scripts/setup_anthropic_cred.py \"PASTE_CODE_HERE\"")

if __name__ == "__main__":
if len(sys.argv) > 1:
asyncio.run(exchange_code(sys.argv[1]))
else:
step1()
3 changes: 2 additions & 1 deletion src/rotator_library/credential_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"qwen_code": Path.home() / ".qwen",
"iflow": Path.home() / ".iflow",
"antigravity": Path.home() / ".antigravity",
# Add other providers like 'claude' here if they have a standard CLI path
"anthropic": Path.home() / ".anthropic",
}

# OAuth providers that support environment variable-based credentials
Expand All @@ -28,6 +28,7 @@
"antigravity": "ANTIGRAVITY",
"qwen_code": "QWEN_CODE",
"iflow": "IFLOW",
"anthropic": "ANTHROPIC_OAUTH",
}


Expand Down
2 changes: 2 additions & 0 deletions src/rotator_library/provider_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
from .providers.qwen_auth_base import QwenAuthBase
from .providers.iflow_auth_base import IFlowAuthBase
from .providers.antigravity_auth_base import AntigravityAuthBase
from .providers.anthropic_auth_base import AnthropicAuthBase

PROVIDER_MAP = {
"gemini_cli": GeminiAuthBase,
"qwen_code": QwenAuthBase,
"iflow": IFlowAuthBase,
"antigravity": AntigravityAuthBase,
"anthropic": AnthropicAuthBase,
}

def get_provider_auth_class(provider_name: str):
Expand Down
Loading
Loading