agentics_config/
storage_config.rs1use std::str::FromStr;
2
3use serde::Deserialize;
4
5use crate::{Config, validate_required_trimmed};
6
7pub const ENV_AGENTICS_STORAGE_BACKEND: &str = "AGENTICS_STORAGE_BACKEND";
9pub const ENV_AGENTICS_STORAGE_ROOT: &str = "AGENTICS_STORAGE_ROOT";
11pub const ENV_AGENTICS_STORAGE_WORK_ROOT: &str = "AGENTICS_STORAGE_WORK_ROOT";
13pub const ENV_AGENTICS_S3_BUCKET: &str = "AGENTICS_S3_BUCKET";
15pub const ENV_AGENTICS_S3_PREFIX: &str = "AGENTICS_S3_PREFIX";
17pub const ENV_AGENTICS_S3_REGION: &str = "AGENTICS_S3_REGION";
19pub const ENV_AGENTICS_S3_ENDPOINT_URL: &str = "AGENTICS_S3_ENDPOINT_URL";
21pub const ENV_AGENTICS_S3_FORCE_PATH_STYLE: &str = "AGENTICS_S3_FORCE_PATH_STYLE";
23
24pub const DEFAULT_STORAGE_BACKEND: StorageBackend = StorageBackend::S3;
26pub const DEFAULT_STORAGE_ROOT: &str = "storage";
28pub const DEFAULT_S3_BUCKET: &str = "agentics";
30pub const DEFAULT_S3_REGION: &str = "us-east-1";
32pub const DEFAULT_S3_ENDPOINT_URL: &str = "http://127.0.0.1:9000";
34pub const DEFAULT_S3_FORCE_PATH_STYLE: bool = true;
36
37pub(crate) const DEFAULT_STORAGE_MAX_BUNDLE_ARCHIVE_BYTES: u64 = 1024 * 1024 * 1024;
38pub(crate) const DEFAULT_STORAGE_MAX_STATEMENT_BYTES: u64 = 1024 * 1024;
39pub(crate) const DEFAULT_STORAGE_MAX_JSON_ARTIFACT_BYTES: u64 = 1024 * 1024;
40pub(crate) const DEFAULT_STORAGE_TMP_OBJECT_GRACE_HOURS: u64 = 24;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum StorageBackend {
46 Local,
48 S3,
50}
51
52impl StorageBackend {
53 pub fn as_str(self) -> &'static str {
55 match self {
56 Self::Local => "local",
57 Self::S3 => "s3",
58 }
59 }
60}
61
62impl FromStr for StorageBackend {
63 type Err = anyhow::Error;
64
65 fn from_str(value: &str) -> Result<Self, Self::Err> {
66 match value.trim() {
67 "local" => Ok(Self::Local),
68 "s3" => Ok(Self::S3),
69 other => anyhow::bail!(
70 "{ENV_AGENTICS_STORAGE_BACKEND} must be either `local` or `s3`; got `{other}`"
71 ),
72 }
73 }
74}
75
76pub(crate) fn validate_object_storage_config(config: &Config) -> anyhow::Result<()> {
78 crate::validation::validate_report(&config.storage)?;
79 match config.storage.backend {
80 StorageBackend::Local => Ok(()),
81 StorageBackend::S3 => {
82 validate_required_trimmed(config.storage.s3_bucket.as_deref(), "AGENTICS_S3_BUCKET")?;
83 validate_s3_prefix(config.storage.s3_prefix.as_deref())?;
84 validate_required_trimmed(Some(&config.storage.s3_region), "AGENTICS_S3_REGION")?;
85 if let Some(endpoint_url) = &config.storage.s3_endpoint_url
86 && !matches!(endpoint_url.scheme(), "http" | "https")
87 {
88 anyhow::bail!("AGENTICS_S3_ENDPOINT_URL must start with http:// or https://");
89 }
90 Ok(())
91 }
92 }
93}
94
95pub(crate) fn validate_s3_prefix(value: Option<&str>) -> anyhow::Result<()> {
97 let Some(prefix) = value.map(str::trim).filter(|value| !value.is_empty()) else {
98 return Ok(());
99 };
100 if prefix.starts_with('/') || prefix.ends_with('/') || prefix.contains('\\') {
101 anyhow::bail!(
102 "AGENTICS_S3_PREFIX must be a relative slash-separated storage prefix without leading or trailing slash"
103 );
104 }
105 for component in prefix.split('/') {
106 if component.is_empty()
107 || component == "."
108 || component == ".."
109 || component.bytes().any(|byte| {
110 byte.is_ascii_whitespace()
111 || byte.is_ascii_control()
112 || !(byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
113 })
114 {
115 anyhow::bail!("AGENTICS_S3_PREFIX contains an unsafe path component");
116 }
117 }
118 Ok(())
119}