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