diff --git a/README.md b/README.md index 0a25e8202..d4fd5d8df 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,46 @@ Claude asks clarifying questions, builds structured plans, and shows clean markd - **Project Management** - Link local folders with automatic Git remote detection - **Integrated Terminal** - Full terminal access within the app +## Authentication Options + +1Code supports two authentication methods for accessing Claude API: + +### Anthropic OAuth (Default) +Sign in with your Anthropic account - works seamlessly with Claude.ai Pro subscriptions. + +### AWS Bedrock + +Use Claude models through AWS Bedrock with your AWS credentials. + +**Prerequisites:** +- AWS account with Bedrock access +- AWS CLI installed and configured +- Claude models enabled in AWS Bedrock console + +**Setup:** + +1. **Configure AWS credentials:** + ```bash + aws configure + # Enter your AWS Access Key ID and Secret Access Key + # Choose a region where Bedrock is available (e.g., us-east-1) + ``` + +2. **Enable Bedrock authentication:** + - Open 1Code Settings (⌘,) + - Go to Authentication tab + - Select "AWS Bedrock" + - Verify credentials are detected (green checkmark) + - Set AWS region + - Click "Save Changes" + +**Supported AWS Regions:** +- us-east-1 (N. Virginia) +- us-west-2 (Oregon) +- See [AWS Bedrock regions](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-regions.html) for the latest availability + +**Cost:** AWS Bedrock charges per API request. See [AWS Bedrock pricing](https://aws.amazon.com/bedrock/pricing/) for details. + ## Installation ### Option 1: Build from source (free) diff --git a/drizzle/0008_lumpy_archangel.sql b/drizzle/0008_lumpy_archangel.sql new file mode 100644 index 000000000..63160ea9b --- /dev/null +++ b/drizzle/0008_lumpy_archangel.sql @@ -0,0 +1,7 @@ +CREATE TABLE `anthropic_auth_settings` ( + `id` text PRIMARY KEY DEFAULT 'singleton' NOT NULL, + `auth_mode` text DEFAULT 'oauth' NOT NULL, + `aws_region` text DEFAULT 'us-east-1', + `aws_profile` text, + `updated_at` integer +); diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json new file mode 100644 index 000000000..55ca7da9c --- /dev/null +++ b/drizzle/meta/0008_snapshot.json @@ -0,0 +1,482 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "2c258ac4-d372-4cd3-9b5a-6613e7338005", + "prevId": "b2d2d602-5de1-43b1-ada8-c9ed3edde22d", + "tables": { + "anthropic_accounts": { + "name": "anthropic_accounts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_token": { + "name": "oauth_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connected_at": { + "name": "connected_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "desktop_user_id": { + "name": "desktop_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "anthropic_auth_settings": { + "name": "anthropic_auth_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "'singleton'" + }, + "auth_mode": { + "name": "auth_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'oauth'" + }, + "aws_region": { + "name": "aws_region", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'us-east-1'" + }, + "aws_profile": { + "name": "aws_profile", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "anthropic_settings": { + "name": "anthropic_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "'singleton'" + }, + "active_account_id": { + "name": "active_account_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chats": { + "name": "chats", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "worktree_path": { + "name": "worktree_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "chats_worktree_path_idx": { + "name": "chats_worktree_path_idx", + "columns": [ + "worktree_path" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chats_project_id_projects_id_fk": { + "name": "chats_project_id_projects_id_fk", + "tableFrom": "chats", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "claude_code_credentials": { + "name": "claude_code_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "oauth_token": { + "name": "oauth_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connected_at": { + "name": "connected_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_remote_url": { + "name": "git_remote_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_provider": { + "name": "git_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_owner": { + "name": "git_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_repo": { + "name": "git_repo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_path": { + "name": "icon_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "projects_path_unique": { + "name": "projects_path_unique", + "columns": [ + "path" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sub_chats": { + "name": "sub_chats", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chat_id": { + "name": "chat_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'agent'" + }, + "messages": { + "name": "messages", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sub_chats_chat_id_chats_id_fk": { + "name": "sub_chats_chat_id_chats_id_fk", + "tableFrom": "sub_chats", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 88a3e0a60..83a23dcbb 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1769810815497, "tag": "0007_clammy_grim_reaper", "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1771309289096, + "tag": "0008_lumpy_archangel", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/main/lib/claude/bedrock-validation.ts b/src/main/lib/claude/bedrock-validation.ts new file mode 100644 index 000000000..d3f666681 --- /dev/null +++ b/src/main/lib/claude/bedrock-validation.ts @@ -0,0 +1,40 @@ +/** + * Validate AWS Bedrock configuration before executing Claude query + * This ensures users get clear error messages if credentials or config are missing + */ +export function validateBedrockConfig(env: Record): { + valid: boolean + error?: string + details?: string +} { + // Check for AWS credentials (either env vars or profile) + const hasEnvCredentials = !!( + env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY + ) + const hasProfile = !!env.AWS_PROFILE + + if (!hasEnvCredentials && !hasProfile) { + return { + valid: false, + error: "AWS credentials not found", + details: + "Please configure AWS CLI credentials:\n" + + "1. Run 'aws configure' in terminal\n" + + "2. Or manually edit ~/.aws/credentials\n" + + "3. Restart 1Code to load new credentials", + } + } + + // Check for AWS region + if (!env.AWS_REGION && !env.AWS_DEFAULT_REGION) { + return { + valid: false, + error: "AWS region not configured", + details: + "Please set AWS region in Settings > Authentication.\n" + + "Claude on Bedrock is available in regions like us-east-1, us-west-2.", + } + } + + return { valid: true } +} diff --git a/src/main/lib/claude/env.ts b/src/main/lib/claude/env.ts index 0ea2ab0cf..105f3442f 100644 --- a/src/main/lib/claude/env.ts +++ b/src/main/lib/claude/env.ts @@ -20,9 +20,9 @@ const DELIMITER = "_CLAUDE_ENV_DELIMITER_" // NOTE: We intentionally keep ANTHROPIC_API_KEY and ANTHROPIC_BASE_URL in production // so users can use their existing Claude Code CLI configuration (API proxy, etc.) // Based on PR #29 by @sa4hnd +// NOTE: CLAUDE_CODE_USE_BEDROCK is NOT stripped to allow Bedrock authentication mode const STRIPPED_ENV_KEYS_BASE = [ "OPENAI_API_KEY", - "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", ] @@ -33,6 +33,28 @@ const STRIPPED_ENV_KEYS = !app.isPackaged ? [...STRIPPED_ENV_KEYS_BASE, "ANTHROPIC_API_KEY"] : STRIPPED_ENV_KEYS_BASE +// AWS credential keys that should be preserved when using Bedrock authentication +// These are stripped in OAuth mode for security, but preserved in Bedrock mode +const AWS_CREDENTIAL_KEYS = [ + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN", + "AWS_PROFILE", + "AWS_DEFAULT_REGION", + "AWS_REGION", + "AWS_CONFIG_FILE", + "AWS_SHARED_CREDENTIALS_FILE", +] + +/** + * Check if Bedrock mode is enabled in the environment + * @param env Environment variables object + * @returns true if CLAUDE_CODE_USE_BEDROCK is set to "true" + */ +function isBedrockModeEnabled(env: Record): boolean { + return env.CLAUDE_CODE_USE_BEDROCK === "true" +} + // Cache the bundled binary path (only compute once) let cachedBinaryPath: string | null = null let binaryPathComputed = false @@ -237,7 +259,19 @@ export function buildClaudeEnv(options?: { env.PATH = shellPath } - // 2b. Strip sensitive keys again (process.env may have re-added them) + // 2b. Add custom environment overrides FIRST (before stripping) + // This is critical: we need CLAUDE_CODE_USE_BEDROCK to be present before checking Bedrock mode + if (options?.customEnv) { + for (const [key, value] of Object.entries(options.customEnv)) { + if (value === "") { + delete env[key] + } else { + env[key] = value + } + } + } + + // 2c. Strip sensitive keys (process.env may have re-added them) // This ensures ANTHROPIC_API_KEY from dev's shell doesn't override OAuth in dev mode // Added by Sergey Bunas for dev purposes for (const key of STRIPPED_ENV_KEYS) { @@ -247,6 +281,23 @@ export function buildClaudeEnv(options?: { } } + // 2d. Conditionally strip AWS credentials based on authentication mode + // In Bedrock mode, preserve AWS credentials for SDK authentication + // In OAuth mode, remove them for security (prevent accidental AWS API access) + const bedrockMode = isBedrockModeEnabled(env) + + if (bedrockMode) { + console.log("[claude-env] Bedrock mode enabled - preserving AWS credentials") + } else { + // OAuth mode: strip AWS credentials for security + for (const key of AWS_CREDENTIAL_KEYS) { + if (key in env) { + console.log(`[claude-env] OAuth mode - stripped AWS credential: ${key}`) + delete env[key] + } + } + } + // 3. Ensure critical vars are present using platform provider const platformEnv = platform.buildEnvironment() if (!env.HOME) env.HOME = platformEnv.HOME @@ -259,19 +310,11 @@ export function buildClaudeEnv(options?: { env.USERPROFILE = os.homedir() } - // 4. Add custom overrides + // 4. Add GitHub token if provided if (options?.ghToken) { env.GH_TOKEN = options.ghToken } - if (options?.customEnv) { - for (const [key, value] of Object.entries(options.customEnv)) { - if (value === "") { - delete env[key] - } else { - env[key] = value - } - } - } + // Note: customEnv is now merged earlier (step 2b) before checking Bedrock mode // 5. Mark as SDK entry env.CLAUDE_CODE_ENTRYPOINT = "sdk-ts" diff --git a/src/main/lib/db/schema/index.ts b/src/main/lib/db/schema/index.ts index fe6aa3490..c72f555e4 100644 --- a/src/main/lib/db/schema/index.ts +++ b/src/main/lib/db/schema/index.ts @@ -128,6 +128,17 @@ export const anthropicSettings = sqliteTable("anthropic_settings", { ), }) +// Stores authentication mode (OAuth vs Bedrock) and AWS configuration +export const anthropicAuthSettings = sqliteTable("anthropic_auth_settings", { + id: text("id").primaryKey().default("singleton"), // Single row + authMode: text("auth_mode").notNull().default("oauth"), // "oauth" | "bedrock" + awsRegion: text("aws_region").default("us-east-1"), + awsProfile: text("aws_profile"), // Optional: override default profile + updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn( + () => new Date(), + ), +}) + // ============ TYPE EXPORTS ============ export type Project = typeof projects.$inferSelect export type NewProject = typeof projects.$inferInsert @@ -140,3 +151,5 @@ export type NewClaudeCodeCredential = typeof claudeCodeCredentials.$inferInsert export type AnthropicAccount = typeof anthropicAccounts.$inferSelect export type NewAnthropicAccount = typeof anthropicAccounts.$inferInsert export type AnthropicSettings = typeof anthropicSettings.$inferSelect +export type AnthropicAuthSettings = typeof anthropicAuthSettings.$inferSelect +export type NewAnthropicAuthSettings = typeof anthropicAuthSettings.$inferInsert diff --git a/src/main/lib/trpc/routers/anthropic-auth.ts b/src/main/lib/trpc/routers/anthropic-auth.ts new file mode 100644 index 000000000..dba3228d4 --- /dev/null +++ b/src/main/lib/trpc/routers/anthropic-auth.ts @@ -0,0 +1,123 @@ +import { z } from "zod" +import { eq } from "drizzle-orm" +import * as fs from "fs" +import * as os from "os" +import * as path from "path" +import { publicProcedure, router } from "../index" +import { anthropicAuthSettings, getDatabase } from "../../db" + +export const anthropicAuthRouter = router({ + // Get current authentication settings + getSettings: publicProcedure.query(async () => { + const db = getDatabase() + let settings = db + .select() + .from(anthropicAuthSettings) + .where(eq(anthropicAuthSettings.id, "singleton")) + .get() + + // Initialize with defaults if not exists + if (!settings) { + db.insert(anthropicAuthSettings) + .values({ + id: "singleton", + authMode: "oauth", + awsRegion: "us-east-1", + }) + .run() + + settings = db + .select() + .from(anthropicAuthSettings) + .where(eq(anthropicAuthSettings.id, "singleton")) + .get() + } + + return settings + }), + + // Update authentication mode and AWS settings + updateSettings: publicProcedure + .input( + z.object({ + authMode: z.enum(["oauth", "bedrock"]), + awsRegion: z.string().optional(), + awsProfile: z.string().optional(), + }), + ) + .mutation(async ({ input }) => { + const db = getDatabase() + + // Upsert settings + const existing = db + .select() + .from(anthropicAuthSettings) + .where(eq(anthropicAuthSettings.id, "singleton")) + .get() + + if (existing) { + db.update(anthropicAuthSettings) + .set({ + authMode: input.authMode, + awsRegion: input.awsRegion, + awsProfile: input.awsProfile, + updatedAt: new Date(), + }) + .where(eq(anthropicAuthSettings.id, "singleton")) + .run() + } else { + db.insert(anthropicAuthSettings) + .values({ + id: "singleton", + authMode: input.authMode, + awsRegion: input.awsRegion || "us-east-1", + awsProfile: input.awsProfile, + }) + .run() + } + + console.log(`[anthropic-auth] Auth mode changed to: ${input.authMode}`) + + return db + .select() + .from(anthropicAuthSettings) + .where(eq(anthropicAuthSettings.id, "singleton")) + .get() + }), + + // Validate AWS credentials are available in environment + validateAwsCredentials: publicProcedure.query(async () => { + // Check if AWS credentials are available in environment + const hasEnvCredentials = !!( + process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY + ) + + const hasProfile = !!process.env.AWS_PROFILE + + const awsConfigFile = + process.env.AWS_CONFIG_FILE || path.join(os.homedir(), ".aws", "config") + const awsCredentialsFile = + process.env.AWS_SHARED_CREDENTIALS_FILE || + path.join(os.homedir(), ".aws", "credentials") + + const hasConfigFile = fs.existsSync(awsConfigFile) + const hasCredentialsFile = fs.existsSync(awsCredentialsFile) + + const hasAwsCredentials = + hasEnvCredentials || + hasProfile || + (hasConfigFile && hasCredentialsFile) + + return { + hasAwsCredentials, + hasEnvCredentials, + hasProfile, + hasConfigFile, + hasCredentialsFile, + awsProfile: process.env.AWS_PROFILE || "default", + awsRegion: process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || null, + awsConfigPath: awsConfigFile, + awsCredentialsPath: awsCredentialsFile, + } + }), +}) diff --git a/src/main/lib/trpc/routers/claude.ts b/src/main/lib/trpc/routers/claude.ts index 11cf53798..5565703dc 100644 --- a/src/main/lib/trpc/routers/claude.ts +++ b/src/main/lib/trpc/routers/claude.ts @@ -29,7 +29,7 @@ import { type ClaudeConfig, type McpServerConfig, } from "../../claude-config" -import { anthropicAccounts, anthropicSettings, chats, claudeCodeCredentials, getDatabase, projects as projectsTable, subChats } from "../../db" +import { anthropicAccounts, anthropicAuthSettings, anthropicSettings, chats, claudeCodeCredentials, getDatabase, projects as projectsTable, subChats } from "../../db" import { createRollbackStash } from "../../git/stash" import { ensureMcpTokensFresh, @@ -1112,14 +1112,32 @@ export const claudeRouter = router({ prompt = createPromptWithImages() } + // Get authentication settings from database FIRST (before building env) + const authSettings = db + .select() + .from(anthropicAuthSettings) + .where(eq(anthropicAuthSettings.id, "singleton")) + .get() + + const isBedrockMode = authSettings?.authMode === "bedrock" + + // Build custom environment variables for Claude SDK + const customEnvVars: Record = {} + + // Add custom config if present (for offline/Ollama mode) + if (finalCustomConfig) { + customEnvVars.ANTHROPIC_AUTH_TOKEN = finalCustomConfig.token + customEnvVars.ANTHROPIC_BASE_URL = finalCustomConfig.baseUrl + } + + // Set Bedrock flag BEFORE building env so AWS credentials aren't stripped + if (isBedrockMode) { + customEnvVars.CLAUDE_CODE_USE_BEDROCK = "true" + } + // Build full environment for Claude SDK (includes HOME, PATH, etc.) const claudeEnv = buildClaudeEnv({ - ...(finalCustomConfig && { - customEnv: { - ANTHROPIC_AUTH_TOKEN: finalCustomConfig.token, - ANTHROPIC_BASE_URL: finalCustomConfig.baseUrl, - }, - }), + customEnv: customEnvVars, enableTasks: input.enableTasks ?? true, }) @@ -1327,20 +1345,58 @@ export const claudeRouter = router({ ) } - // Build final env - only add OAuth token if we have one AND no existing API config - // Existing CLI config takes precedence over OAuth + // Build final environment based on authentication mode + // (authSettings and isBedrockMode were already queried above before buildClaudeEnv) const finalEnv = { ...claudeEnv, - ...(claudeCodeToken && - !hasExistingApiConfig && { - CLAUDE_CODE_OAUTH_TOKEN: claudeCodeToken, - }), - // Re-enable CLAUDE_CONFIG_DIR now that we properly map MCP configs CLAUDE_CONFIG_DIR: isolatedConfigDir, } + if (isBedrockMode) { + // Bedrock mode: Set flag and AWS config, skip OAuth token + console.log("[claude-auth] Using AWS Bedrock authentication") + // CLAUDE_CODE_USE_BEDROCK was already set in buildClaudeEnv() above + + // Set AWS region if specified + if (authSettings?.awsRegion) { + finalEnv.AWS_DEFAULT_REGION = authSettings.awsRegion + finalEnv.AWS_REGION = authSettings.awsRegion + } + + // Set AWS profile if specified (overrides default) + if (authSettings?.awsProfile) { + finalEnv.AWS_PROFILE = authSettings.awsProfile + } + + // Validate AWS credentials are available + const hasAwsCredentials = !!( + finalEnv.AWS_ACCESS_KEY_ID && finalEnv.AWS_SECRET_ACCESS_KEY + ) || !!finalEnv.AWS_PROFILE + + if (!hasAwsCredentials) { + console.error("[claude-auth] AWS credentials not found") + emitError( + new Error("AWS credentials not configured. Please run 'aws configure' or set up ~/.aws/credentials"), + "Bedrock Authentication Failed" + ) + return + } + + console.log("[claude-auth] AWS credentials validated") + console.log("[claude-auth] AWS region:", finalEnv.AWS_REGION || finalEnv.AWS_DEFAULT_REGION) + console.log("[claude-auth] AWS profile:", finalEnv.AWS_PROFILE || "(default)") + } else { + // OAuth mode: Use existing token logic + if (claudeCodeToken && !hasExistingApiConfig) { + finalEnv.CLAUDE_CODE_OAUTH_TOKEN = claudeCodeToken + console.log("[claude-auth] Using OAuth token") + } + } + // Log auth method being used console.log("[claude-auth] ========== AUTH METHOD USED ==========") + console.log("[claude-auth] Auth mode:", authSettings?.authMode || "oauth (default)") + console.log("[claude-auth] Bedrock mode:", isBedrockMode) console.log( "[claude-auth] hasExistingApiConfig:", hasExistingApiConfig, @@ -1353,6 +1409,10 @@ export const claudeRouter = router({ "[claude-auth] Using CLAUDE_CODE_OAUTH_TOKEN:", !!finalEnv.CLAUDE_CODE_OAUTH_TOKEN, ) + console.log( + "[claude-auth] Using CLAUDE_CODE_USE_BEDROCK:", + !!finalEnv.CLAUDE_CODE_USE_BEDROCK, + ) console.log( "[claude-auth] Using ANTHROPIC_API_KEY:", !!finalEnv.ANTHROPIC_API_KEY, diff --git a/src/main/lib/trpc/routers/index.ts b/src/main/lib/trpc/routers/index.ts index b98b18264..897208b8d 100644 --- a/src/main/lib/trpc/routers/index.ts +++ b/src/main/lib/trpc/routers/index.ts @@ -5,6 +5,7 @@ import { claudeRouter } from "./claude" import { claudeCodeRouter } from "./claude-code" import { claudeSettingsRouter } from "./claude-settings" import { anthropicAccountsRouter } from "./anthropic-accounts" +import { anthropicAuthRouter } from "./anthropic-auth" import { ollamaRouter } from "./ollama" import { codexRouter } from "./codex" import { terminalRouter } from "./terminal" @@ -33,6 +34,7 @@ export function createAppRouter(getWindow: () => BrowserWindow | null) { claudeCode: claudeCodeRouter, claudeSettings: claudeSettingsRouter, anthropicAccounts: anthropicAccountsRouter, + anthropicAuth: anthropicAuthRouter, ollama: ollamaRouter, codex: codexRouter, terminal: terminalRouter, diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 0d1926556..80d950f45 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -11,6 +11,7 @@ import { AgentsLayout } from "./features/layout/agents-layout" import { AnthropicOnboardingPage, ApiKeyOnboardingPage, + BedrockOnboardingPage, BillingMethodPage, CodexOnboardingPage, SelectRepoPage, @@ -19,6 +20,7 @@ import { identify, initAnalytics, shutdown } from "./lib/analytics" import { anthropicOnboardingCompletedAtom, apiKeyOnboardingCompletedAtom, + bedrockOnboardingCompletedAtom, billingMethodAtom, codexOnboardingCompletedAtom, } from "./lib/atoms" @@ -54,6 +56,7 @@ function AppContent() { const apiKeyOnboardingCompleted = useAtomValue(apiKeyOnboardingCompletedAtom) const setApiKeyOnboardingCompleted = useSetAtom(apiKeyOnboardingCompletedAtom) const codexOnboardingCompleted = useAtomValue(codexOnboardingCompletedAtom) + const bedrockOnboardingCompleted = useAtomValue(bedrockOnboardingCompletedAtom) const selectedProject = useAtomValue(selectedProjectAtom) const setSelectedChatId = useSetAtom(selectedAgentChatIdAtom) const { setActiveSubChat, addToOpenSubChats, setChatId } = useAgentSubChatStore() @@ -140,6 +143,10 @@ function AppContent() { return } + if (billingMethod === "aws-bedrock" && !bedrockOnboardingCompleted) { + return + } + if (!validatedProject && !isLoadingProjects) { return } diff --git a/src/renderer/components/dialogs/settings-tabs/agents-authentication-tab.tsx b/src/renderer/components/dialogs/settings-tabs/agents-authentication-tab.tsx new file mode 100644 index 000000000..3e40f34f8 --- /dev/null +++ b/src/renderer/components/dialogs/settings-tabs/agents-authentication-tab.tsx @@ -0,0 +1,268 @@ +import { useState, useEffect } from "react" +import { trpc } from "../../../lib/trpc" +import { Label } from "../../ui/label" +import { Button } from "../../ui/button" +import { Input } from "../../ui/input" +import { toast } from "sonner" +import { CheckCircle2, AlertTriangle, ExternalLink } from "lucide-react" + +function useIsNarrowScreen(): boolean { + const [isNarrow, setIsNarrow] = useState(false) + + useEffect(() => { + const checkWidth = () => { + setIsNarrow(window.innerWidth <= 768) + } + + checkWidth() + window.addEventListener("resize", checkWidth) + return () => window.removeEventListener("resize", checkWidth) + }, []) + + return isNarrow +} + +export function AgentsAuthenticationTab() { + const isNarrowScreen = useIsNarrowScreen() + + const { data: settings, refetch } = trpc.anthropicAuth.getSettings.useQuery() + const { data: awsValidation } = trpc.anthropicAuth.validateAwsCredentials.useQuery() + const updateMutation = trpc.anthropicAuth.updateSettings.useMutation() + + const [authMode, setAuthMode] = useState<"oauth" | "bedrock">("oauth") + const [awsRegion, setAwsRegion] = useState("us-east-1") + const [awsProfile, setAwsProfile] = useState("") + + useEffect(() => { + if (settings) { + setAuthMode(settings.authMode as "oauth" | "bedrock") + setAwsRegion(settings.awsRegion || "us-east-1") + setAwsProfile(settings.awsProfile || "") + } + }, [settings]) + + const handleSave = async () => { + try { + await updateMutation.mutateAsync({ + authMode, + awsRegion: authMode === "bedrock" ? awsRegion : undefined, + awsProfile: authMode === "bedrock" && awsProfile ? awsProfile : undefined, + }) + await refetch() + toast.success("Authentication settings updated") + } catch (error) { + toast.error("Failed to update settings") + console.error(error) + } + } + + const hasChanges = settings && ( + authMode !== settings.authMode || + (authMode === "bedrock" && ( + awsRegion !== (settings.awsRegion || "us-east-1") || + awsProfile !== (settings.awsProfile || "") + )) + ) + + return ( +
+ {/* Header - hidden on narrow screens */} + {!isNarrowScreen && ( +
+

Authentication Method

+

+ Choose how 1Code connects to Claude API +

+
+ )} + + {/* Auth Mode Selection */} +
+ {/* OAuth Option */} +
setAuthMode("oauth")} + > +
+
+ {authMode === "oauth" && ( +
+ )} +
+
+ +

