Pre-validate AWS SSO session on host during container startup
All checks were successful
Build App / compute-version (push) Successful in 3s
Build App / build-linux (push) Successful in 4m46s
Build App / build-windows (push) Successful in 6m57s
Build App / build-macos (push) Successful in 9m9s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 12s

For Bedrock Profile projects, SSO credentials are now checked and
refreshed on the host before the container starts, so the entrypoint
copies already-valid tokens. This eliminates the delay where users
had to wait for the terminal to open before being prompted to login.

The terminal-time fallback remains for mid-session credential expiry.
Also consolidates duplicated profile resolution logic into a shared
helper in aws_commands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 06:54:59 -07:00
parent 15b03173a5
commit ef67b447b3
4 changed files with 145 additions and 26 deletions

View File

@@ -1,23 +1,58 @@
use tauri::State; use tauri::State;
use crate::models::Project;
use crate::AppState; use crate::AppState;
#[tauri::command] /// Resolve AWS profile: project-level → global settings → "default".
pub async fn aws_sso_refresh( pub fn resolve_profile_for_project(project: &Project, global_profile: Option<&str>) -> String {
project_id: String, project
state: State<'_, AppState>, .bedrock_config
) -> Result<(), String> { .as_ref()
let project = state.projects_store.get(&project_id)
.ok_or_else(|| format!("Project {} not found", project_id))?;
let profile = project.bedrock_config.as_ref()
.and_then(|b| b.aws_profile.clone()) .and_then(|b| b.aws_profile.clone())
.or_else(|| state.settings_store.get().global_aws.aws_profile.clone()) .or_else(|| global_profile.map(|s| s.to_string()))
.unwrap_or_else(|| "default".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<bool, String> {
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<bool, String> {
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); log::info!("Running host-side AWS SSO login for profile '{}'", profile);
let status = tokio::process::Command::new("aws") let status = tokio::process::Command::new("aws")
.args(["sso", "login", "--profile", &profile]) .args(["sso", "login", "--profile", profile])
.status() .status()
.await .await
.map_err(|e| format!("Failed to run aws sso login: {}", e))?; .map_err(|e| format!("Failed to run aws sso login: {}", e))?;
@@ -28,3 +63,19 @@ pub async fn aws_sso_refresh(
Ok(()) 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
}

View File

@@ -1,7 +1,8 @@
use tauri::{Emitter, State}; use tauri::{Emitter, State};
use crate::commands::aws_commands;
use crate::docker; 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::storage::secure;
use crate::AppState; use crate::AppState;
@@ -208,6 +209,76 @@ pub async fn start_project_container(
// Update status to starting // Update status to starting
state.projects_store.update_status(&project_id, ProjectStatus::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. // Wrap container operations so that any failure resets status to Stopped.
let result: Result<String, String> = async { let result: Result<String, String> = async {
// Ensure image exists // Ensure image exists

View File

@@ -1,5 +1,6 @@
use tauri::{AppHandle, Emitter, State}; use tauri::{AppHandle, Emitter, State};
use crate::commands::aws_commands;
use crate::models::{Backend, BedrockAuthMethod, Project}; use crate::models::{Backend, BedrockAuthMethod, Project};
use crate::AppState; use crate::AppState;
@@ -24,13 +25,10 @@ fn build_terminal_cmd(project: &Project, state: &AppState) -> Vec<String> {
return cmd; return cmd;
} }
// Resolve AWS profile: project-level → global settings → "default" let profile = aws_commands::resolve_profile_for_project(
let profile = project project,
.bedrock_config state.settings_store.get().global_aws.aws_profile.as_deref(),
.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());
// Build a bash wrapper that validates credentials, re-auths if needed, // Build a bash wrapper that validates credentials, re-auths if needed,
// then exec's into claude. // then exec's into claude.

View File

@@ -7,6 +7,7 @@ use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::commands::aws_commands;
use crate::models::{Backend, BedrockAuthMethod, Project, ProjectStatus}; use crate::models::{Backend, BedrockAuthMethod, Project, ProjectStatus};
use super::server::WebTerminalState; use super::server::WebTerminalState;
@@ -212,12 +213,10 @@ fn build_terminal_cmd(project: &Project, settings_store: &crate::storage::settin
return cmd; return cmd;
} }
let profile = project let profile = aws_commands::resolve_profile_for_project(
.bedrock_config project,
.as_ref() settings_store.get().global_aws.aws_profile.as_deref(),
.and_then(|b| b.aws_profile.clone()) );
.or_else(|| settings_store.get().global_aws.aws_profile.clone())
.unwrap_or_else(|| "default".to_string());
let claude_cmd = if project.full_permissions { let claude_cmd = if project.full_permissions {
"exec claude --dangerously-skip-permissions" "exec claude --dangerously-skip-permissions"