Skip to content

Commit dee5343

Browse files
author
Claude Code
committed
feat: add subscription-based OAuth authentication (like pi.dev)
- OAuthManager: Handle OAuth tokens from Claude Pro, ChatGPT Plus, etc - SubscriptionAuthStrategy: Use subscription tokens instead of API keys - AuthManager: Support both API keys AND subscription-based auth - Priority: OAuth subscription → API key → env vars → interactive Usage: # Login to Claude Pro ml-agent login claude # Login to ChatGPT Plus ml-agent login openai # Check subscriptions ml-agent status # Use ml-agent with active subscriptions python3 run_agent.py Supports multiple auth methods: 1. API Keys (pay-as-you-go) 2. Subscription OAuth (Claude Pro, ChatGPT Plus) 3. Environment variables 4. auth.json file Credentials stored locally, auto-refresh on expiry.
1 parent 8836c68 commit dee5343

4 files changed

Lines changed: 371 additions & 3 deletions

File tree

.gates/hook-log.jsonl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,3 +1033,12 @@
10331033
{"ts": "2026-07-23T21:23:41.719488+00:00", "gate": "pr_structure", "tool": "Bash", "decision": "allow", "reason": "not a gh pr create"}
10341034
{"ts": "2026-07-23T21:23:41.726470+00:00", "gate": "metadata", "tool": "Bash", "decision": "allow", "reason": "not a git commit"}
10351035
{"ts": "2026-07-23T21:23:41.737085+00:00", "gate": "file-size", "tool": "", "decision": "allow", "reason": "all 0 staged files within 150-line limit"}
1036+
{"ts": "2026-07-23T21:23:44.278320+00:00", "gate": "file-size", "tool": "", "decision": "allow", "reason": "not a git commit"}
1037+
{"ts": "2026-07-23T21:23:44.281000+00:00", "gate": "pr_structure", "tool": "Bash", "decision": "allow", "reason": "not a gh pr create"}
1038+
{"ts": "2026-07-23T21:23:44.292392+00:00", "gate": "metadata", "tool": "Bash", "decision": "allow", "reason": "not a git commit"}
1039+
{"ts": "2026-07-23T22:04:29.897272+00:00", "gate": "role", "tool": "Write", "decision": "allow", "reason": "CLAUDE_ACTIVE_SPECIALIST not set — permissive mode"}
1040+
{"ts": "2026-07-23T22:04:43.763404+00:00", "gate": "role", "tool": "Edit", "decision": "allow", "reason": "CLAUDE_ACTIVE_SPECIALIST not set — permissive mode"}
1041+
{"ts": "2026-07-23T22:04:59.787609+00:00", "gate": "role", "tool": "Write", "decision": "allow", "reason": "CLAUDE_ACTIVE_SPECIALIST not set — permissive mode"}
1042+
{"ts": "2026-07-23T22:05:04.763901+00:00", "gate": "pr_structure", "tool": "Bash", "decision": "allow", "reason": "not a gh pr create"}
1043+
{"ts": "2026-07-23T22:05:04.786149+00:00", "gate": "metadata", "tool": "Bash", "decision": "allow", "reason": "not a git commit"}
1044+
{"ts": "2026-07-23T22:05:04.800465+00:00", "gate": "file-size", "tool": "", "decision": "allow", "reason": "all 0 staged files within 150-line limit"}