+ Sign in with your Anthropic account. Works with Claude.ai subscriptions. +

+
+
+
+ + {/* Bedrock Option */} +
setAuthMode("bedrock")} + > +
+
+ {authMode === "bedrock" && ( +
+ )} +
+
+
+ +

+ Use AWS CLI credentials from ~/.aws/credentials. Requires Bedrock access. +

+
+ + {authMode === "bedrock" && ( +
e.stopPropagation()}> + {/* AWS Credentials Status */} + {awsValidation && ( +
+
+ {awsValidation.hasAwsCredentials ? ( + <> + +
+
+ AWS credentials detected +
+
+
Profile: {awsValidation.awsProfile}
+ {awsValidation.awsRegion && ( +
Region: {awsValidation.awsRegion}
+ )} + {awsValidation.hasCredentialsFile && ( +
Credentials file: {awsValidation.awsCredentialsPath}
+ )} +
+
+ + ) : ( + <> + +
+
+ No AWS credentials found +
+

+ Please configure AWS CLI credentials before enabling Bedrock mode. +

+ +
+ + )} +
+
+ )} + + {/* AWS Configuration Fields */} +
+ {/* AWS Region */} +
+
+ +

+ Region where Claude on Bedrock is available +

+
+
+ setAwsRegion(e.target.value)} + placeholder="us-east-1" + className="w-full" + /> +
+
+ + {/* AWS Profile (Optional) */} +
+
+ +

