From da078af73fe084306a216b84f81ef10cbc4971f5 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 1 Mar 2026 03:59:58 +0000 Subject: [PATCH] Remove Anthropic API key authentication support API key auth only provides short-lived session tokens (8hrs or until session restart) with no refresh mechanism, unlike OAuth which persists via .credentials.json. Remove the non-functional API key settings UI and all supporting code (frontend state, Tauri commands, keyring storage, container env var injection, and fingerprint-based recreation checks) to avoid user confusion. Co-Authored-By: Claude Opus 4.6 --- .../src/commands/project_commands.rs | 25 +++----- .../src/commands/settings_commands.rs | 16 ----- app/src-tauri/src/docker/container.rs | 28 --------- app/src-tauri/src/lib.rs | 3 - app/src-tauri/src/storage/secure.rs | 39 ------------ app/src/App.tsx | 3 +- app/src/components/settings/ApiKeyInput.tsx | 60 +------------------ app/src/hooks/useSettings.ts | 32 +--------- app/src/lib/tauri-commands.ts | 4 -- app/src/store/appState.ts | 6 -- 10 files changed, 11 insertions(+), 205 deletions(-) diff --git a/app/src-tauri/src/commands/project_commands.rs b/app/src-tauri/src/commands/project_commands.rs index e2d9ea6..92b72f9 100644 --- a/app/src-tauri/src/commands/project_commands.rs +++ b/app/src-tauri/src/commands/project_commands.rs @@ -124,21 +124,15 @@ pub async fn start_project_container( let settings = state.settings_store.get(); let image_name = container_config::resolve_image_name(&settings.image_source, &settings.custom_image_name); - // Get API key only if auth mode requires it - let api_key: Option = match project.auth_mode { - AuthMode::Anthropic => { - None + // Validate auth mode requirements + if project.auth_mode == AuthMode::Bedrock { + let bedrock = project.bedrock_config.as_ref() + .ok_or_else(|| "Bedrock auth mode selected but no Bedrock configuration found.".to_string())?; + // Region can come from per-project or global + if bedrock.aws_region.is_empty() && settings.global_aws.aws_region.is_none() { + return Err("AWS region is required for Bedrock auth mode. Set it per-project or in global AWS settings.".to_string()); } - AuthMode::Bedrock => { - let bedrock = project.bedrock_config.as_ref() - .ok_or_else(|| "Bedrock auth mode selected but no Bedrock configuration found.".to_string())?; - // Region can come from per-project or global - if bedrock.aws_region.is_empty() && settings.global_aws.aws_region.is_none() { - return Err("AWS region is required for Bedrock auth mode. Set it per-project or in global AWS settings.".to_string()); - } - None - } - }; + } // Update status to starting state.projects_store.update_status(&project_id, ProjectStatus::Starting)?; @@ -164,7 +158,6 @@ pub async fn start_project_container( let needs_recreation = docker::container_needs_recreation( &existing_id, &project, - api_key.as_deref(), settings.global_claude_instructions.as_deref(), &settings.global_custom_env_vars, ) @@ -176,7 +169,6 @@ pub async fn start_project_container( docker::remove_container(&existing_id).await?; let new_id = docker::create_container( &project, - api_key.as_deref(), &docker_socket, &image_name, aws_config_path.as_deref(), @@ -193,7 +185,6 @@ pub async fn start_project_container( } else { let new_id = docker::create_container( &project, - api_key.as_deref(), &docker_socket, &image_name, aws_config_path.as_deref(), diff --git a/app/src-tauri/src/commands/settings_commands.rs b/app/src-tauri/src/commands/settings_commands.rs index 5203509..811f59d 100644 --- a/app/src-tauri/src/commands/settings_commands.rs +++ b/app/src-tauri/src/commands/settings_commands.rs @@ -2,24 +2,8 @@ use tauri::State; use crate::docker; use crate::models::AppSettings; -use crate::storage::secure; use crate::AppState; -#[tauri::command] -pub async fn set_api_key(key: String) -> Result<(), String> { - secure::store_api_key(&key) -} - -#[tauri::command] -pub async fn has_api_key() -> Result { - secure::has_api_key() -} - -#[tauri::command] -pub async fn delete_api_key() -> Result<(), String> { - secure::delete_api_key() -} - #[tauri::command] pub async fn get_settings(state: State<'_, AppState>) -> Result { Ok(state.settings_store.get()) diff --git a/app/src-tauri/src/docker/container.rs b/app/src-tauri/src/docker/container.rs index 3c85434..b4cf3fb 100644 --- a/app/src-tauri/src/docker/container.rs +++ b/app/src-tauri/src/docker/container.rs @@ -10,19 +10,6 @@ use std::hash::{Hash, Hasher}; use super::client::get_docker; use crate::models::{AuthMode, BedrockAuthMethod, ContainerInfo, EnvVar, GlobalAwsSettings, Project, ProjectPath}; -/// Compute a fingerprint for the API key so we can detect when it changes -/// without storing the actual key in Docker labels. -fn compute_api_key_fingerprint(api_key: Option<&str>) -> String { - match api_key { - Some(key) => { - let mut hasher = DefaultHasher::new(); - key.hash(&mut hasher); - format!("{:x}", hasher.finish()) - } - None => String::new(), - } -} - /// Compute a fingerprint string for the custom environment variables. /// Sorted alphabetically so order changes do not cause spurious recreation. fn compute_env_fingerprint(custom_env_vars: &[EnvVar]) -> String { @@ -140,7 +127,6 @@ pub async fn find_existing_container(project: &Project) -> Result pub async fn create_container( project: &Project, - api_key: Option<&str>, docker_socket_path: &str, image_name: &str, aws_config_path: Option<&str>, @@ -189,10 +175,6 @@ pub async fn create_container( log::debug!("Skipping HOST_UID/HOST_GID on Windows — Docker Desktop's Linux VM handles user mapping"); } - if let Some(key) = api_key { - env_vars.push(format!("ANTHROPIC_API_KEY={}", key)); - } - if let Some(ref token) = project.git_token { env_vars.push(format!("GIT_TOKEN={}", token)); } @@ -369,7 +351,6 @@ pub async fn create_container( labels.insert("triple-c.project-id".to_string(), project.id.clone()); labels.insert("triple-c.project-name".to_string(), project.name.clone()); labels.insert("triple-c.auth-mode".to_string(), format!("{:?}", project.auth_mode)); - labels.insert("triple-c.api-key-fingerprint".to_string(), compute_api_key_fingerprint(api_key)); labels.insert("triple-c.paths-fingerprint".to_string(), compute_paths_fingerprint(&project.paths)); labels.insert("triple-c.bedrock-fingerprint".to_string(), compute_bedrock_fingerprint(project)); labels.insert("triple-c.image".to_string(), image_name.to_string()); @@ -453,7 +434,6 @@ pub async fn remove_container(container_id: &str) -> Result<(), String> { pub async fn container_needs_recreation( container_id: &str, project: &Project, - api_key: Option<&str>, global_claude_instructions: Option<&str>, global_custom_env_vars: &[EnvVar], ) -> Result { @@ -492,14 +472,6 @@ pub async fn container_needs_recreation( } } - // ── API key fingerprint ───────────────────────────────────────────── - let expected_api_key_fp = compute_api_key_fingerprint(api_key); - let container_api_key_fp = get_label("triple-c.api-key-fingerprint").unwrap_or_default(); - if container_api_key_fp != expected_api_key_fp { - log::info!("API key fingerprint mismatch, triggering recreation"); - return Ok(true); - } - // ── Project paths fingerprint ────────────────────────────────────────── let expected_paths_fp = compute_paths_fingerprint(&project.paths); match get_label("triple-c.paths-fingerprint") { diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 3798f5d..1844397 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -79,9 +79,6 @@ pub fn run() { commands::project_commands::stop_project_container, commands::project_commands::rebuild_project_container, // Settings - commands::settings_commands::set_api_key, - commands::settings_commands::has_api_key, - commands::settings_commands::delete_api_key, commands::settings_commands::get_settings, commands::settings_commands::update_settings, commands::settings_commands::pull_image, diff --git a/app/src-tauri/src/storage/secure.rs b/app/src-tauri/src/storage/secure.rs index e5e7dbd..ba47154 100644 --- a/app/src-tauri/src/storage/secure.rs +++ b/app/src-tauri/src/storage/secure.rs @@ -1,42 +1,3 @@ -const SERVICE_NAME: &str = "triple-c"; -const API_KEY_USER: &str = "anthropic-api-key"; - -pub fn store_api_key(key: &str) -> Result<(), String> { - let entry = keyring::Entry::new(SERVICE_NAME, API_KEY_USER) - .map_err(|e| format!("Keyring error: {}", e))?; - entry - .set_password(key) - .map_err(|e| format!("Failed to store API key: {}", e)) -} - -pub fn get_api_key() -> Result, String> { - let entry = keyring::Entry::new(SERVICE_NAME, API_KEY_USER) - .map_err(|e| format!("Keyring error: {}", e))?; - match entry.get_password() { - Ok(key) => Ok(Some(key)), - Err(keyring::Error::NoEntry) => Ok(None), - Err(e) => Err(format!("Failed to retrieve API key: {}", e)), - } -} - -pub fn delete_api_key() -> Result<(), String> { - let entry = keyring::Entry::new(SERVICE_NAME, API_KEY_USER) - .map_err(|e| format!("Keyring error: {}", e))?; - match entry.delete_credential() { - Ok(()) => Ok(()), - Err(keyring::Error::NoEntry) => Ok(()), - Err(e) => Err(format!("Failed to delete API key: {}", e)), - } -} - -pub fn has_api_key() -> Result { - match get_api_key() { - Ok(Some(_)) => Ok(true), - Ok(None) => Ok(false), - Err(e) => Err(e), - } -} - /// Store a per-project secret in the OS keychain. pub fn store_project_secret(project_id: &str, key_name: &str, value: &str) -> Result<(), String> { let service = format!("triple-c-project-{}-{}", project_id, key_name); diff --git a/app/src/App.tsx b/app/src/App.tsx index 29eb402..7e9a9fc 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -12,7 +12,7 @@ import { useAppState } from "./store/appState"; export default function App() { const { checkDocker, checkImage } = useDocker(); - const { checkApiKey, loadSettings } = useSettings(); + const { loadSettings } = useSettings(); const { refresh } = useProjects(); const { loadVersion, checkForUpdates, startPeriodicCheck } = useUpdates(); const { sessions, activeSessionId } = useAppState( @@ -25,7 +25,6 @@ export default function App() { checkDocker().then((available) => { if (available) checkImage(); }); - checkApiKey(); refresh(); // Update detection diff --git a/app/src/components/settings/ApiKeyInput.tsx b/app/src/components/settings/ApiKeyInput.tsx index 21d5396..7cff023 100644 --- a/app/src/components/settings/ApiKeyInput.tsx +++ b/app/src/components/settings/ApiKeyInput.tsx @@ -1,68 +1,10 @@ -import { useState } from "react"; -import { useSettings } from "../../hooks/useSettings"; - export default function ApiKeyInput() { - const { hasKey, saveApiKey, removeApiKey } = useSettings(); - const [key, setKey] = useState(""); - const [error, setError] = useState(null); - const [saving, setSaving] = useState(false); - - const handleSave = async () => { - if (!key.trim()) return; - setSaving(true); - setError(null); - try { - await saveApiKey(key.trim()); - setKey(""); - } catch (e) { - setError(String(e)); - } finally { - setSaving(false); - } - }; - return (

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

- - - {hasKey ? ( -
- Key configured - -
- ) : ( -
- setKey(e.target.value)} - placeholder="sk-ant-..." - className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-sm text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]" - onKeyDown={(e) => e.key === "Enter" && handleSave()} - /> - -
- )} - {error &&
{error}
}
); } diff --git a/app/src/hooks/useSettings.ts b/app/src/hooks/useSettings.ts index 022372b..fb83d1a 100644 --- a/app/src/hooks/useSettings.ts +++ b/app/src/hooks/useSettings.ts @@ -5,39 +5,13 @@ import * as commands from "../lib/tauri-commands"; import type { AppSettings } from "../lib/types"; export function useSettings() { - const { hasKey, setHasKey, appSettings, setAppSettings } = useAppState( + const { appSettings, setAppSettings } = useAppState( useShallow(s => ({ - hasKey: s.hasKey, - setHasKey: s.setHasKey, appSettings: s.appSettings, setAppSettings: s.setAppSettings, })) ); - const checkApiKey = useCallback(async () => { - try { - const has = await commands.hasApiKey(); - setHasKey(has); - return has; - } catch { - setHasKey(false); - return false; - } - }, [setHasKey]); - - const saveApiKey = useCallback( - async (key: string) => { - await commands.setApiKey(key); - setHasKey(true); - }, - [setHasKey], - ); - - const removeApiKey = useCallback(async () => { - await commands.deleteApiKey(); - setHasKey(false); - }, [setHasKey]); - const loadSettings = useCallback(async () => { try { const settings = await commands.getSettings(); @@ -59,10 +33,6 @@ export function useSettings() { ); return { - hasKey, - checkApiKey, - saveApiKey, - removeApiKey, appSettings, loadSettings, saveSettings, diff --git a/app/src/lib/tauri-commands.ts b/app/src/lib/tauri-commands.ts index 737dde3..830e6a7 100644 --- a/app/src/lib/tauri-commands.ts +++ b/app/src/lib/tauri-commands.ts @@ -26,10 +26,6 @@ export const rebuildProjectContainer = (projectId: string) => invoke("rebuild_project_container", { projectId }); // Settings -export const setApiKey = (key: string) => - invoke("set_api_key", { key }); -export const hasApiKey = () => invoke("has_api_key"); -export const deleteApiKey = () => invoke("delete_api_key"); export const getSettings = () => invoke("get_settings"); export const updateSettings = (settings: AppSettings) => invoke("update_settings", { settings }); diff --git a/app/src/store/appState.ts b/app/src/store/appState.ts index b6b9b20..5f80e29 100644 --- a/app/src/store/appState.ts +++ b/app/src/store/appState.ts @@ -24,9 +24,6 @@ interface AppState { setDockerAvailable: (available: boolean | null) => void; imageExists: boolean | null; setImageExists: (exists: boolean | null) => void; - hasKey: boolean | null; - setHasKey: (has: boolean | null) => void; - // App settings appSettings: AppSettings | null; setAppSettings: (settings: AppSettings) => void; @@ -85,9 +82,6 @@ export const useAppState = create((set) => ({ setDockerAvailable: (available) => set({ dockerAvailable: available }), imageExists: null, setImageExists: (exists) => set({ imageExists: exists }), - hasKey: null, - setHasKey: (has) => set({ hasKey: has }), - // App settings appSettings: null, setAppSettings: (settings) => set({ appSettings: settings }),