AUTH_METHODS.md

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# 🔐 Authentication Methods
2+
3+
ML-Agent supports **both API keys AND subscription-based OAuth** (like pi.dev).
4+
5+
---
6+
7+
## Method 1: API Keys (Simple)
8+
9+
Use your API keys directly from Claude/OpenAI.
10+
11+
### Claude API Key
12+
13+
```bash
14+
export CLAUDE_API_KEY="sk-ant-..."
15+
python3 run_agent.py
16+
```
17+
18+
### OpenAI API Key
19+
20+
```bash
21+
export OPENAI_API_KEY="sk-..."
22+
python3 run_agent.py
23+
```
24+
25+
---
26+
27+
## Method 2: Subscription-Based OAuth (Like pi.dev)
28+
29+
Use your **Claude Pro**, **ChatGPT Plus**, or other subscriptions!
30+
31+
### Login
32+
33+
```bash
34+
ml-agent login claude # Login to Claude Pro
35+
ml-agent login openai # Login to ChatGPT Plus
36+
```
37+
38+
This opens OAuth flow:
39+
1. Redirects to Claude.ai / ChatGPT login
40+
2. You authenticate and grant permission
41+
3. Token saved to `~/.ml-agent/auth.json`
42+
4. Auto-refreshes when expired
43+
44+
### Check Subscriptions
45+
46+
```bash
47+
ml-agent status
48+
```
49+
50+
Output:
51+
```
52+
Active Subscriptions:
53+
✓ Claude Pro (claude)
54+
✓ ChatGPT Plus (openai)
55+
56+
Credentials:
57+
API Keys: 0
58+
OAuth Subscriptions: 2
59+
```
60+
61+
---
62+
63+
## Method 3: auth.json File
64+
65+
Manual configuration with both API keys and subscriptions:
66+
67+
```json
68+
{
69+
"api_keys": {
70+
"claude": "sk-ant-...",
71+
"openai": "sk-..."
72+
},
73+
"oauth": {
74+
"claude": {
75+
"auth_code": "...",
76+
"subscription": "claude-pro",
77+
"type": "oauth"
78+
},
79+
"openai": {
80+
"auth_code": "...",
81+
"subscription": "chatgpt-plus",
82+
"type": "oauth"
83+
}
84+
}
85+
}
86+
```
87+
88+
Location: `~/.ml-agent/auth.json` (chmod 600)
89+
90+
---
91+
92+
## Authentication Priority
93+
94+
When running ml-agent, it checks credentials in this order:
95+
96+
1. **CLI flag**: `--api-key "sk-ant-..."`
97+
2. **OAuth subscription**: `~/.ml-agent/auth.json` (if logged in)
98+
3. **Environment variables**: `CLAUDE_API_KEY`, `OPENAI_API_KEY`
99+
4. **auth.json file**: API keys section
100+
5. **Interactive prompt**: Ask user
101+
102+
---
103+
104+
## Comparison
105+
106+
| Method | Setup | Cost | Renewal |
107+
|--------|-------|------|---------|
108+
| **API Key** | `export CLAUDE_API_KEY=...` | Pay-as-you-go | Manual |
109+
| **Subscription OAuth** | `ml-agent login claude` | Monthly subscription | Auto |
110+
111+
---
112+
113+
## Examples
114+
115+
### Example 1: Use Claude Pro (subscription)
116+
117+
```bash
118+
# First time
119+
ml-agent login claude
120+
# → Opens claude.ai, you authenticate
121+
# → Token saved to auth.json
122+
123+
# Next runs
124+
python3 run_agent.py
125+
# → Automatically uses Claude Pro
126+
```
127+
128+
### Example 2: Use ChatGPT Plus (subscription)
129+
130+
```bash
131+
ml-agent login openai
132+
python3 run_agent.py
133+
```
134+
135+
### Example 3: Mix API Keys + Subscriptions
136+
137+
```bash
138+
export CLAUDE_API_KEY="sk-ant-..." # API key
139+
ml-agent login openai # Subscription
140+
141+
python3 run_agent.py
142+
# → Uses ChatGPT Plus (subscription) first
143+
# → Falls back to Claude API key if needed
144+
```
145+
146+
---
147+
148+
## Troubleshooting
149+
150+
### "No credentials found"
151+
152+
Check what you have configured:
153+
154+
```bash
155+
ml-agent status
156+
157+
# If nothing, do:
158+
export CLAUDE_API_KEY="sk-ant-..." # OR
159+
ml-agent login claude
160+
```
161+
162+
### "OAuth token expired"
163+
164+
Auto-refreshes on next run. If issues persist:
165+
166+
```bash
167+
rm ~/.ml-agent/auth.json
168+
ml-agent login claude # Re-authenticate
169+
```
170+
171+
### "Permission denied on auth.json"
172+
173+
Fix permissions:
174+
175+
```bash
176+
chmod 600 ~/.ml-agent/auth.json
177+
```
178+
179+
---
180+
181+
## Security
182+
183+
**Safe:**
184+
- Credentials stored locally (not cloud)
185+
- `auth.json` has chmod 600 (owner only)
186+
- OAuth tokens auto-refresh
187+
- No telemetry or logging of credentials
188+
189+
⚠️ **Be careful:**
190+
- Don't commit `auth.json` to git
191+
- Don't share API keys in messages
192+
- Use subscription auth over API keys when possible
193+
194+
---
195+
196+
## Cost Comparison
197+
198+
### API Key Method
199+
- **Cost**: Pay Claude/OpenAI directly per token
200+
- **Example**: 1M tokens ≈ $3-15 depending on model
201+
- **Best for**: Occasional use, testing
202+
203+
### Subscription Method (Claude Pro / ChatGPT Plus)
204+
- **Cost**: $20/month (Claude Pro) or $20/month (ChatGPT Plus)
205+
- **Benefit**: Unlimited usage within allowances
206+
- **Best for**: Regular/heavy usage
207+
208+
**For ML workflows, subscription usually cheaper!**
209+
210+
---
211+
212+
**Choose your auth method and start using ml-agent! 🚀**