+ Leave empty to use default profile +

+
+
+ setAwsProfile(e.target.value)} + placeholder="default" + className="w-full" + /> +
+
+
+ + {/* Setup Instructions */} +
+
First time using Bedrock?
+
    +
  1. + Configure AWS CLI:{" "} + + aws configure + +
  2. +
  3. Enable Claude models in AWS Bedrock console
  4. +
  5. Select "AWS Bedrock" above and click "Save Changes"
  6. +
+
+
+ )} +
+
+
+
+ + {/* Save Button */} +
+ +
+
+ ) +} diff --git a/src/renderer/features/agents/components/agent-model-selector.tsx b/src/renderer/features/agents/components/agent-model-selector.tsx index 4f16735ca..209d4ae75 100644 --- a/src/renderer/features/agents/components/agent-model-selector.tsx +++ b/src/renderer/features/agents/components/agent-model-selector.tsx @@ -46,6 +46,7 @@ interface AgentModelSelectorProps { onSelectedAgentIdChange: (provider: AgentProviderId) => void selectedModelLabel: string allowProviderSwitch?: boolean + isBedrockMode?: boolean triggerClassName?: string contentClassName?: string claude: { @@ -79,6 +80,7 @@ export function AgentModelSelector({ onSelectedAgentIdChange, selectedModelLabel, allowProviderSwitch = true, + isBedrockMode = false, triggerClassName, contentClassName, claude, @@ -127,8 +129,13 @@ export function AgentModelSelector({ > {showClaudeGroup && ( <> -
+
Claude Code + {isBedrockMode && ( + + Bedrock + + )}
{claude.isOffline && claude.ollamaModels.length > 0 ? ( diff --git a/src/renderer/features/agents/hooks/use-auth-mode.ts b/src/renderer/features/agents/hooks/use-auth-mode.ts new file mode 100644 index 000000000..a35253fd0 --- /dev/null +++ b/src/renderer/features/agents/hooks/use-auth-mode.ts @@ -0,0 +1,13 @@ +import { trpc } from "../../../lib/trpc" + +export function useAuthMode() { + const { data: authSettings } = trpc.anthropicAuth.getSettings.useQuery( + undefined, + { staleTime: 60 * 1000 }, // 1 minute - auth mode changes rarely + ) + return { + authMode: (authSettings?.authMode ?? "oauth") as "oauth" | "bedrock", + awsRegion: (authSettings?.awsRegion ?? "us-east-1") as string, + isBedrockMode: authSettings?.authMode === "bedrock", + } +} diff --git a/src/renderer/features/agents/main/chat-input-area.tsx b/src/renderer/features/agents/main/chat-input-area.tsx index 75099e109..8263c90b1 100644 --- a/src/renderer/features/agents/main/chat-input-area.tsx +++ b/src/renderer/features/agents/main/chat-input-area.tsx @@ -41,6 +41,7 @@ import { agentsSettingsDialogOpenAtom, anthropicOnboardingCompletedAtom, apiKeyOnboardingCompletedAtom, + bedrockOnboardingCompletedAtom, codexApiKeyAtom, codexOnboardingCompletedAtom, customClaudeConfigAtom, @@ -91,6 +92,8 @@ import { AgentPastedTextItem } from "../ui/agent-pasted-text-item" import { AgentTextContextItem } from "../ui/agent-text-context-item" import { VoiceWaveIndicator } from "../ui/voice-wave-indicator" import { McpStatusDot } from "../../../components/dialogs/settings-tabs/agents-mcp-tab" +import { useAuthMode } from "../hooks/use-auth-mode" +import { ProviderStatusBadge } from "../ui/provider-status-badge" import { handlePasteEvent } from "../utils/paste-text" import type { PastedTextFile } from "../hooks/use-pasted-text-files" import { @@ -465,6 +468,7 @@ export const ChatInputArea = memo(function ChatInputArea({ // Connection status for providers const anthropicOnboardingCompleted = useAtomValue(anthropicOnboardingCompletedAtom) const apiKeyOnboardingCompleted = useAtomValue(apiKeyOnboardingCompletedAtom) + const bedrockOnboardingCompleted = useAtomValue(bedrockOnboardingCompletedAtom) const codexOnboardingCompleted = useAtomValue(codexOnboardingCompletedAtom) const codexUiModels = useMemo( () => { @@ -534,6 +538,9 @@ export const ChatInputArea = memo(function ChatInputArea({ // Extended thinking (reasoning) toggle const [thinkingEnabled, setThinkingEnabled] = useAtom(extendedThinkingEnabledAtom) + // Auth mode (OAuth vs Bedrock) + const { isBedrockMode } = useAuthMode() + const selectedModelLabel = useMemo(() => { if (provider === "codex") { return selectedCodexModel.name @@ -1467,6 +1474,7 @@ export const ChatInputArea = memo(function ChatInputArea({ onProviderChange?.(nextProvider) }} allowProviderSwitch={canSwitchProvider} + isBedrockMode={isBedrockMode} selectedModelLabel={selectedModelLabel} claude={{ models: availableModels.models.filter((m) => !hiddenModels.includes(m.id)), @@ -1485,7 +1493,7 @@ export const ChatInputArea = memo(function ChatInputArea({ selectedOllamaModel: currentOllamaModel, recommendedOllamaModel: availableModels.recommendedModel, onSelectOllamaModel: setSelectedOllamaModel, - isConnected: anthropicOnboardingCompleted || apiKeyOnboardingCompleted || hasCustomClaudeConfig, + isConnected: anthropicOnboardingCompleted || apiKeyOnboardingCompleted || bedrockOnboardingCompleted || hasCustomClaudeConfig, thinkingEnabled, onThinkingChange: setThinkingEnabled, }} @@ -1513,6 +1521,8 @@ export const ChatInputArea = memo(function ChatInputArea({ />
+ +
diff --git a/src/renderer/features/agents/main/new-chat-form.tsx b/src/renderer/features/agents/main/new-chat-form.tsx index dc55b0211..cd725ce80 100644 --- a/src/renderer/features/agents/main/new-chat-form.tsx +++ b/src/renderer/features/agents/main/new-chat-form.tsx @@ -55,6 +55,7 @@ import { agentsSettingsDialogActiveTabAtom, anthropicOnboardingCompletedAtom, apiKeyOnboardingCompletedAtom, + bedrockOnboardingCompletedAtom, codexApiKeyAtom, codexOnboardingCompletedAtom, customClaudeConfigAtom, @@ -107,6 +108,7 @@ import { import { agentsSidebarOpenAtom, agentsUnseenChangesAtom } from "../atoms" import { AgentSendButton } from "../components/agent-send-button" import { AgentModelSelector } from "../components/agent-model-selector" +import { useAuthMode } from "../hooks/use-auth-mode" import { CreateBranchDialog } from "../components/create-branch-dialog" import { formatTimeAgo } from "../utils/format-time-ago" import { handlePasteEvent } from "../utils/paste-text" @@ -243,7 +245,9 @@ export function NewChatForm({ // Connection status for providers const anthropicOnboardingCompleted = useAtomValue(anthropicOnboardingCompletedAtom) const apiKeyOnboardingCompleted = useAtomValue(apiKeyOnboardingCompletedAtom) + const bedrockOnboardingCompleted = useAtomValue(bedrockOnboardingCompletedAtom) const codexOnboardingCompleted = useAtomValue(codexOnboardingCompletedAtom) + const { isBedrockMode } = useAuthMode() const setSettingsDialogOpen = useSetAtom(agentsSettingsDialogOpenAtom) const setSettingsActiveTab = useSetAtom(agentsSettingsDialogActiveTabAtom) const setJustCreatedIds = useSetAtom(justCreatedIdsAtom) @@ -1874,6 +1878,7 @@ export function NewChatForm({ } setLastSelectedAgentId(provider) }} + isBedrockMode={isBedrockMode} selectedModelLabel={selectedModelLabel} claude={{ models: availableModels.models.filter((m) => !hiddenModels.includes(m.id)), @@ -1892,7 +1897,7 @@ export function NewChatForm({ selectedOllamaModel: currentOllamaModel, recommendedOllamaModel: availableModels.recommendedModel, onSelectOllamaModel: setSelectedOllamaModel, - isConnected: anthropicOnboardingCompleted || apiKeyOnboardingCompleted || hasCustomClaudeConfig, + isConnected: anthropicOnboardingCompleted || apiKeyOnboardingCompleted || bedrockOnboardingCompleted || hasCustomClaudeConfig, thinkingEnabled, onThinkingChange: setThinkingEnabled, }} diff --git a/src/renderer/features/agents/ui/provider-status-badge.tsx b/src/renderer/features/agents/ui/provider-status-badge.tsx new file mode 100644 index 000000000..aa760d599 --- /dev/null +++ b/src/renderer/features/agents/ui/provider-status-badge.tsx @@ -0,0 +1,99 @@ +"use client" + +import { useAtomValue } from "jotai" +import { Cloud } from "lucide-react" +import { memo } from "react" +import { Button } from "../../../components/ui/button" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "../../../components/ui/tooltip" +import { ClaudeCodeIcon, KeyFilledIcon } from "../../../components/ui/icons" +import { + billingMethodAtom, +} from "../../../lib/atoms" +import { useAuthMode } from "../hooks/use-auth-mode" + +/** + * Provider Status Badge + * + * Shows the active authentication provider in the chat input toolbar: + * - "Bedrock" with Cloud icon when using AWS Bedrock + * - "API Key" with Key icon when using direct API key + * - Nothing for default OAuth mode (keeps UI clean) + */ +export const ProviderStatusBadge = memo(function ProviderStatusBadge() { + const { isBedrockMode, awsRegion } = useAuthMode() + const billingMethod = useAtomValue(billingMethodAtom) + + // Only show badge for non-default auth modes + if (!isBedrockMode && billingMethod !== "api-key" && billingMethod !== "custom-model") { + return null + } + + if (isBedrockMode) { + return ( + + + + + + Connected via AWS Bedrock ({awsRegion}) + + + ) + } + + if (billingMethod === "api-key") { + return ( + + + + + + Connected via Anthropic API Key + + + ) + } + + if (billingMethod === "custom-model") { + return ( + + + + + + Connected via custom model configuration + + + ) + } + + return null +}) diff --git a/src/renderer/features/onboarding/bedrock-onboarding-page.tsx b/src/renderer/features/onboarding/bedrock-onboarding-page.tsx new file mode 100644 index 000000000..38cde0ef1 --- /dev/null +++ b/src/renderer/features/onboarding/bedrock-onboarding-page.tsx @@ -0,0 +1,195 @@ +"use client" + +import { useSetAtom } from "jotai" +import { useState } from "react" +import { ChevronLeft, Cloud, CheckCircle2, AlertCircle } from "lucide-react" + +import { IconSpinner } from "../../components/ui/icons" +import { Input } from "../../components/ui/input" +import { Label } from "../../components/ui/label" +import { Logo } from "../../components/ui/logo" +import { + bedrockOnboardingCompletedAtom, + billingMethodAtom, +} from "../../lib/atoms" +import { trpc } from "../../lib/trpc" +import { cn } from "../../lib/utils" + +export function BedrockOnboardingPage() { + const setBillingMethod = useSetAtom(billingMethodAtom) + const setBedrockOnboardingCompleted = useSetAtom(bedrockOnboardingCompletedAtom) + + const [region, setRegion] = useState("us-east-1") + const [profile, setProfile] = useState("") + const [isSubmitting, setIsSubmitting] = useState(false) + + // Validate AWS credentials via backend + const { data: validation, isLoading: isValidating } = + trpc.anthropicAuth.validateAwsCredentials.useQuery(undefined, { + staleTime: 10 * 1000, + }) + + const updateSettings = trpc.anthropicAuth.updateSettings.useMutation() + + const handleBack = () => { + setBillingMethod(null) + } + + const handleConnect = async () => { + setIsSubmitting(true) + try { + await updateSettings.mutateAsync({ + authMode: "bedrock", + awsRegion: region.trim() || "us-east-1", + awsProfile: profile.trim() || undefined, + }) + setBedrockOnboardingCompleted(true) + } catch (error) { + console.error("[bedrock-onboarding] Failed to save settings:", error) + } finally { + setIsSubmitting(false) + } + } + + const hasCredentials = validation?.hasAwsCredentials ?? false + const canSubmit = region.trim().length > 0 + + return ( +
+ {/* Draggable title bar area */} +
+ + {/* Back button */} + + +
+ {/* Header */} +
+
+
+ +
+
+ +
+
+
+

+ Connect AWS Bedrock +

+

+ Use Claude models via your AWS account +

+
+
+ + {/* Credential Status */} +
+ {isValidating ? ( + + ) : hasCredentials ? ( + + ) : ( + + )} +
+ {isValidating ? ( + + Checking AWS credentials... + + ) : hasCredentials ? ( + + AWS credentials detected + {validation?.hasEnvCredentials + ? " (environment variables)" + : validation?.hasProfile + ? ` (profile: ${validation.awsProfile})` + : " (~/.aws/credentials)"} + + ) : ( + + No AWS credentials found. Run{" "} + + aws configure + {" "} + or set environment variables. + + )} +
+
+ + {/* Form Fields */} +
+ {/* AWS Region */} +
+ + setRegion(e.target.value)} + placeholder="us-east-1" + className="w-full" + /> +

+ Region where Bedrock is enabled (e.g. us-east-1, us-west-2, eu-west-1) +

+
+ + {/* AWS Profile (optional) */} +
+ + setProfile(e.target.value)} + placeholder="default" + className="w-full" + /> +

+ Named profile from ~/.aws/credentials. Leave empty for default. +

+
+
+ + {/* Connect Button */} + + + {/* Help text */} +

+ Requires AWS CLI configured with{" "} + aws configure{" "} + or AWS environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY). +

+
+
+ ) +} diff --git a/src/renderer/features/onboarding/billing-method-page.tsx b/src/renderer/features/onboarding/billing-method-page.tsx index 68abadabe..3638c591e 100644 --- a/src/renderer/features/onboarding/billing-method-page.tsx +++ b/src/renderer/features/onboarding/billing-method-page.tsx @@ -4,6 +4,7 @@ import { useSetAtom } from "jotai" import { Check } from "lucide-react" import { useMemo, useState } from "react" +import { Cloud } from "lucide-react" import { ClaudeCodeIcon, CodexIcon, @@ -55,6 +56,14 @@ const billingOptions: BillingOption[] = [ subtitle: "Use a custom base URL and model.", icon: , }, + { + id: "aws-bedrock", + method: "aws-bedrock", + group: "claude-code", + title: "AWS Bedrock", + subtitle: "Use Claude via your AWS account credentials.", + icon: , + }, { id: "codex-subscription", method: "codex-subscription", diff --git a/src/renderer/features/onboarding/index.ts b/src/renderer/features/onboarding/index.ts index dde1275c2..ca8a69dbd 100644 --- a/src/renderer/features/onboarding/index.ts +++ b/src/renderer/features/onboarding/index.ts @@ -1,5 +1,6 @@ export { AnthropicOnboardingPage } from "./anthropic-onboarding-page" export { ApiKeyOnboardingPage } from "./api-key-onboarding-page" +export { BedrockOnboardingPage } from "./bedrock-onboarding-page" export { BillingMethodPage } from "./billing-method-page" export { CodexOnboardingPage } from "./codex-onboarding-page" export { SelectRepoPage } from "./select-repo-page" diff --git a/src/renderer/features/settings/settings-content.tsx b/src/renderer/features/settings/settings-content.tsx index fa4ad6d78..1477bec2f 100644 --- a/src/renderer/features/settings/settings-content.tsx +++ b/src/renderer/features/settings/settings-content.tsx @@ -6,6 +6,7 @@ import { } from "../../lib/atoms" import { desktopViewAtom } from "../agents/atoms" import { AgentsAppearanceTab } from "../../components/dialogs/settings-tabs/agents-appearance-tab" +import { AgentsAuthenticationTab } from "../../components/dialogs/settings-tabs/agents-authentication-tab" import { AgentsBetaTab } from "../../components/dialogs/settings-tabs/agents-beta-tab" import { AgentsCustomAgentsTab } from "../../components/dialogs/settings-tabs/agents-custom-agents-tab" import { AgentsDebugTab } from "../../components/dialogs/settings-tabs/agents-debug-tab" @@ -43,6 +44,8 @@ export function SettingsContent() { switch (activeTab) { case "profile": return + case "authentication": + return case "appearance": return case "keyboard": diff --git a/src/renderer/features/settings/settings-sidebar.tsx b/src/renderer/features/settings/settings-sidebar.tsx index e21380cc2..d2773dbb9 100644 --- a/src/renderer/features/settings/settings-sidebar.tsx +++ b/src/renderer/features/settings/settings-sidebar.tsx @@ -20,6 +20,7 @@ import { FlaskFilledIcon, FolderFilledIcon, KeyboardFilledIcon, + KeyFilledIcon, OriginalMCPIcon, PluginFilledIcon, SkillIconFilled, @@ -44,6 +45,11 @@ const MAIN_TABS = [ label: "Account", icon: ProfileIconFilled, }, + { + id: "authentication" as SettingsTab, + label: "Authentication", + icon: KeyFilledIcon, + }, { id: "appearance" as SettingsTab, label: "Appearance", diff --git a/src/renderer/lib/atoms/index.ts b/src/renderer/lib/atoms/index.ts index 3cc7484c0..cedee9c83 100644 --- a/src/renderer/lib/atoms/index.ts +++ b/src/renderer/lib/atoms/index.ts @@ -183,6 +183,7 @@ export const clearSubChatSelectionAtom = atom(null, (_get, set) => { // Settings dialog export type SettingsTab = | "profile" + | "authentication" | "appearance" | "preferences" | "models" @@ -742,6 +743,7 @@ export type BillingMethod = | "claude-subscription" | "api-key" | "custom-model" + | "aws-bedrock" | "codex-subscription" | "codex-api-key" | null @@ -772,6 +774,15 @@ export const apiKeyOnboardingCompletedAtom = atomWithStorage( { getOnInit: true }, ) +// Whether user has completed AWS Bedrock configuration during onboarding +// Only relevant when billingMethod is "aws-bedrock" +export const bedrockOnboardingCompletedAtom = atomWithStorage( + "onboarding:bedrock-completed", + false, + undefined, + { getOnInit: true }, +) + // Whether user has completed Codex auth during onboarding // Only relevant when billingMethod is a Codex method export const codexOnboardingCompletedAtom = atomWithStorage(