Add container registry pull, image source settings, and global AWS config
All checks were successful
Build Container / build-container (push) Successful in 1m59s

Support pulling images from registry (default: repo.anhonesthost.net/cybercovellc/triple-c/triple-c-sandbox:latest),
local builds, or custom images via a new settings UI. Add global AWS configuration
(config path auto-detect, profile picker, region) that serves as defaults overridable
per-project for Bedrock auth.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 15:22:49 +00:00
parent 6e68374604
commit 0f188783e1
22 changed files with 772 additions and 87 deletions

View File

@@ -1,7 +1,7 @@
use tauri::State;
use crate::docker;
use crate::models::ContainerInfo;
use crate::models::{container_config, ContainerInfo};
use crate::AppState;
#[tauri::command]
@@ -10,8 +10,10 @@ pub async fn check_docker() -> Result<bool, String> {
}
#[tauri::command]
pub async fn check_image_exists() -> Result<bool, String> {
docker::image_exists().await
pub async fn check_image_exists(state: State<'_, AppState>) -> Result<bool, String> {
let settings = state.settings_store.get();
let image_name = container_config::resolve_image_name(&settings.image_source, &settings.custom_image_name);
docker::image_exists(&image_name).await
}
#[tauri::command]

View File

@@ -1,7 +1,7 @@
use tauri::State;
use crate::docker;
use crate::models::{AuthMode, Project, ProjectStatus};
use crate::models::{container_config, AuthMode, Project, ProjectStatus};
use crate::storage::secure;
use crate::AppState;
@@ -57,6 +57,10 @@ pub async fn start_project_container(
.get(&project_id)
.ok_or_else(|| format!("Project {} not found", project_id))?;
// Load settings for image resolution and global AWS
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 = match project.auth_mode {
AuthMode::ApiKey => {
@@ -65,16 +69,14 @@ pub async fn start_project_container(
Some(key)
}
AuthMode::Login => {
// Login mode: no API key needed, user runs `claude login` in the 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());
// 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
}
@@ -84,12 +86,19 @@ pub async fn start_project_container(
state.projects_store.update_status(&project_id, ProjectStatus::Starting)?;
// Ensure image exists
if !docker::image_exists().await? {
return Err("Docker image not built. Please build the image first.".to_string());
if !docker::image_exists(&image_name).await? {
state.projects_store.update_status(&project_id, ProjectStatus::Stopped)?;
return Err(format!("Docker image '{}' not found. Please pull or build the image first.", image_name));
}
// Determine docker socket path
let docker_socket = default_docker_socket();
let docker_socket = settings.docker_socket_path
.as_deref()
.map(|s| s.to_string())
.unwrap_or_else(|| default_docker_socket());
// AWS config path from global settings
let aws_config_path = settings.global_aws.aws_config_path.clone();
// Check for existing container
let container_id = if let Some(existing_id) = docker::find_existing_container(&project).await? {
@@ -98,7 +107,14 @@ pub async fn start_project_container(
existing_id
} else {
// Create new container
let new_id = docker::create_container(&project, api_key.as_deref(), &docker_socket).await?;
let new_id = docker::create_container(
&project,
api_key.as_deref(),
&docker_socket,
&image_name,
aws_config_path.as_deref(),
&settings.global_aws,
).await?;
docker::start_container(&new_id).await?;
new_id
};

View File

@@ -1,4 +1,9 @@
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> {
@@ -14,3 +19,88 @@ pub async fn has_api_key() -> Result<bool, String> {
pub async fn delete_api_key() -> Result<(), String> {
secure::delete_api_key()
}
#[tauri::command]
pub async fn get_settings(state: State<'_, AppState>) -> Result<AppSettings, String> {
Ok(state.settings_store.get())
}
#[tauri::command]
pub async fn update_settings(
settings: AppSettings,
state: State<'_, AppState>,
) -> Result<AppSettings, String> {
state.settings_store.update(settings)
}
#[tauri::command]
pub async fn pull_image(
image_name: String,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
use tauri::Emitter;
docker::pull_image(&image_name, move |msg| {
let _ = app_handle.emit("image-pull-progress", msg);
})
.await
}
#[tauri::command]
pub async fn detect_aws_config() -> Result<Option<String>, String> {
if let Some(home) = dirs::home_dir() {
let aws_dir = home.join(".aws");
if aws_dir.exists() {
return Ok(Some(aws_dir.to_string_lossy().to_string()));
}
}
Ok(None)
}
#[tauri::command]
pub async fn list_aws_profiles() -> Result<Vec<String>, String> {
let mut profiles = Vec::new();
let home = match dirs::home_dir() {
Some(h) => h,
None => return Ok(profiles),
};
// Parse ~/.aws/credentials
let credentials_path = home.join(".aws").join("credentials");
if credentials_path.exists() {
if let Ok(contents) = std::fs::read_to_string(&credentials_path) {
for line in contents.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') && trimmed.ends_with(']') {
let profile = trimmed[1..trimmed.len() - 1].to_string();
if !profiles.contains(&profile) {
profiles.push(profile);
}
}
}
}
}
// Parse ~/.aws/config (profiles are prefixed with "profile ")
let config_path = home.join(".aws").join("config");
if config_path.exists() {
if let Ok(contents) = std::fs::read_to_string(&config_path) {
for line in contents.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') && trimmed.ends_with(']') {
let section = &trimmed[1..trimmed.len() - 1];
let profile = if let Some(name) = section.strip_prefix("profile ") {
name.to_string()
} else {
section.to_string()
};
if !profiles.contains(&profile) {
profiles.push(profile);
}
}
}
}
}
Ok(profiles)
}