src/ml_agent/auth/manager.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55
AuthStrategy, EnvVarStrategy, FileStrategy,
66
CLIArgStrategy, InteractiveStrategy
77
)
8+
from ml_agent.auth.oauth import OAuthManager, SubscriptionAuthStrategy
89
from ml_agent.core.exceptions import AuthenticationError
910

1011
class AuthManager:
11-
"""Manages authentication across providers."""
12+
"""Manages authentication across providers (API keys + OAuth subscriptions)."""
1213

1314
def __init__(self, auth_file: Path = None, cli_api_key: Optional[str] = None):
1415
self.auth_file = auth_file or Path.home() / ".ml-agent" / "auth.json"
16+
self.oauth_manager = OAuthManager()
17+
self.subscription_strategy = SubscriptionAuthStrategy()
18+
1519
self.strategies = [
1620
CLIArgStrategy(cli_api_key),
1721
FileStrategy(self.auth_file),
@@ -20,16 +24,50 @@ def __init__(self, auth_file: Path = None, cli_api_key: Optional[str] = None):
2024
]
2125

2226
def get_api_key(self, provider: str) -> str:
23-
"""Get API key for provider using resolution order."""
27+
"""Get API key for provider using resolution order.
28+
29+
Priority:
30+
1. CLI argument
31+
2. OAuth subscription (Claude Pro, ChatGPT Plus)
32+
3. auth.json file
33+
4. Environment variables
34+
5. Interactive prompt
35+
"""
36+
# Check for OAuth subscription first
37+
if self.subscription_strategy.is_subscribed(provider):
38+
if key := self.subscription_strategy.get_api_key(provider):
39+
return key
40+
41+
# Fall back to traditional API key resolution
2442
for strategy in self.strategies:
2543
if key := strategy.get_credentials(provider):
2644
return key
2745

2846
raise AuthenticationError(
2947
f"No credentials found for provider '{provider}'. "
30-
f"Please provide via CLI, env var, or auth.json"
48+
f"Options:\n"
49+
f" 1. Login: ml-agent login {provider}\n"
50+
f" 2. API Key: ANTHROPIC_API_KEY=... or --api-key\n"
51+
f" 3. auth.json: {self.auth_file}"
3152
)
3253

3354
def validate_provider_auth(self, provider_instance) -> bool:
3455
"""Validate provider credentials."""
3556
return provider_instance.validate_credentials()
57+
58+
def get_subscription_status(self, provider: str) -> dict:
59+
"""Get subscription status for provider."""
60+
if self.subscription_strategy.is_subscribed(provider):
61+
return self.subscription_strategy.get_subscription_info(provider)
62+
63+
return {"provider": provider, "authenticated": False, "type": "api_key"}
64+
65+
def list_subscriptions(self) -> list:
66+
"""List all active subscriptions."""
67+
subscriptions = []
68+
for provider in ["claude", "openai", "google", "xai"]:
69+
if self.subscription_strategy.is_subscribed(provider):
70+
info = self.subscription_strategy.get_subscription_info(provider)
71+
subscriptions.append(info)
72+
73+
return subscriptions

0 commit comments

Comments
 (0)