diff --git a/app/src-tauri/src/commands/aws_commands.rs b/app/src-tauri/src/commands/aws_commands.rs index c1b8db7..73ba069 100644 --- a/app/src-tauri/src/commands/aws_commands.rs +++ b/app/src-tauri/src/commands/aws_commands.rs @@ -1,23 +1,58 @@ use tauri::State; + +use crate::models::Project; use crate::AppState; -#[tauri::command] -pub async fn aws_sso_refresh( - project_id: String, - state: State<'_, AppState>, -) -> Result<(), String> { - let project = state.projects_store.get(&project_id) - .ok_or_else(|| format!("Project {} not found", project_id))?; - - let profile = project.bedrock_config.as_ref() +/// Resolve AWS profile: project-level → global settings → "default". +pub fn resolve_profile_for_project(project: &Project, global_profile: Option<&str>) -> String { + project + .bedrock_config + .as_ref() .and_then(|b| b.aws_profile.clone()) - .or_else(|| state.settings_store.get().global_aws.aws_profile.clone()) - .unwrap_or_else(|| "default".to_string()); + .or_else(|| global_profile.map(|s| s.to_string())) + .unwrap_or_else(|| "default".to_string()) +} +/// Check if the AWS session is valid for the given profile on the host. +/// Returns `Ok(true)` if valid, `Ok(false)` if expired/invalid. +pub async fn check_sso_session(profile: &str) -> Result { + let output = tokio::process::Command::new("aws") + .args(["sts", "get-caller-identity", "--profile", profile]) + .output() + .await + .map_err(|e| format!("Failed to run aws sts get-caller-identity: {}", e))?; + Ok(output.status.success()) +} + +/// Check if the given AWS profile uses SSO (has sso_start_url or sso_session configured). +pub async fn is_sso_profile(profile: &str) -> Result { + let check_start_url = tokio::process::Command::new("aws") + .args(["configure", "get", "sso_start_url", "--profile", profile]) + .output() + .await; + if let Ok(out) = check_start_url { + if out.status.success() { + return Ok(true); + } + } + let check_session = tokio::process::Command::new("aws") + .args(["configure", "get", "sso_session", "--profile", profile]) + .output() + .await; + if let Ok(out) = check_session { + if out.status.success() { + return Ok(true); + } + } + Ok(false) +} + +/// Run `aws sso login --profile X` on the host. This is interactive (opens a browser). +pub async fn run_sso_login(profile: &str) -> Result<(), String> { log::info!("Running host-side AWS SSO login for profile '{}'", profile); let status = tokio::process::Command::new("aws") - .args(["sso", "login", "--profile", &profile]) + .args(["sso", "login", "--profile", profile]) .status() .await .map_err(|e| format!("Failed to run aws sso login: {}", e))?; @@ -28,3 +63,19 @@ pub async fn aws_sso_refresh( Ok(()) } + +#[tauri::command] +pub async fn aws_sso_refresh( + project_id: String, + state: State<'_, AppState>, +) -> Result<(), String> { + let project = state.projects_store.get(&project_id) + .ok_or_else(|| format!("Project {} not found", project_id))?; + + let profile = resolve_profile_for_project( + &project, + state.settings_store.get().global_aws.aws_profile.as_deref(), + ); + + run_sso_login(&profile).await +} diff --git a/app/src-tauri/src/commands/project_commands.rs b/app/src-tauri/src/commands/project_commands.rs index 211a396..b3a3972 100644 --- a/app/src-tauri/src/commands/project_commands.rs +++ b/app/src-tauri/src/commands/project_commands.rs @@ -1,7 +1,8 @@ use tauri::{Emitter, State}; +use crate::commands::aws_commands; use crate::docker; -use crate::models::{container_config, Backend, McpServer, Project, ProjectPath, ProjectStatus}; +use crate::models::{container_config, Backend, BedrockAuthMethod, McpServer, Project, ProjectPath, ProjectStatus}; use crate::storage::secure; use crate::AppState; @@ -208,6 +209,76 @@ pub async fn start_project_container( // Update status to starting state.projects_store.update_status(&project_id, ProjectStatus::Starting)?; + // Pre-validate AWS SSO session on the host for Bedrock Profile projects. + // If the session is expired, trigger `aws sso login` before starting the container + // so the entrypoint copies already-fresh credentials from the host mount. + if project.backend == Backend::Bedrock { + if let Some(ref bedrock) = project.bedrock_config { + if bedrock.auth_method == BedrockAuthMethod::Profile { + let profile = aws_commands::resolve_profile_for_project( + &project, + settings.global_aws.aws_profile.as_deref(), + ); + + emit_progress(&app_handle, &project_id, "Validating AWS session..."); + + let session_valid = tokio::time::timeout( + std::time::Duration::from_secs(10), + aws_commands::check_sso_session(&profile), + ) + .await; + + match session_valid { + Ok(Ok(true)) => { + emit_progress(&app_handle, &project_id, "AWS session valid."); + } + Ok(Ok(false)) => { + // Session expired — check if this is an SSO profile + if aws_commands::is_sso_profile(&profile).await.unwrap_or(false) { + emit_progress( + &app_handle, + &project_id, + "AWS session expired. Starting SSO login (check your browser)...", + ); + match aws_commands::run_sso_login(&profile).await { + Ok(()) => { + emit_progress( + &app_handle, + &project_id, + "SSO login successful.", + ); + } + Err(e) => { + log::warn!( + "SSO login failed for profile '{}': {} — continuing anyway", + profile, + e + ); + emit_progress( + &app_handle, + &project_id, + "SSO login failed or cancelled. Continuing...", + ); + } + } + } else { + log::warn!( + "AWS session invalid for profile '{}' (not SSO). Continuing...", + profile + ); + } + } + Ok(Err(e)) => { + log::warn!("Failed to check AWS session: {} — continuing anyway", e); + } + Err(_) => { + log::warn!("AWS session check timed out — continuing anyway"); + } + } + } + } + } + // Wrap container operations so that any failure resets status to Stopped. let result: Result = async { // Ensure image exists diff --git a/app/src-tauri/src/commands/terminal_commands.rs b/app/src-tauri/src/commands/terminal_commands.rs index eced0e0..00faab4 100644 --- a/app/src-tauri/src/commands/terminal_commands.rs +++ b/app/src-tauri/src/commands/terminal_commands.rs @@ -1,5 +1,6 @@ use tauri::{AppHandle, Emitter, State}; +use crate::commands::aws_commands; use crate::models::{Backend, BedrockAuthMethod, Project}; use crate::AppState; @@ -24,13 +25,10 @@ fn build_terminal_cmd(project: &Project, state: &AppState) -> Vec { return cmd; } - // Resolve AWS profile: project-level → global settings → "default" - let profile = project - .bedrock_config - .as_ref() - .and_then(|b| b.aws_profile.clone()) - .or_else(|| state.settings_store.get().global_aws.aws_profile.clone()) - .unwrap_or_else(|| "default".to_string()); + let profile = aws_commands::resolve_profile_for_project( + project, + state.settings_store.get().global_aws.aws_profile.as_deref(), + ); // Build a bash wrapper that validates credentials, re-auths if needed, // then exec's into claude. diff --git a/app/src-tauri/src/web_terminal/ws_handler.rs b/app/src-tauri/src/web_terminal/ws_handler.rs index 6619e80..3a49b18 100644 --- a/app/src-tauri/src/web_terminal/ws_handler.rs +++ b/app/src-tauri/src/web_terminal/ws_handler.rs @@ -7,6 +7,7 @@ use futures_util::{SinkExt, StreamExt}; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; +use crate::commands::aws_commands; use crate::models::{Backend, BedrockAuthMethod, Project, ProjectStatus}; use super::server::WebTerminalState; @@ -212,12 +213,10 @@ fn build_terminal_cmd(project: &Project, settings_store: &crate::storage::settin return cmd; } - let profile = project - .bedrock_config - .as_ref() - .and_then(|b| b.aws_profile.clone()) - .or_else(|| settings_store.get().global_aws.aws_profile.clone()) - .unwrap_or_else(|| "default".to_string()); + let profile = aws_commands::resolve_profile_for_project( + project, + settings_store.get().global_aws.aws_profile.as_deref(), + ); let claude_cmd = if project.full_permissions { "exec claude --dangerously-skip-permissions"