refactor: update goose2 credential management behind provider-scoped ACP/core API (#8887)
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
Generated
-87
@@ -1152,17 +1152,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"home",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
@@ -1670,7 +1659,6 @@ dependencies = [
|
||||
"chrono",
|
||||
"dirs",
|
||||
"doctor",
|
||||
"etcetera",
|
||||
"ignore",
|
||||
"keyring",
|
||||
"log",
|
||||
@@ -1809,15 +1797,6 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.29.1"
|
||||
@@ -5829,15 +5808,6 @@ dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -5889,21 +5859,6 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
@@ -5961,12 +5916,6 @@ version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
@@ -5985,12 +5934,6 @@ version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -6009,12 +5952,6 @@ version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
@@ -6045,12 +5982,6 @@ version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -6069,12 +6000,6 @@ version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
@@ -6093,12 +6018,6 @@ version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
@@ -6117,12 +6036,6 @@ version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
|
||||
@@ -31,7 +31,6 @@ tokio = { version = "1.50.0", features = ["full"] }
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
serde_yaml = "0.9"
|
||||
etcetera = "0.8"
|
||||
doctor = { git = "https://github.com/block/builderbot", rev = "8e1c3ec145edc0df5f04b4427cfd758378036862" }
|
||||
ignore = "0.4.25"
|
||||
base64 = "0.22"
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
use tauri::State;
|
||||
|
||||
use crate::services::goose_config::{FieldValue, GooseConfig, ProviderStatus};
|
||||
use crate::services::provider_defs::find_config_key;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_provider_config(
|
||||
config: State<'_, GooseConfig>,
|
||||
provider_id: String,
|
||||
) -> Result<Vec<FieldValue>, String> {
|
||||
config.get_provider_field_values(&provider_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_provider_field(
|
||||
config: State<'_, GooseConfig>,
|
||||
key: String,
|
||||
value: String,
|
||||
) -> Result<(), String> {
|
||||
let config_key =
|
||||
find_config_key(&key).ok_or_else(|| format!("Unknown provider config key '{key}'"))?;
|
||||
let trimmed_value = value.trim();
|
||||
if trimmed_value.is_empty() {
|
||||
return Err("Field value cannot be empty".to_string());
|
||||
}
|
||||
|
||||
if config_key.is_secret {
|
||||
config.set_secret(&key, trimmed_value)
|
||||
} else {
|
||||
config.set_param(&key, trimmed_value)
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_provider_config(
|
||||
config: State<'_, GooseConfig>,
|
||||
provider_id: String,
|
||||
) -> Result<(), String> {
|
||||
config.delete_all_provider_fields(&provider_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn check_all_provider_status(config: State<'_, GooseConfig>) -> Vec<ProviderStatus> {
|
||||
config.check_all_provider_status()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn restart_app(app: tauri::AppHandle) {
|
||||
app.restart();
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
pub mod acp;
|
||||
pub mod agent_setup;
|
||||
pub mod agents;
|
||||
pub mod credentials;
|
||||
pub mod doctor;
|
||||
pub mod git;
|
||||
pub mod git_changes;
|
||||
|
||||
@@ -2,7 +2,6 @@ mod commands;
|
||||
mod services;
|
||||
mod types;
|
||||
|
||||
use services::goose_config::GooseConfig;
|
||||
use services::personas::PersonaStore;
|
||||
use tauri_plugin_window_state::StateFlags;
|
||||
|
||||
@@ -25,8 +24,7 @@ pub fn run() {
|
||||
.with_state_flags(StateFlags::all() & !StateFlags::VISIBLE)
|
||||
.build(),
|
||||
)
|
||||
.manage(PersonaStore::new())
|
||||
.manage(GooseConfig::new());
|
||||
.manage(PersonaStore::new());
|
||||
|
||||
#[cfg(feature = "app-test-driver")]
|
||||
let builder = builder.plugin(tauri_plugin_app_test_driver::init());
|
||||
@@ -65,11 +63,6 @@ pub fn run() {
|
||||
commands::git::git_pull,
|
||||
commands::git::git_create_branch,
|
||||
commands::git::git_create_worktree,
|
||||
commands::credentials::get_provider_config,
|
||||
commands::credentials::save_provider_field,
|
||||
commands::credentials::delete_provider_config,
|
||||
commands::credentials::check_all_provider_status,
|
||||
commands::credentials::restart_app,
|
||||
commands::model_setup::authenticate_model_provider,
|
||||
commands::agent_setup::check_agent_installed,
|
||||
commands::agent_setup::check_agent_auth,
|
||||
|
||||
@@ -1,452 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::provider_defs::{find_provider_def, PROVIDER_CONFIG_DEFS};
|
||||
|
||||
const KEYRING_SERVICE: &str = "goose";
|
||||
const KEYRING_USERNAME: &str = "secrets";
|
||||
const CONFIG_YAML_NAME: &str = "config.yaml";
|
||||
const SECRETS_YAML_NAME: &str = "secrets.yaml";
|
||||
const SECRET_MASK_PREFIX_LEN: usize = 4;
|
||||
const SECRET_MASK_SUFFIX_LEN: usize = 3;
|
||||
const SECRET_MASK_FALLBACK: &str = "***";
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProviderStatus {
|
||||
pub provider_id: String,
|
||||
pub is_configured: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FieldValue {
|
||||
pub key: String,
|
||||
pub value: Option<String>,
|
||||
pub is_set: bool,
|
||||
pub is_secret: bool,
|
||||
pub required: bool,
|
||||
}
|
||||
|
||||
pub struct GooseConfig {
|
||||
config_dir: PathBuf,
|
||||
guard: Mutex<()>,
|
||||
}
|
||||
|
||||
impl GooseConfig {
|
||||
pub fn new() -> Self {
|
||||
let config_dir = Self::resolve_config_dir();
|
||||
log::info!("GooseConfig using config dir: {}", config_dir.display());
|
||||
Self {
|
||||
config_dir,
|
||||
guard: Mutex::new(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_config_dir() -> PathBuf {
|
||||
if let Ok(root) = std::env::var("GOOSE_PATH_ROOT") {
|
||||
return PathBuf::from(root).join("config");
|
||||
}
|
||||
|
||||
let strategy = choose_app_strategy(AppStrategyArgs {
|
||||
top_level_domain: "Block".to_string(),
|
||||
author: "Block".to_string(),
|
||||
app_name: "goose".to_string(),
|
||||
})
|
||||
.expect("goose requires a home dir");
|
||||
|
||||
strategy.config_dir()
|
||||
}
|
||||
|
||||
fn read_config_map(&self) -> serde_yaml::Mapping {
|
||||
let config_path = self.config_dir.join(CONFIG_YAML_NAME);
|
||||
match std::fs::read_to_string(&config_path) {
|
||||
Ok(contents) => {
|
||||
serde_yaml::from_str::<serde_yaml::Mapping>(&contents).unwrap_or_default()
|
||||
}
|
||||
Err(_) => serde_yaml::Mapping::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_config_map(&self, config: &serde_yaml::Mapping) -> Result<(), String> {
|
||||
let path = self.config_dir.join(CONFIG_YAML_NAME);
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
let yaml = serde_yaml::to_string(config)
|
||||
.map_err(|e| format!("Failed to serialize config: {e}"))?;
|
||||
|
||||
std::fs::write(&path, yaml).map_err(|e| format!("Failed to write config file: {e}"))
|
||||
}
|
||||
|
||||
fn get_secret(&self, key: &str) -> Option<String> {
|
||||
let env_key = key.to_uppercase();
|
||||
if let Ok(value) = std::env::var(&env_key) {
|
||||
return Some(value);
|
||||
}
|
||||
|
||||
let secrets = self.all_secrets();
|
||||
secrets
|
||||
.get(key)
|
||||
.and_then(|value| value.as_str().map(|secret| secret.to_string()))
|
||||
}
|
||||
|
||||
fn mask_secret_value(value: &str) -> String {
|
||||
let prefix: String = value.chars().take(SECRET_MASK_PREFIX_LEN).collect();
|
||||
let suffix_chars: Vec<char> = value.chars().rev().take(SECRET_MASK_SUFFIX_LEN).collect();
|
||||
let suffix: String = suffix_chars.into_iter().rev().collect();
|
||||
|
||||
if prefix.is_empty()
|
||||
|| suffix.is_empty()
|
||||
|| value.chars().count() <= SECRET_MASK_PREFIX_LEN + SECRET_MASK_SUFFIX_LEN
|
||||
{
|
||||
return SECRET_MASK_FALLBACK.to_string();
|
||||
}
|
||||
|
||||
format!("{prefix}...{suffix}")
|
||||
}
|
||||
|
||||
fn has_param(&self, key: &str) -> bool {
|
||||
self.get_param(key).is_some()
|
||||
}
|
||||
|
||||
fn has_secret(&self, key: &str) -> bool {
|
||||
self.get_secret(key).is_some()
|
||||
}
|
||||
|
||||
pub fn get_param(&self, key: &str) -> Option<String> {
|
||||
let env_key = key.to_uppercase();
|
||||
if let Ok(value) = std::env::var(&env_key) {
|
||||
return Some(value);
|
||||
}
|
||||
|
||||
let config = self.read_config_map();
|
||||
let yaml_key = serde_yaml::Value::String(key.to_string());
|
||||
config
|
||||
.get(&yaml_key)
|
||||
.and_then(|value| value.as_str().map(|param| param.to_string()))
|
||||
}
|
||||
|
||||
pub fn get_secret_masked(&self, key: &str) -> Option<String> {
|
||||
self.get_secret(key)
|
||||
.map(|value| Self::mask_secret_value(&value))
|
||||
}
|
||||
|
||||
pub fn set_param(&self, key: &str, value: &str) -> Result<(), String> {
|
||||
let _guard = self.guard.lock().unwrap();
|
||||
|
||||
let mut config = self.read_config_map();
|
||||
config.insert(
|
||||
serde_yaml::Value::String(key.to_string()),
|
||||
serde_yaml::Value::String(value.to_string()),
|
||||
);
|
||||
|
||||
self.write_config_map(&config)
|
||||
}
|
||||
|
||||
pub fn delete_param(&self, key: &str) -> Result<bool, String> {
|
||||
let _guard = self.guard.lock().unwrap();
|
||||
|
||||
let mut config = self.read_config_map();
|
||||
let yaml_key = serde_yaml::Value::String(key.to_string());
|
||||
let removed = config.remove(&yaml_key).is_some();
|
||||
|
||||
if removed {
|
||||
self.write_config_map(&config)?;
|
||||
}
|
||||
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
pub fn set_secret(&self, key: &str, value: &str) -> Result<(), String> {
|
||||
let _guard = self.guard.lock().unwrap();
|
||||
|
||||
let mut secrets = self.all_secrets();
|
||||
secrets.insert(key.to_string(), Value::String(value.to_string()));
|
||||
|
||||
if self.is_keyring_disabled() {
|
||||
self.write_secrets_to_file(&secrets)
|
||||
} else {
|
||||
self.write_secrets_to_keyring(&secrets)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_secret(&self, key: &str) -> Result<bool, String> {
|
||||
let _guard = self.guard.lock().unwrap();
|
||||
|
||||
let mut secrets = self.all_secrets();
|
||||
let removed = secrets.remove(key).is_some();
|
||||
|
||||
if removed {
|
||||
if self.is_keyring_disabled() {
|
||||
self.write_secrets_to_file(&secrets)?;
|
||||
} else {
|
||||
self.write_secrets_to_keyring(&secrets)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
fn has_oauth_cache(&self, cache_path: &str) -> bool {
|
||||
let full_path = self.config_dir.join(cache_path);
|
||||
if full_path.is_dir() {
|
||||
std::fs::read_dir(&full_path)
|
||||
.map(|mut entries| entries.any(|_| true))
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
full_path.exists()
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_oauth_cache(&self, cache_path: &str) -> Result<bool, String> {
|
||||
let _guard = self.guard.lock().unwrap();
|
||||
let full_path = self.config_dir.join(cache_path);
|
||||
|
||||
if full_path.is_dir() {
|
||||
std::fs::remove_dir_all(&full_path)
|
||||
.map_err(|e| format!("Failed to remove OAuth cache directory: {e}"))?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if full_path.exists() {
|
||||
std::fs::remove_file(&full_path)
|
||||
.map_err(|e| format!("Failed to remove OAuth cache file: {e}"))?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub fn check_provider_status(&self, provider_id: &str) -> ProviderStatus {
|
||||
if provider_id == "databricks" {
|
||||
let has_host = self.has_param("DATABRICKS_HOST");
|
||||
let has_token = self.has_secret("DATABRICKS_TOKEN");
|
||||
let has_oauth = self.has_oauth_cache("databricks/oauth");
|
||||
|
||||
return ProviderStatus {
|
||||
provider_id: provider_id.to_string(),
|
||||
is_configured: has_host && (has_token || has_oauth),
|
||||
};
|
||||
}
|
||||
|
||||
let def = match find_provider_def(provider_id) {
|
||||
Some(def) => def,
|
||||
None => {
|
||||
return ProviderStatus {
|
||||
provider_id: provider_id.to_string(),
|
||||
is_configured: false,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let has_oauth = def
|
||||
.oauth_cache_path
|
||||
.map(|p| self.has_oauth_cache(p))
|
||||
.unwrap_or(false);
|
||||
|
||||
if has_oauth {
|
||||
return ProviderStatus {
|
||||
provider_id: provider_id.to_string(),
|
||||
is_configured: true,
|
||||
};
|
||||
}
|
||||
|
||||
let all_required_present = def.keys.iter().all(|k| {
|
||||
if !k.required {
|
||||
return true;
|
||||
}
|
||||
if k.is_secret {
|
||||
self.has_secret(k.name)
|
||||
} else {
|
||||
self.has_param(k.name)
|
||||
}
|
||||
});
|
||||
|
||||
let has_any_key = def.keys.iter().any(|k| {
|
||||
if k.is_secret {
|
||||
self.has_secret(k.name)
|
||||
} else {
|
||||
self.has_param(k.name)
|
||||
}
|
||||
});
|
||||
|
||||
let is_configured = if def.keys.is_empty() {
|
||||
false
|
||||
} else {
|
||||
all_required_present && has_any_key
|
||||
};
|
||||
|
||||
ProviderStatus {
|
||||
provider_id: provider_id.to_string(),
|
||||
is_configured,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_all_provider_status(&self) -> Vec<ProviderStatus> {
|
||||
PROVIDER_CONFIG_DEFS
|
||||
.iter()
|
||||
.map(|def| self.check_provider_status(def.id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_provider_field_values(&self, provider_id: &str) -> Result<Vec<FieldValue>, String> {
|
||||
let def = find_provider_def(provider_id)
|
||||
.ok_or_else(|| format!("Unknown provider '{provider_id}'"))?;
|
||||
|
||||
Ok(def
|
||||
.keys
|
||||
.iter()
|
||||
.map(|config_key| {
|
||||
let value = if config_key.is_secret {
|
||||
self.get_secret_masked(config_key.name)
|
||||
} else {
|
||||
self.get_param(config_key.name)
|
||||
};
|
||||
|
||||
FieldValue {
|
||||
key: config_key.name.to_string(),
|
||||
is_set: value.is_some(),
|
||||
value,
|
||||
is_secret: config_key.is_secret,
|
||||
required: config_key.required,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn delete_all_provider_fields(&self, provider_id: &str) -> Result<(), String> {
|
||||
let def = find_provider_def(provider_id)
|
||||
.ok_or_else(|| format!("Unknown provider '{provider_id}'"))?;
|
||||
|
||||
for config_key in def.keys {
|
||||
if config_key.is_secret {
|
||||
self.delete_secret(config_key.name)?;
|
||||
} else {
|
||||
self.delete_param(config_key.name)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(oauth_cache_path) = def.oauth_cache_path {
|
||||
self.delete_oauth_cache(oauth_cache_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_keyring_disabled(&self) -> bool {
|
||||
if std::env::var("GOOSE_DISABLE_KEYRING").is_ok() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let config_path = self.config_dir.join(CONFIG_YAML_NAME);
|
||||
if let Ok(contents) = std::fs::read_to_string(&config_path) {
|
||||
if let Ok(yaml) = serde_yaml::from_str::<serde_yaml::Mapping>(&contents) {
|
||||
let key = serde_yaml::Value::String("GOOSE_DISABLE_KEYRING".to_string());
|
||||
if let Some(val) = yaml.get(&key) {
|
||||
return val.as_bool().unwrap_or(false)
|
||||
|| val
|
||||
.as_str()
|
||||
.map(|s| s == "true" || s == "1")
|
||||
.unwrap_or(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn all_secrets(&self) -> HashMap<String, Value> {
|
||||
if self.is_keyring_disabled() {
|
||||
return self.read_secrets_from_file();
|
||||
}
|
||||
|
||||
match self.read_secrets_from_keyring() {
|
||||
Ok(secrets) => secrets,
|
||||
Err(e) => {
|
||||
log::warn!("Keyring read failed, falling back to secrets file: {e}");
|
||||
self.read_secrets_from_file()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_secrets_from_keyring(&self) -> Result<HashMap<String, Value>, String> {
|
||||
let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_USERNAME)
|
||||
.map_err(|e| format!("Failed to access keyring: {e}"))?;
|
||||
|
||||
match entry.get_password() {
|
||||
Ok(json_str) => serde_json::from_str(&json_str)
|
||||
.map_err(|e| format!("Failed to parse keyring JSON: {e}")),
|
||||
Err(keyring::Error::NoEntry) => Ok(HashMap::new()),
|
||||
Err(e) => Err(format!("Failed to read keyring: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_secrets_from_file(&self) -> HashMap<String, Value> {
|
||||
let path = self.config_dir.join(SECRETS_YAML_NAME);
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(contents) => serde_yaml::from_str::<HashMap<String, String>>(&contents)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, Value::String(v)))
|
||||
.collect(),
|
||||
Err(_) => HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_secrets_to_keyring(&self, secrets: &HashMap<String, Value>) -> Result<(), String> {
|
||||
let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_USERNAME)
|
||||
.map_err(|e| format!("Failed to access keyring: {e}"))?;
|
||||
|
||||
let json_str = serde_json::to_string(secrets)
|
||||
.map_err(|e| format!("Failed to serialize secrets: {e}"))?;
|
||||
|
||||
entry
|
||||
.set_password(&json_str)
|
||||
.map_err(|e| format!("Failed to write to keyring: {e}"))
|
||||
}
|
||||
|
||||
fn write_secrets_to_file(&self, secrets: &HashMap<String, Value>) -> Result<(), String> {
|
||||
let path = self.config_dir.join(SECRETS_YAML_NAME);
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
let yaml_map: HashMap<String, String> = secrets
|
||||
.iter()
|
||||
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
|
||||
.collect();
|
||||
|
||||
let yaml = serde_yaml::to_string(&yaml_map)
|
||||
.map_err(|e| format!("Failed to serialize secrets: {e}"))?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.mode(0o600)
|
||||
.open(&path)
|
||||
.map_err(|e| format!("Failed to open secrets file: {e}"))?;
|
||||
|
||||
file.write_all(yaml.as_bytes())
|
||||
.map_err(|e| format!("Failed to write secrets file: {e}"))
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
std::fs::write(&path, yaml).map_err(|e| format!("Failed to write secrets file: {e}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,2 @@
|
||||
pub mod acp;
|
||||
pub mod goose_config;
|
||||
pub mod personas;
|
||||
pub(crate) mod provider_defs;
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
pub(crate) struct ConfigKey {
|
||||
pub name: &'static str,
|
||||
pub is_secret: bool,
|
||||
pub required: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct ProviderConfigDef {
|
||||
pub id: &'static str,
|
||||
pub keys: &'static [ConfigKey],
|
||||
pub oauth_cache_path: Option<&'static str>,
|
||||
}
|
||||
|
||||
const fn key(name: &'static str, is_secret: bool, required: bool) -> ConfigKey {
|
||||
ConfigKey {
|
||||
name,
|
||||
is_secret,
|
||||
required,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) static PROVIDER_CONFIG_DEFS: &[ProviderConfigDef] = &[
|
||||
ProviderConfigDef {
|
||||
id: "anthropic",
|
||||
keys: &[key("ANTHROPIC_API_KEY", true, true)],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "openai",
|
||||
keys: &[key("OPENAI_API_KEY", true, true)],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "google",
|
||||
keys: &[key("GOOGLE_API_KEY", true, true)],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "openrouter",
|
||||
keys: &[key("OPENROUTER_API_KEY", true, true)],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "xai",
|
||||
keys: &[key("XAI_API_KEY", true, true)],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "nanogpt",
|
||||
keys: &[key("NANOGPT_API_KEY", true, true)],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "venice",
|
||||
keys: &[key("VENICE_API_KEY", true, true)],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "tetrate",
|
||||
keys: &[key("TETRATE_API_KEY", true, true)],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "databricks",
|
||||
keys: &[
|
||||
key("DATABRICKS_HOST", false, true),
|
||||
key("DATABRICKS_TOKEN", true, false),
|
||||
],
|
||||
oauth_cache_path: Some("databricks/oauth"),
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "snowflake",
|
||||
keys: &[
|
||||
key("SNOWFLAKE_HOST", false, true),
|
||||
key("SNOWFLAKE_TOKEN", true, true),
|
||||
],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "litellm",
|
||||
keys: &[
|
||||
key("LITELLM_HOST", false, true),
|
||||
key("LITELLM_API_KEY", true, false),
|
||||
],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "azure",
|
||||
keys: &[
|
||||
key("AZURE_OPENAI_ENDPOINT", false, true),
|
||||
key("AZURE_OPENAI_DEPLOYMENT_NAME", false, true),
|
||||
key("AZURE_OPENAI_API_KEY", true, false),
|
||||
],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "bedrock",
|
||||
keys: &[key("AWS_REGION", false, false)],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "gcp_vertex_ai",
|
||||
keys: &[
|
||||
key("GCP_PROJECT_ID", false, true),
|
||||
key("GCP_LOCATION", false, true),
|
||||
],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "chatgpt_codex",
|
||||
keys: &[key("CHATGPT_CODEX_TOKEN", true, true)],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "github_copilot",
|
||||
keys: &[],
|
||||
oauth_cache_path: Some("githubcopilot/info.json"),
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "ollama",
|
||||
keys: &[],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "local_inference",
|
||||
keys: &[],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
// Dictation providers (voice input)
|
||||
ProviderConfigDef {
|
||||
id: "dictation_groq",
|
||||
keys: &[key("GROQ_API_KEY", true, true)],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
ProviderConfigDef {
|
||||
id: "dictation_elevenlabs",
|
||||
keys: &[key("ELEVENLABS_API_KEY", true, true)],
|
||||
oauth_cache_path: None,
|
||||
},
|
||||
];
|
||||
|
||||
pub(crate) fn find_config_key(key_name: &str) -> Option<&'static ConfigKey> {
|
||||
PROVIDER_CONFIG_DEFS
|
||||
.iter()
|
||||
.flat_map(|def| def.keys.iter())
|
||||
.find(|key| key.name == key_name)
|
||||
}
|
||||
|
||||
pub(crate) fn find_provider_def(provider_id: &str) -> Option<&'static ProviderConfigDef> {
|
||||
PROVIDER_CONFIG_DEFS.iter().find(|d| d.id == provider_id)
|
||||
}
|
||||
Reference in New Issue
Block a user