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:
@@ -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)
|
||||
# =============================================================================
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user