Skip to main content

agentics_config/
storage_config.rs

1use std::str::FromStr;
2
3use serde::Deserialize;
4
5use crate::{Config, validate_required_trimmed};
6
7/// Environment variable that selects durable object storage backend.
8pub const ENV_AGENTICS_STORAGE_BACKEND: &str = "AGENTICS_STORAGE_BACKEND";
9/// Environment variable that configures local filesystem durable object storage.
10pub const ENV_AGENTICS_STORAGE_ROOT: &str = "AGENTICS_STORAGE_ROOT";
11/// Environment variable that configures local storage staging and downloads.
12pub const ENV_AGENTICS_STORAGE_WORK_ROOT: &str = "AGENTICS_STORAGE_WORK_ROOT";
13/// Environment variable that configures the S3-compatible bucket name.
14pub const ENV_AGENTICS_S3_BUCKET: &str = "AGENTICS_S3_BUCKET";
15/// Environment variable that configures the S3-compatible object key prefix.
16pub const ENV_AGENTICS_S3_PREFIX: &str = "AGENTICS_S3_PREFIX";
17/// Environment variable that configures the S3-compatible region.
18pub const ENV_AGENTICS_S3_REGION: &str = "AGENTICS_S3_REGION";
19/// Environment variable that configures the S3-compatible endpoint URL.
20pub const ENV_AGENTICS_S3_ENDPOINT_URL: &str = "AGENTICS_S3_ENDPOINT_URL";
21/// Environment variable that enables S3 path-style access.
22pub const ENV_AGENTICS_S3_FORCE_PATH_STYLE: &str = "AGENTICS_S3_FORCE_PATH_STYLE";
23
24/// Default durable object storage backend.
25pub const DEFAULT_STORAGE_BACKEND: StorageBackend = StorageBackend::S3;
26/// Default local filesystem durable object storage root for explicit local mode.
27pub const DEFAULT_STORAGE_ROOT: &str = "storage";
28/// Default S3-compatible bucket for local, test, and single-host deployments.
29pub const DEFAULT_S3_BUCKET: &str = "agentics";
30/// Default S3-compatible region for RustFS and local-compatible services.
31pub const DEFAULT_S3_REGION: &str = "us-east-1";
32/// Default local RustFS endpoint for non-Compose S3-backed development.
33pub const DEFAULT_S3_ENDPOINT_URL: &str = "http://127.0.0.1:9000";
34/// Default S3 path-style setting for RustFS-compatible object storage.
35pub 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/// Durable storage backend for platform objects.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum StorageBackend {
46    /// Store durable objects under `AGENTICS_STORAGE_ROOT`.
47    Local,
48    /// Store durable objects in an S3-compatible bucket.
49    S3,
50}
51
52impl StorageBackend {
53    /// Stable environment string for this durable storage backend.
54    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
76/// Validate durable object storage configuration.
77pub(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
95/// Validate an optional S3 key prefix.
96pub(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}