use std::collections::HashMap;
use std::path::PathBuf;
use figment::providers::{Format, Toml};
use figment::Figment;
use getmyconfig::{ConfigReader, StorageConfig};
const DEFAULT_CONFIG: &str = include_str!("../config/trustee_default.toml");
fn build_info() -> abk::cli::BuildInfo {
abk::cli::BuildInfo::new(
option_env!("GIT_SHA"),
option_env!("BUILD_DATE"),
option_env!("RUSTC_VERSION"),
option_env!("BUILD_PROFILE"),
)
}
fn load_env_file(path: &PathBuf) -> Result<HashMap<String, String>, Box<dyn std::error::Error>> {
let mut secrets = HashMap::new();
if path.exists() {
let content = std::fs::read_to_string(path)?;
parse_env_content(&content, &mut secrets);
}
for (key, value) in std::env::vars() {
if key.starts_with("GETMYCONFIG_") {
secrets.insert(key, value);
}
}
Ok(secrets)
}
fn parse_env_content(content: &str, secrets: &mut HashMap<String, String>) {
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim().to_string();
let value = value.trim().to_string();
let value = value.trim_matches('"').trim_matches('\'').to_string();
secrets.insert(key, value);
}
}
}
fn get_config_paths(agent_name: &str) -> (PathBuf, PathBuf, String, String) {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
let share_dir = PathBuf::from(home).join(format!(".{}", agent_name));
let local_env_path = share_dir.join(".env");
let mut config_filename = format!("{}.toml", agent_name);
let mut env_filename = String::new();
if local_env_path.exists() {
if let Ok(content) = std::fs::read_to_string(&local_env_path) {
for line in content.lines() {
let line = line.trim();
if line.starts_with('#') || line.is_empty() {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
let value = value.trim().trim_matches('"').trim_matches('\'');
match key {
"TRUSTEE_CONFIG_FILE" => config_filename = value.to_string(),
"TRUSTEE_ENV_FILE" => env_filename = value.to_string(),
_ => {}
}
}
}
}
}
if env_filename.is_empty() {
env_filename = ".env".to_string();
}
let config_path = share_dir.join("config").join(&config_filename);
let env_path = share_dir.join(&env_filename);
(config_path, env_path, config_filename, env_filename)
}
fn build_storage_config(secrets: &HashMap<String, String>) -> Option<StorageConfig> {
let endpoint = secrets.get("GETMYCONFIG_ENDPOINT").filter(|s| !s.is_empty())?;
let access_key = secrets.get("GETMYCONFIG_ACCESS_KEY").filter(|s| !s.is_empty())?;
let secret_key = secrets.get("GETMYCONFIG_SECRET_KEY").filter(|s| !s.is_empty())?;
let bucket = secrets.get("GETMYCONFIG_BUCKET").filter(|s| !s.is_empty())?;
let encryption_key = secrets.get("GETMYCONFIG_ENCRYPTION_KEY").filter(|s| !s.is_empty())?;
let region = secrets.get("GETMYCONFIG_REGION").filter(|s| !s.is_empty()).cloned();
let endpoint = if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
endpoint.clone()
} else {
format!("https://{}", endpoint)
};
Some(StorageConfig {
endpoint,
access_key: access_key.clone(),
secret_key: secret_key.clone(),
bucket: bucket.clone(),
region,
encryption_key: encryption_key.clone(),
})
}
async fn load_remote_config(
local_secrets: &HashMap<String, String>,
) -> Option<(String, HashMap<String, String>)> {
let storage_config = build_storage_config(local_secrets)?;
let reader = match ConfigReader::new(storage_config) {
Ok(r) => r,
Err(e) => {
eprintln!("[getmyconfig] Failed to create reader: {}", e);
return None;
}
};
let config_file_name = local_secrets
.get("GETMYCONFIG_CONFIG_FILE")
.filter(|s| !s.is_empty())
.unwrap_or(&"trustee.toml.enc".to_string())
.clone();
let env_file_name = local_secrets
.get("GETMYCONFIG_ENV_FILE")
.filter(|s| !s.is_empty())
.unwrap_or(&".env.enc".to_string())
.clone();
let config_toml = match reader.read_raw(&config_file_name).await {
Ok(bytes) => match String::from_utf8(bytes) {
Ok(s) => {
eprintln!("[getmyconfig] ✓ Loaded {} from remote storage", config_file_name);
s
}
Err(e) => {
eprintln!("[getmyconfig] {} is not valid UTF-8: {}", config_file_name, e);
return None;
}
},
Err(e) => {
eprintln!("[getmyconfig] Failed to read {}: {}", config_file_name, e);
return None;
}
};
let mut remote_secrets = HashMap::new();
match reader.read_raw(&env_file_name).await {
Ok(bytes) => match String::from_utf8(bytes) {
Ok(content) => {
parse_env_content(&content, &mut remote_secrets);
eprintln!(
"[getmyconfig] ✓ Loaded {} from remote storage ({} keys)",
env_file_name,
remote_secrets.len()
);
}
Err(e) => {
eprintln!("[getmyconfig] {} is not valid UTF-8: {}", env_file_name, e);
return None;
}
},
Err(e) => {
eprintln!("[getmyconfig] Failed to read {}: {}", env_file_name, e);
return None;
}
}
Some((config_toml, remote_secrets))
}
fn merge_config(user_config_toml: &str) -> Result<String, Box<dyn std::error::Error>> {
let version_override = format!(
"[agent]\nversion = \"{v}\"\n\n[cli]\nversion = \"{v}\"\n",
v = env!("CARGO_PKG_VERSION")
);
let merged: toml::Table = Figment::new()
.merge(Toml::string(DEFAULT_CONFIG))
.merge(Toml::string(user_config_toml))
.merge(Toml::string(&version_override))
.extract()
.map_err(|e| format!("Failed to merge configuration: {}", e))?;
let merged_toml = toml::to_string(&merged)
.map_err(|e| format!("Failed to serialize merged config: {}", e))?;
Ok(merged_toml)
}
#[cfg(feature = "tui")]
fn restore_terminal() {
let _ = crossterm::terminal::disable_raw_mode();
let _ = crossterm::execute!(
std::io::stdout(),
crossterm::terminal::LeaveAlternateScreen,
crossterm::event::DisableBracketedPaste,
crossterm::event::DisableMouseCapture,
crossterm::cursor::Show,
);
}
#[cfg(feature = "tui")]
fn setup_panic_hook() {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
restore_terminal();
original_hook(panic_info);
}));
}
#[cfg(feature = "tui")]
async fn run_tui_mode() -> Result<(), Box<dyn std::error::Error>> {
std::env::set_var("ABK_AGENT_NAME", "trustee");
let logger = abk::observability::Logger::new(None, None)?;
abk::observability::init_global_logger(logger);
let agent_name = "trustee";
let (config_path, secrets_path, _, _) = get_config_paths(agent_name);
let local_secrets = load_env_file(&secrets_path)
.map_err(|e| format!("Failed to read secrets from {}: {}", secrets_path.display(), e))?;
let (user_config_toml, secrets) = match load_remote_config(&local_secrets).await {
Some((remote_config, remote_secrets)) => {
let mut merged = local_secrets.clone();
merged.extend(remote_secrets);
(remote_config, merged)
}
None => {
if !config_path.exists() {
eprintln!("Error: Configuration not found at: {}", config_path.display());
eprintln!("Remote config also unavailable.");
eprintln!("\nRun 'trustee init --force' to set up your environment.");
std::process::exit(1);
}
let config_toml = std::fs::read_to_string(&config_path)
.map_err(|e| format!("Failed to read config from {}: {}", config_path.display(), e))?;
(config_toml, local_secrets)
}
};
let merged_config = merge_config(&user_config_toml)?;
#[cfg(unix)]
tokio::spawn(async {
if let Ok(mut sig) = tokio::signal::unix::signal(
tokio::signal::unix::SignalKind::terminate(),
) {
sig.recv().await;
restore_terminal();
std::process::exit(0);
}
});
trustee_tui::run(merged_config, secrets, build_info()).await?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "tui")]
setup_panic_hook();
let args: Vec<String> = std::env::args().collect();
#[cfg(feature = "tui")]
if args.len() == 1 {
return run_tui_mode().await;
}
let agent_name = "trustee";
let is_init = args.get(1).map(|s| s.as_str()) == Some("init");
if is_init {
let project_config = std::fs::read_to_string("config/trustee.toml")
.unwrap_or_default();
let merged = merge_config(&project_config)?;
let secrets = HashMap::new();
abk::cli::run_from_raw_config(&merged, secrets, Some(build_info())).await
} else {
let (config_path, secrets_path, _config_filename, _env_filename) = get_config_paths(agent_name);
if !config_path.exists() && !secrets_path.exists() {
eprintln!("Error: Configuration not found at: {}", config_path.display());
eprintln!("\nRun 'trustee init --force' to set up your environment.");
std::process::exit(1);
}
let local_secrets = load_env_file(&secrets_path)
.map_err(|e| format!("Failed to read secrets from {}: {}", secrets_path.display(), e))?;
let (user_config_toml, secrets) = match load_remote_config(&local_secrets).await {
Some((remote_config, remote_secrets)) => {
let mut merged = local_secrets.clone();
merged.extend(remote_secrets);
(remote_config, merged)
}
None => {
if !config_path.exists() {
eprintln!("Error: Configuration not found at: {}", config_path.display());
eprintln!("Remote config also unavailable.");
eprintln!("\nRun 'trustee init --force' to set up your environment.");
std::process::exit(1);
}
let config_toml = std::fs::read_to_string(&config_path)
.map_err(|e| format!("Failed to read config from {}: {}", config_path.display(), e))?;
eprintln!("[getmyconfig] Using local config fallback");
(config_toml, local_secrets)
}
};
let merged_config = merge_config(&user_config_toml)?;
abk::cli::run_from_raw_config(&merged_config, secrets, Some(build_info())).await
}
}