Skip to main content

nexo_memory_snapshot/
request.rs

1//! Inputs to [`crate::snapshotter::MemorySnapshotter::snapshot`] and
2//! [`crate::snapshotter::MemorySnapshotter::restore`].
3
4use std::path::PathBuf;
5
6use crate::id::AgentId;
7
8/// Encryption recipient supplied at snapshot time.
9#[derive(Debug, Clone)]
10#[non_exhaustive]
11pub enum EncryptionKey {
12    /// `age` recipient string in the canonical bech32 form
13    /// (`age1...`). Single-recipient path; CLI uses this for
14    /// `--encrypt age:age1...`. Wraps the bundle body; manifest
15    /// stays plaintext inside the encrypted payload.
16    AgePublicKey(String),
17
18    /// Multi-recipient variant. Bundle body
19    /// is wrapped with one `age` header per recipient; any matching
20    /// identity can decrypt the body. Used by the admin RPC create
21    /// path so all `EncryptionSection.recipients` participate in
22    /// the encrypted bundle. Empty Vec is rejected at pack time
23    /// as `SnapshotError::Encryption("empty recipients")`.
24    /// Duplicate recipient strings are silently deduplicated with
25    /// a `tracing::debug!` (operator paste-twice typo is non-fatal).
26    AgePublicKeys(Vec<String>),
27}
28
29/// Identity supplied at restore time to decrypt an age-wrapped bundle.
30#[derive(Debug, Clone)]
31#[non_exhaustive]
32pub enum DecryptionIdentity {
33    /// Path to a file containing one or more age identities. Loaded
34    /// once per restore call; the file's bytes do not leave this
35    /// process.
36    AgeIdentityFile(PathBuf),
37}
38
39#[derive(Debug, Clone)]
40pub struct SnapshotRequest {
41    pub agent_id: AgentId,
42    pub tenant: String,
43    pub label: Option<String>,
44    /// When `true`, secret-guard scanner runs over the staged bundle
45    /// before packing and the manifest carries a `RedactionReport`.
46    pub redact_secrets: bool,
47    pub encrypt: Option<EncryptionKey>,
48    /// Free-form provenance string surfaced in the manifest's
49    /// `created_by` column. Caller picks the value (`cli`, `tool`,
50    /// `auto-pre-restore`, …); no validation here.
51    pub created_by: String,
52}
53
54impl SnapshotRequest {
55    /// Operator-driven snapshot via the CLI. Defaults secrets-on
56    /// because operators rarely want to ship secrets into a portable
57    /// bundle by accident.
58    pub fn cli(agent_id: impl Into<AgentId>, tenant: impl Into<String>) -> Self {
59        Self {
60            agent_id: agent_id.into(),
61            tenant: tenant.into(),
62            label: None,
63            redact_secrets: true,
64            encrypt: None,
65            created_by: "cli".into(),
66        }
67    }
68}
69
70#[derive(Debug, Clone)]
71pub struct RestoreRequest {
72    pub agent_id: AgentId,
73    pub tenant: String,
74    pub bundle: PathBuf,
75    /// `true` reports the diff that would be applied without mutating
76    /// the live agent.
77    pub dry_run: bool,
78    /// `true` (default) snapshots the live state before applying the
79    /// restore so the operation can be reversed.
80    pub auto_pre_snapshot: bool,
81    /// Required when the bundle's manifest has an `encryption` block.
82    pub decrypt: Option<DecryptionIdentity>,
83}
84
85impl RestoreRequest {
86    /// Sensible default: dry-run off, auto-pre-snapshot on. Callers
87    /// flip the booleans explicitly when they want destructive behavior.
88    pub fn new(
89        agent_id: impl Into<AgentId>,
90        tenant: impl Into<String>,
91        bundle: impl Into<PathBuf>,
92    ) -> Self {
93        Self {
94            agent_id: agent_id.into(),
95            tenant: tenant.into(),
96            bundle: bundle.into(),
97            dry_run: false,
98            auto_pre_snapshot: true,
99            decrypt: None,
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn snapshot_request_cli_defaults_redact_on() {
110        let r = SnapshotRequest::cli("ana", "default");
111        assert!(r.redact_secrets);
112        assert!(r.encrypt.is_none());
113        assert_eq!(r.created_by, "cli");
114    }
115
116    #[test]
117    fn restore_request_defaults_to_destructive_with_pre_snapshot() {
118        let r = RestoreRequest::new("ana", "default", "/tmp/x.tar.zst");
119        assert!(!r.dry_run);
120        assert!(r.auto_pre_snapshot);
121    }
122
123    #[test]
124    fn encryption_key_age_round_trip_via_clone() {
125        let k = EncryptionKey::AgePublicKey("age1abc".into());
126        let cloned = k.clone();
127        match cloned {
128            EncryptionKey::AgePublicKey(s) => assert_eq!(s, "age1abc"),
129            EncryptionKey::AgePublicKeys(_) => panic!("wrong variant"),
130        }
131    }
132}