From 2a1a0f393d7456c2b8ecdf8e7f3441f7dce7906a Mon Sep 17 00:00:00 2001 From: Anmol1696 Date: Thu, 5 Feb 2026 20:24:10 +0400 Subject: [PATCH 1/2] feat: make auth optional on first run Remove blocking auth requirement on first launch. Users now see a warning that no auth is configured but the container starts anyway. Auth can be configured later in the Control UI. --- app/macos/Sources/OpenClawLib/OpenClawLauncher.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/macos/Sources/OpenClawLib/OpenClawLauncher.swift b/app/macos/Sources/OpenClawLib/OpenClawLauncher.swift index 6f32762..cc5779c 100644 --- a/app/macos/Sources/OpenClawLib/OpenClawLauncher.swift +++ b/app/macos/Sources/OpenClawLib/OpenClawLauncher.swift @@ -173,10 +173,9 @@ public class OpenClawLauncher: ObservableObject { // Check for expired OAuth tokens and attempt refresh await refreshOAuthIfNeeded() - // Pause for auth on first run + // Note if no auth configured (non-blocking - user can configure in Control UI) if isFirstRun && !authProfileExists() && !oauthCredentialsExist() { - state = .needsAuth - return + addStep(.warning, "No model auth configured — set up in Control UI") } try await continueAfterSetup() From 41064c53d7811833afd3be44d23afd8473cb37d4 Mon Sep 17 00:00:00 2001 From: Anmol1696 Date: Thu, 5 Feb 2026 21:27:47 +0400 Subject: [PATCH 2/2] feat: add multi-provider auth support Add support for multiple AI providers in the auth flow: - Anthropic (OAuth + API key) - OpenAI (API key) - Google AI (API key) New UI shows provider picker on first run, with each provider having its own configuration flow. Users can skip and configure later in Control UI. --- .../Sources/OpenClawLib/LauncherViews.swift | 146 +++++++++++++----- app/macos/Sources/OpenClawLib/Models.swift | 51 +++++- .../OpenClawLib/OpenClawLauncher.swift | 99 +++++++++--- 3 files changed, 237 insertions(+), 59 deletions(-) diff --git a/app/macos/Sources/OpenClawLib/LauncherViews.swift b/app/macos/Sources/OpenClawLib/LauncherViews.swift index f72add4..0664a74 100644 --- a/app/macos/Sources/OpenClawLib/LauncherViews.swift +++ b/app/macos/Sources/OpenClawLib/LauncherViews.swift @@ -339,13 +339,13 @@ struct SetupView: View { // Bottom actions VStack(spacing: 12) { if launcher.state == .needsAuth { - AuthChoiceView(launcher: launcher) + ProviderSelectionView(launcher: launcher) + } else if launcher.state == .selectingProvider { + AuthMethodView(launcher: launcher) + } else if launcher.state == .waitingForApiKey { + ApiKeyInputView(launcher: launcher) } else if launcher.state == .waitingForOAuthCode { - if launcher.showApiKeyField { - ApiKeyInputView(launcher: launcher) - } else { - OAuthCodeInputView(launcher: launcher) - } + OAuthCodeInputView(launcher: launcher) } else if launcher.state == .stopped { Button("Start OpenClaw") { launcher.start() @@ -380,65 +380,139 @@ struct SetupView: View { } } -struct AuthChoiceView: View { +// MARK: - Provider Selection View + +struct ProviderSelectionView: View { @ObservedObject var launcher: OpenClawLauncher var body: some View { - VStack(spacing: 12) { - Text("Authentication") + VStack(spacing: 16) { + Text("Connect AI Model") .font(.system(size: 14, weight: .semibold)) - Text("Choose how to connect to Anthropic.") + Text("Choose your AI provider to get started.") .font(.system(size: 11)) .foregroundStyle(.secondary) VStack(spacing: 8) { - Button("Sign in with Claude") { - launcher.startOAuth() - } - .buttonStyle(.borderedProminent) - .controlSize(.large) - - Button("Use API Key") { - launcher.showApiKeyInput() + ForEach(AuthProvider.allCases) { provider in + Button(action: { launcher.selectProvider(provider) }) { + HStack { + Text(provider.displayName) + .fontWeight(.medium) + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 12)) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + .padding(.horizontal, 16) + .frame(height: 44) + } + .buttonStyle(.bordered) + .controlSize(.large) } - .buttonStyle(.bordered) - .controlSize(.large) - Button("Skip") { + Button("Skip for now") { launcher.skipAuth() } .buttonStyle(.borderless) .foregroundStyle(.secondary) .font(.system(size: 12)) + .padding(.top, 4) + } + .frame(maxWidth: 300) + + Text("You can always change this in the Control UI later.") + .font(.system(size: 10)) + .foregroundStyle(.tertiary) + } + } +} + +// MARK: - Auth Method View (OAuth vs API Key for providers that support both) + +struct AuthMethodView: View { + @ObservedObject var launcher: OpenClawLauncher + + var body: some View { + VStack(spacing: 12) { + if let provider = launcher.selectedProvider { + Text(provider.displayName) + .font(.system(size: 14, weight: .semibold)) + Text(provider.description) + .font(.system(size: 11)) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + + VStack(spacing: 8) { + if provider.supportsOAuth { + Button("Sign in with Claude") { + launcher.startOAuthForProvider() + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + + Button("Use API Key") { + launcher.showApiKeyInputForProvider() + } + .buttonStyle(.bordered) + .controlSize(.large) + } else { + Button("Use API Key") { + launcher.showApiKeyInputForProvider() + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + } + + Button("Back") { + launcher.backToProviderSelection() + } + .buttonStyle(.borderless) + .foregroundStyle(.secondary) + .font(.system(size: 12)) + } } } } } +// MARK: - API Key Input View + struct ApiKeyInputView: View { @ObservedObject var launcher: OpenClawLauncher var body: some View { VStack(spacing: 12) { - Text("API Key Setup") - .font(.system(size: 14, weight: .semibold)) - Text("Enter your Anthropic API key.") - .font(.system(size: 11)) - .foregroundStyle(.secondary) - SecureField("sk-ant-...", text: $launcher.apiKeyInput) - .textFieldStyle(.roundedBorder) - .font(.system(size: 12, design: .monospaced)) - .frame(maxWidth: 360) - HStack(spacing: 12) { - Button("Continue") { launcher.submitApiKey() } - .buttonStyle(.borderedProminent).controlSize(.large) - Button("Back") { launcher.state = .needsAuth } - .buttonStyle(.bordered).controlSize(.large) + if let provider = launcher.selectedProvider { + Text("\(provider.displayName) API Key") + .font(.system(size: 14, weight: .semibold)) + Text("Enter your \(provider.rawValue) API key.") + .font(.system(size: 11)) + .foregroundStyle(.secondary) + SecureField(provider.apiKeyPlaceholder, text: $launcher.apiKeyInput) + .textFieldStyle(.roundedBorder) + .font(.system(size: 12, design: .monospaced)) + .frame(maxWidth: 360) + HStack(spacing: 12) { + Button("Continue") { launcher.submitApiKey() } + .buttonStyle(.borderedProminent).controlSize(.large) + Button("Back") { + if launcher.selectedProvider?.supportsOAuth == true { + launcher.state = .selectingProvider + } else { + launcher.backToProviderSelection() + } + } + .buttonStyle(.bordered).controlSize(.large) + } } } } } +// MARK: - OAuth Code Input View + struct OAuthCodeInputView: View { @ObservedObject var launcher: OpenClawLauncher @@ -458,7 +532,7 @@ struct OAuthCodeInputView: View { Button("Exchange") { launcher.exchangeOAuthCode() } .buttonStyle(.borderedProminent).controlSize(.large) .disabled(launcher.oauthCodeInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) - Button("Back") { launcher.state = .needsAuth } + Button("Back") { launcher.state = .selectingProvider } .buttonStyle(.bordered).controlSize(.large) } } diff --git a/app/macos/Sources/OpenClawLib/Models.swift b/app/macos/Sources/OpenClawLib/Models.swift index ea2e4e3..550f4b7 100644 --- a/app/macos/Sources/OpenClawLib/Models.swift +++ b/app/macos/Sources/OpenClawLib/Models.swift @@ -2,10 +2,59 @@ import Foundation public enum StepStatus { case pending, running, done, error, warning } -public enum LauncherState { case idle, working, needsAuth, waitingForOAuthCode, running, stopped, error } +public enum LauncherState { case idle, working, needsAuth, selectingProvider, waitingForApiKey, waitingForOAuthCode, running, stopped, error } public enum MenuBarStatus { case starting, running, stopped } +// MARK: - Auth Providers + +public enum AuthProvider: String, CaseIterable, Identifiable { + case anthropic = "Anthropic" + case openai = "OpenAI" + case google = "Google AI" + + public var id: String { rawValue } + + public var displayName: String { + switch self { + case .anthropic: return "Claude (Anthropic)" + case .openai: return "GPT (OpenAI)" + case .google: return "Gemini (Google AI)" + } + } + + public var description: String { + switch self { + case .anthropic: return "Sign in with your Claude account or use an API key" + case .openai: return "Enter your OpenAI API key" + case .google: return "Enter your Google AI API key" + } + } + + public var supportsOAuth: Bool { + switch self { + case .anthropic: return true + case .openai, .google: return false + } + } + + public var apiKeyPrefix: String { + switch self { + case .anthropic: return "sk-ant-" + case .openai: return "sk-" + case .google: return "" // Google AI keys don't have a standard prefix + } + } + + public var apiKeyPlaceholder: String { + switch self { + case .anthropic: return "sk-ant-api..." + case .openai: return "sk-proj-..." + case .google: return "AIza..." + } + } +} + public struct GatewayStatus: Codable { public let uptime: Int? } diff --git a/app/macos/Sources/OpenClawLib/OpenClawLauncher.swift b/app/macos/Sources/OpenClawLib/OpenClawLauncher.swift index cc5779c..4cafb7a 100644 --- a/app/macos/Sources/OpenClawLib/OpenClawLauncher.swift +++ b/app/macos/Sources/OpenClawLib/OpenClawLauncher.swift @@ -67,6 +67,7 @@ public class OpenClawLauncher: ObservableObject { @Published public var showResetConfirm: Bool = false @Published public var needsDockerInstall: Bool = false @Published public var authExpiredBanner: String? + @Published public var selectedProvider: AuthProvider? private var isFirstRun = false private var currentPKCE: AnthropicOAuth.PKCE? @@ -269,17 +270,74 @@ public class OpenClawLauncher: ObservableObject { } } + // MARK: - Provider Selection + + public func selectProvider(_ provider: AuthProvider) { + selectedProvider = provider + apiKeyInput = "" + + if provider.supportsOAuth { + // Show choice between OAuth and API key for Anthropic + state = .selectingProvider + } else { + // Go directly to API key input for OpenAI/Google + state = .waitingForApiKey + } + } + + public func startOAuthForProvider() { + guard selectedProvider == .anthropic else { return } + do { + let pkce = try AnthropicOAuth.generatePKCE() + currentPKCE = pkce + let url = AnthropicOAuth.buildAuthorizeURL(pkce: pkce) + NSWorkspace.shared.open(url) + oauthCodeInput = "" + state = .waitingForOAuthCode + addStep(.running, "Opened browser for Anthropic sign-in") + } catch { + addStep(.error, "Failed to start OAuth: \(error.localizedDescription)") + } + } + + public func showApiKeyInputForProvider() { + apiKeyInput = "" + state = .waitingForApiKey + } + public func submitApiKey() { + guard let provider = selectedProvider else { + addStep(.error, "No provider selected") + state = .needsAuth + return + } + let key = apiKeyInput.trimmingCharacters(in: .whitespacesAndNewlines) if !key.isEmpty { let authDir = configDir.appendingPathComponent("agents/default/agent") let authFile = authDir.appendingPathComponent("auth-profiles.json") + + // Build profile key based on provider + let profileKey: String + let providerName: String + switch provider { + case .anthropic: + profileKey = "anthropic:default" + providerName = "anthropic" + case .openai: + profileKey = "openai:default" + providerName = "openai" + case .google: + profileKey = "google:default" + providerName = "google" + } + let payload: [String: Any] = [ "version": 1, "profiles": [ - "anthropic:default": [ + profileKey: [ "type": "api_key", - "provider": "anthropic", + "provider": providerName, "key": key ] ] @@ -287,7 +345,7 @@ public class OpenClawLauncher: ObservableObject { if let data = try? JSONSerialization.data(withJSONObject: payload, options: .prettyPrinted), let _ = try? data.write(to: authFile) { try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: authFile.path) - addStep(.done, "API key saved") + addStep(.done, "\(provider.displayName) API key saved") } else { addStep(.error, "Failed to save API key") } @@ -295,6 +353,7 @@ public class OpenClawLauncher: ObservableObject { addStep(.warning, "Skipped API key — set up later in Control UI") } + selectedProvider = nil state = .working Task { do { @@ -306,29 +365,14 @@ public class OpenClawLauncher: ObservableObject { } } - public func startOAuth() { - do { - let pkce = try AnthropicOAuth.generatePKCE() - currentPKCE = pkce - let url = AnthropicOAuth.buildAuthorizeURL(pkce: pkce) - NSWorkspace.shared.open(url) - showApiKeyField = false - oauthCodeInput = "" - state = .waitingForOAuthCode - addStep(.running, "Opened browser for Anthropic sign-in") - } catch { - addStep(.error, "Failed to start OAuth: \(error.localizedDescription)") - } - } - - public func showApiKeyInput() { - showApiKeyField = true - apiKeyInput = "" - state = .waitingForOAuthCode + public func backToProviderSelection() { + state = .needsAuth + selectedProvider = nil } public func skipAuth() { addStep(.warning, "Skipped auth — set up later in Control UI") + selectedProvider = nil state = .working Task { do { try await continueAfterSetup() } @@ -336,6 +380,17 @@ public class OpenClawLauncher: ObservableObject { } } + // Legacy method for backward compatibility + public func startOAuth() { + selectedProvider = .anthropic + startOAuthForProvider() + } + + public func showApiKeyInput() { + selectedProvider = .anthropic + showApiKeyInputForProvider() + } + public func exchangeOAuthCode() { guard let pkce = currentPKCE else { addStep(.error, "No PKCE session. Try signing in again.")