baqup_agent/
lib.rs

1//! # baqup-agent
2//!
3//! SDK for building baqup backup agents in Rust.
4//!
5//! > ⚠️ **This is a placeholder crate.** Full implementation coming soon.
6//!
7//! ## What is baqup?
8//!
9//! [baqup](https://github.com/baqupio/baqup) is a container-native backup orchestration system.
10//! Agents are stateless containers that perform backup and restore operations.
11//!
12//! This SDK provides everything needed to build a compliant baqup agent:
13//!
14//! - **Contract types** - `ExitCode`, `AgentState`, `LogLevel`
15//! - **Structured logging** - JSON logs with required fields
16//! - **Redis communication** - Bus client with filesystem fallback
17//! - **Heartbeat management** - Background thread with intent signalling
18//! - **Staging utilities** - Atomic writes, checksums, path validation
19//! - **Secret handling** - Wrapper preventing accidental exposure
20//!
21//! ## Example (Preview API)
22//!
23//! ```rust
24//! use baqup_agent::{ExitCode, AgentState, Secret};
25//!
26//! // Exit codes are already available
27//! assert_eq!(ExitCode::Success as i32, 0);
28//! assert_eq!(ExitCode::UsageConfigError as i32, 64);
29//!
30//! // Secret wrapper (available now)
31//! let password = Secret::new("my-secret-password");
32//! assert_eq!(format!("{}", password), "[REDACTED]");
33//! assert_eq!(password.reveal(), "my-secret-password");
34//! ```
35
36use std::fmt;
37use std::path::Path;
38use thiserror::Error;
39
40/// Crate version
41pub const VERSION: &str = env!("CARGO_PKG_VERSION");
42
43/// Exit codes from AGENT-CONTRACT-SPEC.md §5
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[repr(i32)]
46pub enum ExitCode {
47    /// Success
48    Success = 0,
49    /// General failure
50    GeneralFailure = 1,
51    /// Usage/config error
52    UsageConfigError = 64,
53    /// Data error
54    DataError = 65,
55    /// Resource unavailable
56    ResourceUnavailable = 69,
57    /// Internal error
58    InternalError = 70,
59    /// Can't create output
60    CantCreateOutput = 73,
61    /// I/O error
62    IoError = 74,
63    /// Completed but unreported
64    CompletedUnreported = 75,
65    /// Partial failure
66    PartialFailure = 76,
67}
68
69impl From<ExitCode> for i32 {
70    fn from(code: ExitCode) -> i32 {
71        code as i32
72    }
73}
74
75/// Agent lifecycle states from AGENT-CONTRACT-SPEC.md §1
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum AgentState {
78    /// Config validation, bus connection, first heartbeat
79    Initializing,
80    /// Active work, heartbeats every N seconds
81    Running,
82    /// Writing final artifacts, checksums, status
83    Completing,
84    /// Exit 0, status reported
85    Terminated,
86    /// Exit non-zero, error reported if possible
87    Failed,
88}
89
90impl fmt::Display for AgentState {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            AgentState::Initializing => write!(f, "initializing"),
94            AgentState::Running => write!(f, "running"),
95            AgentState::Completing => write!(f, "completing"),
96            AgentState::Terminated => write!(f, "terminated"),
97            AgentState::Failed => write!(f, "failed"),
98        }
99    }
100}
101
102/// Log levels from AGENT-CONTRACT-SPEC.md §6
103#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
104#[repr(u8)]
105pub enum LogLevel {
106    Trace = 0,
107    Debug = 1,
108    Info = 2,
109    Warn = 3,
110    Error = 4,
111    Fatal = 5,
112}
113
114/// Secret wrapper that prevents accidental exposure in logs.
115///
116/// From AGENT-CONTRACT-SPEC.md §7.
117///
118/// # Example
119///
120/// ```rust
121/// use baqup_agent::Secret;
122///
123/// let password = Secret::new("my-secret");
124/// assert_eq!(format!("{}", password), "[REDACTED]");
125/// assert_eq!(password.reveal(), "my-secret");
126/// ```
127#[derive(Clone)]
128pub struct Secret {
129    value: String,
130}
131
132impl Secret {
133    /// Create a new secret wrapper
134    pub fn new(value: impl Into<String>) -> Self {
135        Self {
136            value: value.into(),
137        }
138    }
139
140    /// Reveal the actual secret value
141    pub fn reveal(&self) -> &str {
142        &self.value
143    }
144}
145
146impl fmt::Display for Secret {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        write!(f, "[REDACTED]")
149    }
150}
151
152impl fmt::Debug for Secret {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        write!(f, "Secret([REDACTED])")
155    }
156}
157
158/// A single backup artifact
159#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
160pub struct Artifact {
161    /// Filename of the artifact
162    pub filename: String,
163    /// Size in bytes
164    pub size_bytes: u64,
165    /// Checksum algorithm (e.g., "sha256")
166    pub checksum_algorithm: String,
167    /// Checksum value
168    pub checksum_value: String,
169    /// Compression algorithm (if any)
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub compression: Option<String>,
172    /// Whether the artifact is encrypted
173    #[serde(default)]
174    pub encrypted: bool,
175}
176
177/// Backup manifest from AGENT-CONTRACT-SPEC.md §4
178#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
179pub struct Manifest {
180    /// Manifest version (e.g., "1.0")
181    pub version: String,
182    /// Job ID (UUID)
183    pub job_id: String,
184    /// Agent type (e.g., "agent-postgres")
185    pub agent: String,
186    /// Agent version
187    pub agent_version: String,
188    /// Role (snapshot, restore, etc.)
189    pub role: String,
190    /// Creation timestamp (ISO 8601)
191    pub created_at: String,
192    /// List of artifacts
193    pub artifacts: Vec<Artifact>,
194    /// Source metadata (optional)
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub source_metadata: Option<serde_json::Value>,
197}
198
199/// Agent SDK error types
200#[derive(Error, Debug)]
201pub enum AgentError {
202    #[error("Placeholder: {0}")]
203    Placeholder(String),
204
205    #[error("Configuration error: {0}")]
206    Config(String),
207
208    #[error("Bus connection error: {0}")]
209    Bus(String),
210
211    #[error("IO error: {0}")]
212    Io(#[from] std::io::Error),
213
214    #[error("JSON error: {0}")]
215    Json(#[from] serde_json::Error),
216
217    #[error("Path traversal detected: {0}")]
218    PathTraversal(String),
219}
220
221const PLACEHOLDER_MESSAGE: &str = concat!(
222    "baqup-agent is currently a placeholder crate. ",
223    "Full implementation coming soon. ",
224    "See https://github.com/baqupio/baqup for updates."
225);
226
227/// Compute file checksum
228///
229/// # Arguments
230///
231/// * `path` - Path to the file
232/// * `algorithm` - Hash algorithm (default: "sha256")
233///
234/// # Errors
235///
236/// Returns `AgentError::Placeholder` - this is a placeholder crate
237pub fn compute_checksum(_path: &Path, _algorithm: &str) -> Result<String, AgentError> {
238    Err(AgentError::Placeholder(PLACEHOLDER_MESSAGE.to_string()))
239}
240
241/// Validate path is within boundary (prevent traversal)
242///
243/// # Arguments
244///
245/// * `path` - Path to validate
246/// * `boundary` - Allowed root directory
247///
248/// # Returns
249///
250/// `true` if path is within boundary, `false` otherwise
251///
252/// # Errors
253///
254/// Returns `AgentError::Placeholder` - this is a placeholder crate
255pub fn validate_path(_path: &Path, _boundary: &Path) -> Result<bool, AgentError> {
256    Err(AgentError::Placeholder(PLACEHOLDER_MESSAGE.to_string()))
257}
258
259/// Atomic write pattern: .staging/ -> parent
260///
261/// # Arguments
262///
263/// * `staging_dir` - Root staging directory
264/// * `job_id` - Job identifier
265///
266/// # Errors
267///
268/// Returns `AgentError::Placeholder` - this is a placeholder crate
269pub fn atomic_write(_staging_dir: &Path, _job_id: &str) -> Result<String, AgentError> {
270    Err(AgentError::Placeholder(PLACEHOLDER_MESSAGE.to_string()))
271}
272
273/// Load configuration from schema
274///
275/// # Arguments
276///
277/// * `schema_path` - Path to the agent's JSON Schema file
278///
279/// # Errors
280///
281/// Returns `AgentError::Placeholder` - this is a placeholder crate
282pub fn load_config(
283    _schema_path: &str,
284) -> Result<std::collections::HashMap<String, serde_json::Value>, AgentError> {
285    Err(AgentError::Placeholder(PLACEHOLDER_MESSAGE.to_string()))
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn test_exit_codes() {
294        assert_eq!(ExitCode::Success as i32, 0);
295        assert_eq!(ExitCode::GeneralFailure as i32, 1);
296        assert_eq!(ExitCode::UsageConfigError as i32, 64);
297        assert_eq!(ExitCode::DataError as i32, 65);
298        assert_eq!(ExitCode::ResourceUnavailable as i32, 69);
299        assert_eq!(ExitCode::InternalError as i32, 70);
300        assert_eq!(ExitCode::CantCreateOutput as i32, 73);
301        assert_eq!(ExitCode::IoError as i32, 74);
302        assert_eq!(ExitCode::CompletedUnreported as i32, 75);
303        assert_eq!(ExitCode::PartialFailure as i32, 76);
304    }
305
306    #[test]
307    fn test_agent_state_display() {
308        assert_eq!(AgentState::Initializing.to_string(), "initializing");
309        assert_eq!(AgentState::Running.to_string(), "running");
310        assert_eq!(AgentState::Completing.to_string(), "completing");
311        assert_eq!(AgentState::Terminated.to_string(), "terminated");
312        assert_eq!(AgentState::Failed.to_string(), "failed");
313    }
314
315    #[test]
316    fn test_secret_redaction() {
317        let secret = Secret::new("my-password");
318        assert_eq!(format!("{}", secret), "[REDACTED]");
319        assert_eq!(format!("{:?}", secret), "Secret([REDACTED])");
320        assert_eq!(secret.reveal(), "my-password");
321    }
322
323    #[test]
324    fn test_log_level_ordering() {
325        assert!(LogLevel::Trace < LogLevel::Debug);
326        assert!(LogLevel::Debug < LogLevel::Info);
327        assert!(LogLevel::Info < LogLevel::Warn);
328        assert!(LogLevel::Warn < LogLevel::Error);
329        assert!(LogLevel::Error < LogLevel::Fatal);
330    }
331}