diff --git a/app/src-tauri/src/commands/project_commands.rs b/app/src-tauri/src/commands/project_commands.rs index 8451964..d8c5c7e 100644 --- a/app/src-tauri/src/commands/project_commands.rs +++ b/app/src-tauri/src/commands/project_commands.rs @@ -69,6 +69,15 @@ pub async fn start_project_container( // Auth state persists in the .claude config volume. None } + AuthMode::Bedrock => { + // Bedrock mode: no Anthropic API key needed, uses AWS credentials. + let bedrock = project.bedrock_config.as_ref() + .ok_or_else(|| "Bedrock auth mode selected but no Bedrock configuration found.".to_string())?; + if bedrock.aws_region.is_empty() { + return Err("AWS region is required for Bedrock auth mode.".to_string()); + } + None + } }; // Update status to starting diff --git a/app/src-tauri/src/docker/container.rs b/app/src-tauri/src/docker/container.rs index b8d47bb..7fedf98 100644 --- a/app/src-tauri/src/docker/container.rs +++ b/app/src-tauri/src/docker/container.rs @@ -6,7 +6,7 @@ use bollard::models::{ContainerSummary, HostConfig, Mount, MountTypeEnum}; use std::collections::HashMap; use super::client::get_docker; -use crate::models::{container_config, ContainerInfo, Project}; +use crate::models::{container_config, AuthMode, BedrockAuthMethod, ContainerInfo, Project}; pub async fn find_existing_container(project: &Project) -> Result, String> { let docker = get_docker()?; @@ -78,6 +78,46 @@ pub async fn create_container( env_vars.push(format!("GIT_USER_EMAIL={}", email)); } + // Bedrock configuration + if project.auth_mode == AuthMode::Bedrock { + if let Some(ref bedrock) = project.bedrock_config { + env_vars.push("CLAUDE_CODE_USE_BEDROCK=1".to_string()); + env_vars.push(format!("AWS_REGION={}", bedrock.aws_region)); + + match bedrock.auth_method { + BedrockAuthMethod::StaticCredentials => { + if let Some(ref key_id) = bedrock.aws_access_key_id { + env_vars.push(format!("AWS_ACCESS_KEY_ID={}", key_id)); + } + if let Some(ref secret) = bedrock.aws_secret_access_key { + env_vars.push(format!("AWS_SECRET_ACCESS_KEY={}", secret)); + } + if let Some(ref token) = bedrock.aws_session_token { + env_vars.push(format!("AWS_SESSION_TOKEN={}", token)); + } + } + BedrockAuthMethod::Profile => { + if let Some(ref profile) = bedrock.aws_profile { + env_vars.push(format!("AWS_PROFILE={}", profile)); + } + } + BedrockAuthMethod::BearerToken => { + if let Some(ref token) = bedrock.aws_bearer_token { + env_vars.push(format!("AWS_BEARER_TOKEN_BEDROCK={}", token)); + } + } + } + + if let Some(ref model) = bedrock.model_id { + env_vars.push(format!("ANTHROPIC_MODEL={}", model)); + } + + if bedrock.disable_prompt_caching { + env_vars.push("DISABLE_PROMPT_CACHING=1".to_string()); + } + } + } + let mut mounts = vec![ // Project directory -> /workspace Mount { @@ -108,6 +148,26 @@ pub async fn create_container( }); } + // AWS config mount (read-only, for profile-based auth) + if project.auth_mode == AuthMode::Bedrock { + if let Some(ref bedrock) = project.bedrock_config { + if bedrock.auth_method == BedrockAuthMethod::Profile { + if let Some(home) = dirs::home_dir() { + let aws_dir = home.join(".aws"); + if aws_dir.exists() { + mounts.push(Mount { + target: Some("/home/claude/.aws".to_string()), + source: Some(aws_dir.to_string_lossy().to_string()), + typ: Some(MountTypeEnum::BIND), + read_only: Some(true), + ..Default::default() + }); + } + } + } + } + } + // Docker socket (only if allowed) if project.allow_docker_access { mounts.push(Mount { diff --git a/app/src-tauri/src/models/project.rs b/app/src-tauri/src/models/project.rs index fb577ff..4f8d9f7 100644 --- a/app/src-tauri/src/models/project.rs +++ b/app/src-tauri/src/models/project.rs @@ -8,6 +8,7 @@ pub struct Project { pub container_id: Option, pub status: ProjectStatus, pub auth_mode: AuthMode, + pub bedrock_config: Option, pub allow_docker_access: bool, pub ssh_key_path: Option, pub git_token: Option, @@ -30,11 +31,13 @@ pub enum ProjectStatus { /// How the project authenticates with Claude. /// - `Login`: User runs `claude login` inside the container (OAuth, persisted via config volume) /// - `ApiKey`: Uses the API key stored in the OS keychain +/// - `Bedrock`: Uses AWS Bedrock with per-project AWS credentials #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum AuthMode { Login, ApiKey, + Bedrock, } impl Default for AuthMode { @@ -43,6 +46,35 @@ impl Default for AuthMode { } } +/// How Bedrock authenticates with AWS. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum BedrockAuthMethod { + StaticCredentials, + Profile, + BearerToken, +} + +impl Default for BedrockAuthMethod { + fn default() -> Self { + Self::StaticCredentials + } +} + +/// AWS Bedrock configuration for a project. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BedrockConfig { + pub auth_method: BedrockAuthMethod, + pub aws_region: String, + pub aws_access_key_id: Option, + pub aws_secret_access_key: Option, + pub aws_session_token: Option, + pub aws_profile: Option, + pub aws_bearer_token: Option, + pub model_id: Option, + pub disable_prompt_caching: bool, +} + impl Project { pub fn new(name: String, path: String) -> Self { let now = chrono::Utc::now().to_rfc3339(); @@ -53,6 +85,7 @@ impl Project { container_id: None, status: ProjectStatus::Stopped, auth_mode: AuthMode::default(), + bedrock_config: None, allow_docker_access: false, ssh_key_path: None, git_token: None, diff --git a/app/src/components/projects/ProjectCard.tsx b/app/src/components/projects/ProjectCard.tsx index 123af60..6736305 100644 --- a/app/src/components/projects/ProjectCard.tsx +++ b/app/src/components/projects/ProjectCard.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { open } from "@tauri-apps/plugin-dialog"; -import type { Project, AuthMode } from "../../lib/types"; +import type { Project, AuthMode, BedrockConfig, BedrockAuthMethod } from "../../lib/types"; import { useProjects } from "../../hooks/useProjects"; import { useTerminal } from "../../hooks/useTerminal"; import { useAppState } from "../../store/appState"; @@ -51,14 +51,37 @@ export default function ProjectCard({ project }: Props) { } }; + const defaultBedrockConfig: BedrockConfig = { + auth_method: "static_credentials", + aws_region: "us-east-1", + aws_access_key_id: null, + aws_secret_access_key: null, + aws_session_token: null, + aws_profile: null, + aws_bearer_token: null, + model_id: null, + disable_prompt_caching: false, + }; + const handleAuthModeChange = async (mode: AuthMode) => { try { - await update({ ...project, auth_mode: mode }); + const updates: Partial = { auth_mode: mode }; + if (mode === "bedrock" && !project.bedrock_config) { + updates.bedrock_config = defaultBedrockConfig; + } + await update({ ...project, ...updates }); } catch (e) { setError(String(e)); } }; + const updateBedrockConfig = async (patch: Partial) => { + try { + const current = project.bedrock_config ?? defaultBedrockConfig; + await update({ ...project, bedrock_config: { ...current, ...patch } }); + } catch {} + }; + const handleBrowseSSH = async () => { const selected = await open({ directory: true, multiple: false }); if (selected) { @@ -122,16 +145,24 @@ export default function ProjectCard({ project }: Props) { > API key + {/* Action buttons */}
{isStopped ? ( - - ) : project.status === "running" ? ( <> - - + { setLoading(true); @@ -142,6 +173,11 @@ export default function ProjectCard({ project }: Props) { label="Reset" /> + ) : project.status === "running" ? ( + <> + + + ) : ( {project.status}... @@ -250,6 +286,124 @@ export default function ProjectCard({ project }: Props) { {project.allow_docker_access ? "ON" : "OFF"}
+ + {/* Bedrock config */} + {project.auth_mode === "bedrock" && (() => { + const bc = project.bedrock_config ?? defaultBedrockConfig; + const inputCls = "w-full px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50"; + return ( +
+ + + {/* Sub-method selector */} +
+ Method: + {(["static_credentials", "profile", "bearer_token"] as BedrockAuthMethod[]).map((m) => ( + + ))} +
+ + {/* AWS Region (always shown) */} +
+ + updateBedrockConfig({ aws_region: e.target.value })} + placeholder="us-east-1" + disabled={!isStopped} + className={inputCls} + /> +
+ + {/* Static credentials fields */} + {bc.auth_method === "static_credentials" && ( + <> +
+ + updateBedrockConfig({ aws_access_key_id: e.target.value || null })} + placeholder="AKIA..." + disabled={!isStopped} + className={inputCls} + /> +
+
+ + updateBedrockConfig({ aws_secret_access_key: e.target.value || null })} + disabled={!isStopped} + className={inputCls} + /> +
+
+ + updateBedrockConfig({ aws_session_token: e.target.value || null })} + disabled={!isStopped} + className={inputCls} + /> +
+ + )} + + {/* Profile field */} + {bc.auth_method === "profile" && ( +
+ + updateBedrockConfig({ aws_profile: e.target.value || null })} + placeholder="default" + disabled={!isStopped} + className={inputCls} + /> +
+ )} + + {/* Bearer token field */} + {bc.auth_method === "bearer_token" && ( +
+ + updateBedrockConfig({ aws_bearer_token: e.target.value || null })} + disabled={!isStopped} + className={inputCls} + /> +
+ )} + + {/* Model override */} +
+ + updateBedrockConfig({ model_id: e.target.value || null })} + placeholder="anthropic.claude-sonnet-4-20250514-v1:0" + disabled={!isStopped} + className={inputCls} + /> +
+
+ ); + })()} )} diff --git a/app/src/components/settings/ApiKeyInput.tsx b/app/src/components/settings/ApiKeyInput.tsx index 1879811..21d5396 100644 --- a/app/src/components/settings/ApiKeyInput.tsx +++ b/app/src/components/settings/ApiKeyInput.tsx @@ -25,7 +25,7 @@ export default function ApiKeyInput() {

- Each project can use either claude login (OAuth, run inside the terminal) or an API key. Set auth mode per-project. + Each project can use claude login (OAuth, run inside the terminal), an API key, or AWS Bedrock. Set auth mode per-project.