- Add aider-runner.ts sidecar that spawns aider CLI in non-interactive mode - Add Aider provider metadata with OpenRouter model presets - Add aider-messages.ts adapter for Aider event format - Refactor SidecarManager from single-process to per-provider process management with lazy startup on first query and session→provider routing - Add openrouter_api_key to secrets system (keyring storage) - Inject OPENROUTER_API_KEY from secrets into Aider agent environment - Register Aider in provider registry, build pipeline, and resource bundle Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
130 lines
4.2 KiB
Rust
130 lines
4.2 KiB
Rust
//! Secrets management via system keyring (libsecret on Linux).
|
|
//!
|
|
//! Stores secrets in the OS keyring (GNOME Keyring / KDE Wallet).
|
|
//! A metadata entry "__bterminal_keys__" tracks known key names.
|
|
//! If the keyring is unavailable, operations return explicit errors
|
|
//! rather than falling back to plaintext storage.
|
|
|
|
use keyring::Entry;
|
|
|
|
const SERVICE: &str = "bterminal";
|
|
const KEYS_META: &str = "__bterminal_keys__";
|
|
|
|
/// Known secret key identifiers.
|
|
pub const KNOWN_KEYS: &[&str] = &[
|
|
"anthropic_api_key",
|
|
"openai_api_key",
|
|
"openrouter_api_key",
|
|
"github_token",
|
|
"relay_token",
|
|
];
|
|
|
|
pub struct SecretsManager;
|
|
|
|
impl SecretsManager {
|
|
/// Store a secret value in the system keyring.
|
|
pub fn store_secret(key: &str, value: &str) -> Result<(), String> {
|
|
let entry = Entry::new(SERVICE, key).map_err(|e| format!("keyring init error: {e}"))?;
|
|
entry
|
|
.set_password(value)
|
|
.map_err(|e| format!("failed to store secret '{key}': {e}"))?;
|
|
|
|
// Track the key in metadata
|
|
Self::add_key_to_meta(key)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Retrieve a secret value from the system keyring.
|
|
/// Returns Ok(None) if the key does not exist.
|
|
pub fn get_secret(key: &str) -> Result<Option<String>, String> {
|
|
let entry = Entry::new(SERVICE, key).map_err(|e| format!("keyring init error: {e}"))?;
|
|
match entry.get_password() {
|
|
Ok(pw) => Ok(Some(pw)),
|
|
Err(keyring::Error::NoEntry) => Ok(None),
|
|
Err(e) => Err(format!("failed to get secret '{key}': {e}")),
|
|
}
|
|
}
|
|
|
|
/// Delete a secret from the system keyring.
|
|
pub fn delete_secret(key: &str) -> Result<(), String> {
|
|
let entry = Entry::new(SERVICE, key).map_err(|e| format!("keyring init error: {e}"))?;
|
|
match entry.delete_credential() {
|
|
Ok(()) => {}
|
|
Err(keyring::Error::NoEntry) => {} // already absent, not an error
|
|
Err(e) => return Err(format!("failed to delete secret '{key}': {e}")),
|
|
}
|
|
|
|
Self::remove_key_from_meta(key)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// List keys that have been stored (read from metadata entry).
|
|
pub fn list_keys() -> Result<Vec<String>, String> {
|
|
let entry =
|
|
Entry::new(SERVICE, KEYS_META).map_err(|e| format!("keyring init error: {e}"))?;
|
|
match entry.get_password() {
|
|
Ok(raw) => {
|
|
let keys: Vec<String> = raw
|
|
.split('\n')
|
|
.filter(|s| !s.is_empty())
|
|
.map(String::from)
|
|
.collect();
|
|
Ok(keys)
|
|
}
|
|
Err(keyring::Error::NoEntry) => Ok(Vec::new()),
|
|
Err(e) => Err(format!("failed to list secret keys: {e}")),
|
|
}
|
|
}
|
|
|
|
/// Check whether the system keyring is available.
|
|
pub fn has_keyring() -> bool {
|
|
// Attempt to create an entry — this is the cheapest probe.
|
|
Entry::new(SERVICE, "__probe__").is_ok()
|
|
}
|
|
|
|
// --- internal helpers ---
|
|
|
|
fn add_key_to_meta(key: &str) -> Result<(), String> {
|
|
let mut keys = Self::list_keys().unwrap_or_default();
|
|
if !keys.iter().any(|k| k == key) {
|
|
keys.push(key.to_string());
|
|
Self::save_meta(&keys)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn remove_key_from_meta(key: &str) -> Result<(), String> {
|
|
let mut keys = Self::list_keys().unwrap_or_default();
|
|
keys.retain(|k| k != key);
|
|
Self::save_meta(&keys)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn save_meta(keys: &[String]) -> Result<(), String> {
|
|
let entry =
|
|
Entry::new(SERVICE, KEYS_META).map_err(|e| format!("keyring init error: {e}"))?;
|
|
let data = keys.join("\n");
|
|
entry
|
|
.set_password(&data)
|
|
.map_err(|e| format!("failed to save key metadata: {e}"))?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn known_keys_are_not_empty() {
|
|
assert!(!KNOWN_KEYS.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn known_keys_contains_expected() {
|
|
assert!(KNOWN_KEYS.contains(&"anthropic_api_key"));
|
|
assert!(KNOWN_KEYS.contains(&"openai_api_key"));
|
|
assert!(KNOWN_KEYS.contains(&"github_token"));
|
|
assert!(KNOWN_KEYS.contains(&"relay_token"));
|
|
}
|
|
}
|