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}