Skip to main content

auths_id/
agent_identity.rs

1//! Agent identity config + TOML preview types.
2//!
3//! The standalone-`icp` agent provisioning (`provision_agent_identity`) was retired
4//! in Epic E: an agent is now a KERI **delegated identifier** (`dip` delegated by a
5//! root/org, anchored by the root's `ixn`), created with `auths id agent add`
6//! (SDK `agents::add`) — not a standalone root identity stamped with an `Agent`
7//! attestation. What remains here is the configuration shape and the
8//! `auths-agent.toml` formatter still used by the `init` dry-run preview.
9
10use std::path::PathBuf;
11
12use auths_core::storage::keychain::IdentityDID;
13
14/// Storage mode for an agent identity.
15#[derive(Debug, Clone)]
16pub enum AgentStorageMode {
17    /// Persistent storage at a filesystem path.
18    /// Defaults to `~/.auths-agent` if `repo_path` is `None`.
19    Persistent {
20        /// Repository path; `None` selects the default `~/.auths-agent`.
21        repo_path: Option<PathBuf>,
22    },
23    /// In-memory storage for ephemeral/stateless containers (Fargate, Docker).
24    /// Agent identity lives only for the process lifetime.
25    InMemory,
26}
27
28/// Configuration describing an agent identity (for previews / config files).
29#[derive(Debug, Clone)]
30pub struct AgentProvisioningConfig {
31    /// Human-readable agent name (e.g., "ci-bot", "release-agent").
32    pub agent_name: String,
33    /// Capabilities to grant (e.g., `sign_commit`, `pr:create`).
34    pub capabilities: Vec<auths_keri::Capability>,
35    /// Duration in seconds until expiration (per RFC 6749).
36    pub expires_in: Option<u64>,
37    /// DID of the root/org that delegates this agent.
38    pub delegated_by: Option<IdentityDID>,
39    /// Storage mode (persistent or ephemeral).
40    pub storage_mode: AgentStorageMode,
41}
42
43/// Render an `auths-agent.toml` preview for the given agent config.
44///
45/// Args:
46/// * `did`: The agent's `did:keri:` (or a `<pending>` placeholder in a dry run).
47/// * `key_alias`: The keychain alias the agent key is stored under.
48/// * `config`: The agent configuration to render.
49///
50/// Usage:
51/// ```ignore
52/// let toml = format_agent_toml("did:keri:E...", "agent-key", &config);
53/// ```
54pub fn format_agent_toml(did: &str, key_alias: &str, config: &AgentProvisioningConfig) -> String {
55    let caps = config
56        .capabilities
57        .iter()
58        .map(|c| format!("\"{}\"", c))
59        .collect::<Vec<_>>()
60        .join(", ");
61
62    let mut out = format!(
63        "# Auths Agent Configuration\n\
64         # An agent is a KERI delegated identifier (dip) — create with `auths id agent add`\n\n\
65         [agent]\n\
66         name = \"{}\"\n\
67         did = \"{}\"\n\
68         key_alias = \"{}\"\n\
69         signer_type = \"Agent\"\n",
70        config.agent_name, did, key_alias,
71    );
72
73    if let Some(ref delegator) = config.delegated_by {
74        out.push_str(&format!("delegated_by = \"{}\"\n", delegator));
75    }
76
77    out.push_str(&format!("\n[capabilities]\ngranted = [{}]\n", caps));
78
79    if let Some(secs) = config.expires_in {
80        out.push_str(&format!("\n[expiry]\nexpires_in = {}\n", secs));
81    }
82
83    out
84}
85
86#[cfg(test)]
87#[allow(clippy::disallowed_methods)] // INVARIANT: tests construct IdentityDID via new_unchecked with literal DIDs
88mod tests {
89    use super::*;
90
91    #[test]
92    fn format_agent_toml_with_all_fields() {
93        let config = AgentProvisioningConfig {
94            agent_name: "ci-bot".to_string(),
95            capabilities: vec![
96                auths_keri::Capability::sign_commit(),
97                auths_keri::Capability::parse("pr:create").unwrap(),
98            ],
99            expires_in: Some(86400),
100            delegated_by: Some(IdentityDID::new_unchecked("did:keri:Eabc123")),
101            storage_mode: AgentStorageMode::Persistent { repo_path: None },
102        };
103        let toml = format_agent_toml("did:keri:Eagent", "agent-key", &config);
104        assert!(toml.contains("name = \"ci-bot\""));
105        assert!(toml.contains("did = \"did:keri:Eagent\""));
106        assert!(toml.contains("delegated_by = \"did:keri:Eabc123\""));
107        assert!(toml.contains("\"sign_commit\", \"pr:create\""));
108        assert!(toml.contains("expires_in = 86400"));
109    }
110
111    #[test]
112    fn format_agent_toml_minimal() {
113        let config = AgentProvisioningConfig {
114            agent_name: "solo".to_string(),
115            capabilities: vec![],
116            expires_in: None,
117            delegated_by: None,
118            storage_mode: AgentStorageMode::InMemory,
119        };
120        let toml = format_agent_toml("did:keri:E1", "k", &config);
121        assert!(!toml.contains("delegated_by"));
122        assert!(!toml.contains("[expiry]"));
123    }
124
125    #[test]
126    fn init_dryrun_shows_delegated_agent() {
127        // The `init --agent` dry-run renders this preview; after Epic E it must
128        // present a *delegated* identifier (not a standalone identity) and name the
129        // delegating root when one is supplied.
130        let config = AgentProvisioningConfig {
131            agent_name: "deploy-bot".to_string(),
132            capabilities: vec![auths_keri::Capability::sign_commit()],
133            expires_in: None,
134            delegated_by: Some(IdentityDID::new_unchecked("did:keri:Eroot")),
135            storage_mode: AgentStorageMode::Persistent { repo_path: None },
136        };
137        let toml = format_agent_toml("did:keri:E<pending>", "agent-key", &config);
138        assert!(
139            toml.contains("delegated identifier"),
140            "dry-run must frame the agent as a delegated identifier"
141        );
142        assert!(
143            toml.contains("delegated_by = \"did:keri:Eroot\""),
144            "dry-run must name the delegating root"
145        );
146    }
147}