feat(auth): add Anthropic setup-token authentication
Add support for Claude CLI setup-tokens (sk-ant-oat01-*) as an authentication method for the Anthropic provider, complementing the existing API key paste flow. Changes: - pkg/auth/token.go: add LoginSetupToken() with prefix and length validation, keeping LoginPasteToken untouched - pkg/providers/claude_provider.go: add NewClaudeProviderWithBearerToken() that uses Authorization Bearer with required headers (anthropic-beta: claude-code-20250219,oauth-2025-04-20, user-agent, x-app) to enable OAuth on the Anthropic API; add useAPIKey field to distinguish between x-api-key (regular keys) and Bearer (setup-token/oauth) at request time - pkg/providers/http_provider.go: route setup-token credentials through the Bearer provider; recognize "setup-token" as valid auth method in both provider selection paths - cmd/picoclaw/main.go: add --setup-token CLI flag for anthropic login, new authLoginSetupToken() handler, updated help text Usage: picoclaw auth login --provider anthropic --setup-token Author: Edgar Gomero Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9a3f3611c3
commit
32a5bf841c
4 changed files with 97 additions and 6 deletions
|
|
@ -777,11 +777,13 @@ func authHelp() {
|
|||
fmt.Println("Login options:")
|
||||
fmt.Println(" --provider <name> Provider to login with (openai, anthropic)")
|
||||
fmt.Println(" --device-code Use device code flow (for headless environments)")
|
||||
fmt.Println(" --setup-token Use Claude CLI setup-token (anthropic only)")
|
||||
fmt.Println()
|
||||
fmt.Println("Examples:")
|
||||
fmt.Println(" picoclaw auth login --provider openai")
|
||||
fmt.Println(" picoclaw auth login --provider openai --device-code")
|
||||
fmt.Println(" picoclaw auth login --provider anthropic")
|
||||
fmt.Println(" picoclaw auth login --provider anthropic --setup-token")
|
||||
fmt.Println(" picoclaw auth logout --provider openai")
|
||||
fmt.Println(" picoclaw auth status")
|
||||
}
|
||||
|
|
@ -789,6 +791,7 @@ func authHelp() {
|
|||
func authLoginCmd() {
|
||||
provider := ""
|
||||
useDeviceCode := false
|
||||
useSetupToken := false
|
||||
|
||||
args := os.Args[3:]
|
||||
for i := 0; i < len(args); i++ {
|
||||
|
|
@ -800,6 +803,8 @@ func authLoginCmd() {
|
|||
}
|
||||
case "--device-code":
|
||||
useDeviceCode = true
|
||||
case "--setup-token":
|
||||
useSetupToken = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -813,13 +818,40 @@ func authLoginCmd() {
|
|||
case "openai":
|
||||
authLoginOpenAI(useDeviceCode)
|
||||
case "anthropic":
|
||||
authLoginPasteToken(provider)
|
||||
if useSetupToken {
|
||||
authLoginSetupToken()
|
||||
} else {
|
||||
authLoginPasteToken(provider)
|
||||
}
|
||||
default:
|
||||
fmt.Printf("Unsupported provider: %s\n", provider)
|
||||
fmt.Println("Supported providers: openai, anthropic")
|
||||
}
|
||||
}
|
||||
|
||||
func authLoginSetupToken() {
|
||||
cred, err := auth.LoginSetupToken(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Printf("Login failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := auth.SetCredential("anthropic", cred); err != nil {
|
||||
fmt.Printf("Failed to save credentials: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
appCfg, err := loadConfig()
|
||||
if err == nil {
|
||||
appCfg.Providers.Anthropic.AuthMethod = "setup-token"
|
||||
if err := config.SaveConfig(getConfigPath(), appCfg); err != nil {
|
||||
fmt.Printf("Warning: could not update config: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Setup-token saved for Anthropic!")
|
||||
}
|
||||
|
||||
func authLoginOpenAI(useDeviceCode bool) {
|
||||
cfg := auth.OpenAIOAuthConfig()
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,40 @@ func LoginPasteToken(provider string, r io.Reader) (*AuthCredential, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
const anthropicSetupTokenPrefix = "sk-ant-oat01-"
|
||||
|
||||
func LoginSetupToken(r io.Reader) (*AuthCredential, error) {
|
||||
fmt.Println("Paste your setup-token from Claude CLI (claude setup-token):")
|
||||
fmt.Print("> ")
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
if !scanner.Scan() {
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("reading setup-token: %w", err)
|
||||
}
|
||||
return nil, fmt.Errorf("no input received")
|
||||
}
|
||||
|
||||
token := strings.TrimSpace(scanner.Text())
|
||||
if token == "" {
|
||||
return nil, fmt.Errorf("setup-token cannot be empty")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(token, anthropicSetupTokenPrefix) {
|
||||
return nil, fmt.Errorf("invalid setup-token: must start with %s", anthropicSetupTokenPrefix)
|
||||
}
|
||||
|
||||
if len(token) < 80 {
|
||||
return nil, fmt.Errorf("invalid setup-token: too short (expected at least 80 characters)")
|
||||
}
|
||||
|
||||
return &AuthCredential{
|
||||
AccessToken: token,
|
||||
Provider: "anthropic",
|
||||
AuthMethod: "setup-token",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func providerDisplayName(provider string) string {
|
||||
switch provider {
|
||||
case "anthropic":
|
||||
|
|
|
|||
|
|
@ -13,14 +13,26 @@ import (
|
|||
type ClaudeProvider struct {
|
||||
client *anthropic.Client
|
||||
tokenSource func() (string, error)
|
||||
useAPIKey bool
|
||||
}
|
||||
|
||||
func NewClaudeProvider(token string) *ClaudeProvider {
|
||||
client := anthropic.NewClient(
|
||||
option.WithAuthToken(token),
|
||||
option.WithAPIKey(token),
|
||||
option.WithBaseURL("https://api.anthropic.com"),
|
||||
)
|
||||
return &ClaudeProvider{client: &client}
|
||||
return &ClaudeProvider{client: &client, useAPIKey: true}
|
||||
}
|
||||
|
||||
func NewClaudeProviderWithBearerToken(token string) *ClaudeProvider {
|
||||
client := anthropic.NewClient(
|
||||
option.WithAuthToken(token),
|
||||
option.WithBaseURL("https://api.anthropic.com"),
|
||||
option.WithHeader("anthropic-beta", "claude-code-20250219,oauth-2025-04-20"),
|
||||
option.WithHeader("user-agent", "claude-cli/2.1.2 (external, cli)"),
|
||||
option.WithHeader("x-app", "cli"),
|
||||
)
|
||||
return &ClaudeProvider{client: &client, useAPIKey: false}
|
||||
}
|
||||
|
||||
func NewClaudeProviderWithTokenSource(token string, tokenSource func() (string, error)) *ClaudeProvider {
|
||||
|
|
@ -29,6 +41,12 @@ func NewClaudeProviderWithTokenSource(token string, tokenSource func() (string,
|
|||
return p
|
||||
}
|
||||
|
||||
func NewClaudeProviderWithTokenSourceBearer(token string, tokenSource func() (string, error)) *ClaudeProvider {
|
||||
p := NewClaudeProviderWithBearerToken(token)
|
||||
p.tokenSource = tokenSource
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *ClaudeProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
|
||||
var opts []option.RequestOption
|
||||
if p.tokenSource != nil {
|
||||
|
|
@ -36,7 +54,11 @@ func (p *ClaudeProvider) Chat(ctx context.Context, messages []Message, tools []T
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("refreshing token: %w", err)
|
||||
}
|
||||
opts = append(opts, option.WithAuthToken(tok))
|
||||
if p.useAPIKey {
|
||||
opts = append(opts, option.WithAPIKey(tok))
|
||||
} else {
|
||||
opts = append(opts, option.WithAuthToken(tok))
|
||||
}
|
||||
}
|
||||
|
||||
params, err := buildClaudeParams(messages, tools, model, options)
|
||||
|
|
|
|||
|
|
@ -205,6 +205,9 @@ func createClaudeAuthProvider() (LLMProvider, error) {
|
|||
if cred == nil {
|
||||
return nil, fmt.Errorf("no credentials for anthropic. Run: picoclaw auth login --provider anthropic")
|
||||
}
|
||||
if cred.AuthMethod == "oauth" || cred.AuthMethod == "setup-token" {
|
||||
return NewClaudeProviderWithTokenSourceBearer(cred.AccessToken, createClaudeTokenSource()), nil
|
||||
}
|
||||
return NewClaudeProviderWithTokenSource(cred.AccessToken, createClaudeTokenSource()), nil
|
||||
}
|
||||
|
||||
|
|
@ -251,7 +254,7 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
|||
}
|
||||
case "anthropic", "claude":
|
||||
if cfg.Providers.Anthropic.APIKey != "" || cfg.Providers.Anthropic.AuthMethod != "" {
|
||||
if cfg.Providers.Anthropic.AuthMethod == "oauth" || cfg.Providers.Anthropic.AuthMethod == "token" {
|
||||
if cfg.Providers.Anthropic.AuthMethod == "oauth" || cfg.Providers.Anthropic.AuthMethod == "token" || cfg.Providers.Anthropic.AuthMethod == "setup-token" {
|
||||
return createClaudeAuthProvider()
|
||||
}
|
||||
apiKey = cfg.Providers.Anthropic.APIKey
|
||||
|
|
@ -348,7 +351,7 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
|||
}
|
||||
|
||||
case (strings.Contains(lowerModel, "claude") || strings.HasPrefix(model, "anthropic/")) && (cfg.Providers.Anthropic.APIKey != "" || cfg.Providers.Anthropic.AuthMethod != ""):
|
||||
if cfg.Providers.Anthropic.AuthMethod == "oauth" || cfg.Providers.Anthropic.AuthMethod == "token" {
|
||||
if cfg.Providers.Anthropic.AuthMethod == "oauth" || cfg.Providers.Anthropic.AuthMethod == "token" || cfg.Providers.Anthropic.AuthMethod == "setup-token" {
|
||||
return createClaudeAuthProvider()
|
||||
}
|
||||
apiKey = cfg.Providers.Anthropic.APIKey
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue