aether_cli/
credentials.rs1use crate::settings_args::SettingsSourceArgs;
2use aether_auth::{
3 EncryptedFileOAuthCredentialStorage, FakeOAuthCredentialStore, OAuthCredentialStorage, OAuthError, OsKeyringStore,
4};
5use aether_project::{AetherSettings, CredentialsStoreConfig};
6use std::path::Path;
7use std::sync::Arc;
8
9pub fn build_oauth_credential_store(
10 settings_source: &SettingsSourceArgs,
11 cwd: &Path,
12) -> Result<Arc<dyn OAuthCredentialStorage>, OAuthError> {
13 let result = match settings_source.source(cwd) {
14 Some(source) => AetherSettings::load(cwd, [source]),
15 None => AetherSettings::load_default(cwd),
16 };
17
18 let settings_source = match result {
19 Ok(settings) => Ok(settings.credentials_store),
20 Err(e) => Err(OAuthError::CredentialStore(format!("Failed to load settings for credential store config: {e}"))),
21 }?;
22
23 match settings_source {
24 Some(CredentialsStoreConfig::Memory) => Ok(Arc::new(FakeOAuthCredentialStore::new())),
25 Some(CredentialsStoreConfig::EncryptedFile { path, password_env }) => {
26 Ok(Arc::new(EncryptedFileOAuthCredentialStorage::from_settings(path, password_env.as_deref())?))
27 }
28 Some(CredentialsStoreConfig::Keyring) | None => Ok(Arc::new(OsKeyringStore::with_platform_store())),
29 }
30}
31
32#[cfg(test)]
33mod tests {
34 use super::*;
35
36 fn json_source(json: &str) -> SettingsSourceArgs {
37 SettingsSourceArgs { settings_json: Some(json.to_string()), settings_file: None }
38 }
39
40 #[test]
41 fn memory_store_selected_from_settings() {
42 let source = json_source(
43 r#"{
44 "credentialsStore": { "type": "memory" },
45 "agents": [{"name":"a","description":"a","model":"anthropic:claude-sonnet-4-5","userInvocable":true}]
46 }"#,
47 );
48
49 let store = build_oauth_credential_store(&source, Path::new(".")).unwrap();
50 assert!(!store.has_credential("anything"));
51 }
52
53 #[test]
54 fn encrypted_file_from_settings_propagates_missing_passphrase_error() {
55 let source = json_source(
56 r#"{
57 "credentialsStore": { "type": "encryptedFile", "passwordEnv": "AETHER_DEFINITELY_UNSET_VAR_98765" },
58 "agents": [{"name":"a","description":"a","model":"anthropic:claude-sonnet-4-5","userInvocable":true}]
59 }"#,
60 );
61
62 let result = build_oauth_credential_store(&source, Path::new("."));
63 assert!(result.is_err(), "missing passphrase should surface as a clean error");
64 }
65}