feat: Add multi-auth token support and ANTHROPIC_BASE_URL passthrough

Implements support for multiple authentication environment variables:
- CLAUDE_CODE_OAUTH_TOKEN (original, highest priority)
- ANTHROPIC_AUTH_TOKEN (for proxies like CCR)
- ANTHROPIC_API_KEY (direct Anthropic API)

Also adds ANTHROPIC_BASE_URL and related env vars passthrough to SDK.

Changes:
- New core/auth.py with centralized auth logic
- Updated core/client.py to use auth helpers
- Updated cli/utils.py to show auth source and base URL
- Updated all modules using auth tokens
- Updated .env.example with documentation
This commit is contained in:
Jacob
2025-12-19 00:32:49 +08:00
committed by AndyMik90
parent d3cdd3a1c7
commit 9dea155505
9 changed files with 195 additions and 33 deletions
+28 -3
View File
@@ -1,14 +1,39 @@
# Auto Claude Environment Variables
# Copy this file to .env and fill in your values
# Claude Code OAuth Token (REQUIRED)
# Get this by running: claude setup-token
CLAUDE_CODE_OAUTH_TOKEN=your-oauth-token-here
# =============================================================================
# AUTHENTICATION (REQUIRED - set ONE of the following)
# =============================================================================
# The framework checks these in order of priority:
# 1. CLAUDE_CODE_OAUTH_TOKEN - Original (from `claude setup-token`)
# 2. ANTHROPIC_AUTH_TOKEN - For proxies like CCR
# 3. ANTHROPIC_API_KEY - Direct Anthropic API key
#
# CLAUDE_CODE_OAUTH_TOKEN=your-oauth-token-here
# ANTHROPIC_AUTH_TOKEN=sk-zcf-x-ccr
# ANTHROPIC_API_KEY=sk-ant-...
# =============================================================================
# CUSTOM API ENDPOINT (OPTIONAL)
# =============================================================================
# Override the default Anthropic API endpoint. Useful for:
# - Local proxies (ccr, litellm)
# - API gateways
# - Self-hosted Claude instances
#
# ANTHROPIC_BASE_URL=http://127.0.0.1:3456
#
# Related settings (usually set together with ANTHROPIC_BASE_URL):
# NO_PROXY=127.0.0.1
# DISABLE_TELEMETRY=true
# DISABLE_COST_WARNINGS=true
# API_TIMEOUT_MS=600000
# Model override (OPTIONAL)
# Default: claude-opus-4-5-20251101
# AUTO_BUILD_MODEL=claude-opus-4-5-20251101
# =============================================================================
# GIT/WORKTREE SETTINGS (OPTIONAL)
# =============================================================================
+9 -5
View File
@@ -28,6 +28,8 @@ except ImportError:
ClaudeAgentOptions = None
ClaudeSDKClient = None
from core.auth import ensure_claude_code_oauth_token, get_auth_token
# Default model for insight extraction (fast and cheap)
DEFAULT_EXTRACTION_MODEL = "claude-3-5-haiku-latest"
@@ -40,10 +42,10 @@ MAX_ATTEMPTS_TO_INCLUDE = 3
def is_extraction_enabled() -> bool:
"""Check if insight extraction is enabled."""
# Extraction requires Claude SDK and OAuth token
# Extraction requires Claude SDK and authentication token
if not SDK_AVAILABLE:
return False
if not os.environ.get("CLAUDE_CODE_OAUTH_TOKEN"):
if not get_auth_token():
return False
enabled_str = os.environ.get("INSIGHT_EXTRACTION_ENABLED", "true").lower()
return enabled_str in ("true", "1", "yes")
@@ -348,11 +350,13 @@ async def run_insight_extraction(
logger.warning("Claude SDK not available, skipping insight extraction")
return None
oauth_token = os.environ.get("CLAUDE_CODE_OAUTH_TOKEN")
if not oauth_token:
logger.warning("CLAUDE_CODE_OAUTH_TOKEN not set, skipping insight extraction")
if not get_auth_token():
logger.warning("No authentication token found, skipping insight extraction")
return None
# Ensure SDK can find the token
ensure_claude_code_oauth_token()
model = get_extraction_model()
prompt = _build_extraction_prompt(inputs)
+16 -4
View File
@@ -14,6 +14,7 @@ _PARENT_DIR = Path(__file__).parent.parent
if str(_PARENT_DIR) not in sys.path:
sys.path.insert(0, str(_PARENT_DIR))
from core.auth import AUTH_TOKEN_ENV_VARS, get_auth_token, get_auth_token_source
from dotenv import load_dotenv
from graphiti_config import get_graphiti_status
from linear_integration import LinearManager
@@ -95,14 +96,25 @@ def validate_environment(spec_dir: Path) -> bool:
"""
valid = True
# Check for Claude Code OAuth token
if not os.environ.get("CLAUDE_CODE_OAUTH_TOKEN"):
print("Error: CLAUDE_CODE_OAUTH_TOKEN environment variable not set")
print("\nGet your OAuth token by running:")
# Check for authentication token (supports multiple env vars)
if not get_auth_token():
print("Error: No authentication token found")
print(f"\nSet one of: {', '.join(AUTH_TOKEN_ENV_VARS)}")
print("\nFor Claude Code CLI, get your OAuth token by running:")
print(" claude setup-token")
print("\nThen set it:")
print(" export CLAUDE_CODE_OAUTH_TOKEN='your-token-here'")
valid = False
else:
# Show which auth source is being used
source = get_auth_token_source()
if source and source != "CLAUDE_CODE_OAUTH_TOKEN":
print(f"Auth: Using token from {source}")
# Show custom base URL if set
base_url = os.environ.get("ANTHROPIC_BASE_URL")
if base_url:
print(f"API Endpoint: {base_url}")
# Check for spec.md in spec directory
spec_file = spec_dir / "spec.md"
+104
View File
@@ -0,0 +1,104 @@
"""
Authentication helpers for Auto Claude.
Provides centralized authentication token resolution with fallback support
for multiple environment variables, and SDK environment variable passthrough
for custom API endpoints.
"""
import os
# Priority order for auth token resolution
AUTH_TOKEN_ENV_VARS = [
"CLAUDE_CODE_OAUTH_TOKEN", # Original (highest priority)
"ANTHROPIC_AUTH_TOKEN", # CCR/proxy token
"ANTHROPIC_API_KEY", # Direct API key (lowest priority)
]
# Environment variables to pass through to SDK subprocess
SDK_ENV_VARS = [
"ANTHROPIC_BASE_URL",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_API_KEY",
"NO_PROXY",
"DISABLE_TELEMETRY",
"DISABLE_COST_WARNINGS",
"API_TIMEOUT_MS",
]
def get_auth_token() -> str | None:
"""
Get authentication token from environment variables.
Checks multiple env vars in priority order:
1. CLAUDE_CODE_OAUTH_TOKEN (original)
2. ANTHROPIC_AUTH_TOKEN (ccr/proxy)
3. ANTHROPIC_API_KEY (direct API key)
Returns:
Token string if found, None otherwise
"""
for var in AUTH_TOKEN_ENV_VARS:
token = os.environ.get(var)
if token:
return token
return None
def get_auth_token_source() -> str | None:
"""Get the name of the env var that provided the auth token."""
for var in AUTH_TOKEN_ENV_VARS:
if os.environ.get(var):
return var
return None
def require_auth_token() -> str:
"""
Get authentication token or raise ValueError.
Raises:
ValueError: If no auth token is found in any supported env var
"""
token = get_auth_token()
if not token:
raise ValueError(
"No authentication token found.\n"
f"Set one of: {', '.join(AUTH_TOKEN_ENV_VARS)}\n"
"For Claude Code CLI: run 'claude setup-token'"
)
return token
def get_sdk_env_vars() -> dict[str, str]:
"""
Get environment variables to pass to SDK.
Collects relevant env vars (ANTHROPIC_BASE_URL, etc.) that should
be passed through to the claude-agent-sdk subprocess.
Returns:
Dict of env var name -> value for non-empty vars
"""
env = {}
for var in SDK_ENV_VARS:
value = os.environ.get(var)
if value:
env[var] = value
return env
def ensure_claude_code_oauth_token() -> None:
"""
Ensure CLAUDE_CODE_OAUTH_TOKEN is set (for SDK compatibility).
If not set but other auth tokens are available, copies the value
to CLAUDE_CODE_OAUTH_TOKEN so the underlying SDK can use it.
"""
if os.environ.get("CLAUDE_CODE_OAUTH_TOKEN"):
return
token = get_auth_token()
if token:
os.environ["CLAUDE_CODE_OAUTH_TOKEN"] = token
+8 -6
View File
@@ -18,6 +18,7 @@ from auto_claude_tools import (
)
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
from claude_agent_sdk.types import HookMatcher
from core.auth import get_sdk_env_vars, require_auth_token
from linear_updater import is_linear_enabled
from security import bash_security_hook
@@ -152,12 +153,12 @@ def create_client(
(see security.py for ALLOWED_COMMANDS)
4. Tool filtering - Each agent type only sees relevant tools (prevents misuse)
"""
oauth_token = os.environ.get("CLAUDE_CODE_OAUTH_TOKEN")
if not oauth_token:
raise ValueError(
"CLAUDE_CODE_OAUTH_TOKEN environment variable not set.\n"
"Get your token by running: claude setup-token"
)
oauth_token = require_auth_token()
# Ensure SDK can access it via its expected env var
os.environ["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
# Collect env vars to pass to SDK (ANTHROPIC_BASE_URL, etc.)
sdk_env = get_sdk_env_vars()
# Check if Linear integration is enabled
linear_enabled = is_linear_enabled()
@@ -318,5 +319,6 @@ def create_client(
max_turns=1000,
cwd=str(project_dir.resolve()),
settings=str(settings_file.resolve()),
env=sdk_env, # Pass ANTHROPIC_BASE_URL etc. to subprocess
)
)
+13 -3
View File
@@ -113,14 +113,23 @@ def _create_linear_client() -> ClaudeSDKClient:
Create a minimal Claude client with only Linear MCP tools.
Used for focused mini-agent calls.
"""
oauth_token = os.environ.get("CLAUDE_CODE_OAUTH_TOKEN")
if not oauth_token:
raise ValueError("CLAUDE_CODE_OAUTH_TOKEN not set")
from core.auth import get_sdk_env_vars, require_auth_token
require_auth_token() # Raises ValueError if no token found
# Ensure SDK can find the token
from core.auth import ensure_claude_code_oauth_token
ensure_claude_code_oauth_token()
linear_api_key = get_linear_api_key()
if not linear_api_key:
raise ValueError("LINEAR_API_KEY not set")
sdk_env = get_sdk_env_vars()
if not linear_api_key:
raise ValueError("LINEAR_API_KEY not set")
return ClaudeSDKClient(
options=ClaudeAgentOptions(
model="claude-haiku-4-5", # Fast & cheap model for simple API calls
@@ -134,6 +143,7 @@ def _create_linear_client() -> ClaudeSDKClient:
}
},
max_turns=10, # Should complete in 1-3 turns
env=sdk_env, # Pass ANTHROPIC_BASE_URL etc. to subprocess
)
)
@@ -12,7 +12,6 @@ from __future__ import annotations
import asyncio
import logging
import os
import sys
from typing import TYPE_CHECKING
@@ -32,13 +31,17 @@ def create_claude_resolver() -> AIResolver:
Configured AIResolver instance
"""
# Import here to avoid circular dependency
from core.auth import ensure_claude_code_oauth_token, get_auth_token
from .resolver import AIResolver
oauth_token = os.environ.get("CLAUDE_CODE_OAUTH_TOKEN")
if not oauth_token:
logger.warning("CLAUDE_CODE_OAUTH_TOKEN not set, AI resolution unavailable")
if not get_auth_token():
logger.warning("No authentication token found, AI resolution unavailable")
return AIResolver()
# Ensure SDK can find the token
ensure_claude_code_oauth_token()
try:
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
except ImportError:
@@ -3,7 +3,6 @@ Claude SDK client wrapper for AI analysis.
"""
import json
import os
from pathlib import Path
from typing import Any
@@ -38,9 +37,10 @@ class ClaudeAnalysisClient:
self._validate_oauth_token()
def _validate_oauth_token(self) -> None:
"""Validate that CLAUDE_CODE_OAUTH_TOKEN is set."""
if not os.environ.get("CLAUDE_CODE_OAUTH_TOKEN"):
raise ValueError("CLAUDE_CODE_OAUTH_TOKEN not set. Run: claude setup-token")
"""Validate that an authentication token is available."""
from core.auth import require_auth_token
require_auth_token() # Raises ValueError if no token found
async def run_analysis_query(self, prompt: str) -> str:
"""
+6 -4
View File
@@ -9,7 +9,6 @@ about a codebase. It can also suggest tasks based on the conversation.
import argparse
import asyncio
import json
import os
import sys
from pathlib import Path
@@ -32,6 +31,7 @@ except ImportError:
ClaudeAgentOptions = None
ClaudeSDKClient = None
from core.auth import ensure_claude_code_oauth_token, get_auth_token
from debug import (
debug,
debug_detailed,
@@ -135,15 +135,17 @@ async def run_with_sdk(project_dir: str, message: str, history: list) -> None:
run_simple(project_dir, message, history)
return
oauth_token = os.environ.get("CLAUDE_CODE_OAUTH_TOKEN")
if not oauth_token:
if not get_auth_token():
print(
"CLAUDE_CODE_OAUTH_TOKEN not set, falling back to simple mode",
"No authentication token found, falling back to simple mode",
file=sys.stderr,
)
run_simple(project_dir, message, history)
return
# Ensure SDK can find the token
ensure_claude_code_oauth_token()
system_prompt = build_system_prompt(project_dir)
project_path = Path(project_dir).resolve()