Skip to main content

aegis_tools/
remotes.rs

1//! Named remote-server credentials ("credential vault" by handle).
2//!
3//! Credentials (host/user/password) are stored locally in
4//! `~/.aegis/remotes.json` and referenced by a short **name** ("handle"). The
5//! agent operates a server via its handle (e.g. `remote run server=srv1 …`), so
6//! the real host/user/password are resolved locally at execution time and never
7//! appear in the model's tool-call arguments — i.e. they never reach the LLM
8//! provider. Add credentials through a local channel (the `/server` command),
9//! not by telling the model, so they stay off the prompt entirely.
10
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::PathBuf;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct RemoteCred {
17    pub host: String,
18    pub user: String,
19    #[serde(default)]
20    pub password: Option<String>,
21    #[serde(default = "default_port")]
22    pub port: u64,
23    /// Optional path to an SSH private key (alternative to password).
24    #[serde(default)]
25    pub key: Option<String>,
26}
27
28fn default_port() -> u64 {
29    22
30}
31
32fn store_path() -> PathBuf {
33    aegis_types::paths::config_dir().join("remotes.json")
34}
35
36/// Load all stored credentials (name → cred).
37pub fn load_all() -> HashMap<String, RemoteCred> {
38    std::fs::read_to_string(store_path())
39        .ok()
40        .and_then(|c| serde_json::from_str(&c).ok())
41        .unwrap_or_default()
42}
43
44/// Resolve a server handle to its credentials.
45pub fn get(name: &str) -> Option<RemoteCred> {
46    load_all().get(name).cloned()
47}
48
49/// Names of all stored servers (sorted).
50pub fn list_names() -> Vec<String> {
51    let mut v: Vec<String> = load_all().into_keys().collect();
52    v.sort();
53    v
54}
55
56/// Save (insert/overwrite) a named credential; file is chmod 600 on unix.
57pub fn save(name: &str, cred: RemoteCred) -> anyhow::Result<()> {
58    let mut all = load_all();
59    all.insert(name.to_string(), cred);
60    let path = store_path();
61    if let Some(p) = path.parent() {
62        std::fs::create_dir_all(p)?;
63    }
64    std::fs::write(&path, serde_json::to_string_pretty(&all)?)?;
65    #[cfg(unix)]
66    {
67        use std::os::unix::fs::PermissionsExt;
68        let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
69    }
70    Ok(())
71}
72
73/// Remove a named credential. Returns true if it existed.
74pub fn remove(name: &str) -> anyhow::Result<bool> {
75    let mut all = load_all();
76    let existed = all.remove(name).is_some();
77    if existed {
78        std::fs::write(store_path(), serde_json::to_string_pretty(&all)?)?;
79    }
80    Ok(existed)
81}
82
83/// All stored passwords (≥4 chars), for exact-match egress masking.
84pub fn all_passwords() -> Vec<String> {
85    load_all()
86        .into_values()
87        .filter_map(|c| c.password)
88        .filter(|p| p.len() >= 4)
89        .collect()
90}