agentics-config 0.3.0

Configuration loading and validation for Agentics services and tooling.
Documentation
use std::str::FromStr;

use serde::Deserialize;

use crate::{Config, validate_required_trimmed};

/// Environment variable that selects durable object storage backend.
pub const ENV_AGENTICS_STORAGE_BACKEND: &str = "AGENTICS_STORAGE_BACKEND";
/// Environment variable that configures local filesystem durable object storage.
pub const ENV_AGENTICS_STORAGE_ROOT: &str = "AGENTICS_STORAGE_ROOT";
/// Environment variable that configures local storage staging and downloads.
pub const ENV_AGENTICS_STORAGE_WORK_ROOT: &str = "AGENTICS_STORAGE_WORK_ROOT";
/// Environment variable that configures the S3-compatible bucket name.
pub const ENV_AGENTICS_S3_BUCKET: &str = "AGENTICS_S3_BUCKET";
/// Environment variable that configures the S3-compatible object key prefix.
pub const ENV_AGENTICS_S3_PREFIX: &str = "AGENTICS_S3_PREFIX";
/// Environment variable that configures the S3-compatible region.
pub const ENV_AGENTICS_S3_REGION: &str = "AGENTICS_S3_REGION";
/// Environment variable that configures the S3-compatible endpoint URL.
pub const ENV_AGENTICS_S3_ENDPOINT_URL: &str = "AGENTICS_S3_ENDPOINT_URL";
/// Environment variable that enables S3 path-style access.
pub const ENV_AGENTICS_S3_FORCE_PATH_STYLE: &str = "AGENTICS_S3_FORCE_PATH_STYLE";

/// Default durable object storage backend.
pub const DEFAULT_STORAGE_BACKEND: StorageBackend = StorageBackend::S3;
/// Default local filesystem durable object storage root for explicit local mode.
pub const DEFAULT_STORAGE_ROOT: &str = "storage";
/// Default S3-compatible bucket for local, test, and single-host deployments.
pub const DEFAULT_S3_BUCKET: &str = "agentics";
/// Default S3-compatible region for RustFS and local-compatible services.
pub const DEFAULT_S3_REGION: &str = "us-east-1";
/// Default local RustFS endpoint for non-Compose S3-backed development.
pub const DEFAULT_S3_ENDPOINT_URL: &str = "http://127.0.0.1:9000";
/// Default S3 path-style setting for RustFS-compatible object storage.
pub const DEFAULT_S3_FORCE_PATH_STYLE: bool = true;

pub(crate) const DEFAULT_STORAGE_MAX_BUNDLE_ARCHIVE_BYTES: u64 = 1024 * 1024 * 1024;
pub(crate) const DEFAULT_STORAGE_MAX_STATEMENT_BYTES: u64 = 1024 * 1024;
pub(crate) const DEFAULT_STORAGE_MAX_JSON_ARTIFACT_BYTES: u64 = 1024 * 1024;
pub(crate) const DEFAULT_STORAGE_TMP_OBJECT_GRACE_HOURS: u64 = 24;

/// Durable storage backend for platform objects.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StorageBackend {
    /// Store durable objects under `AGENTICS_STORAGE_ROOT`.
    Local,
    /// Store durable objects in an S3-compatible bucket.
    S3,
}

impl StorageBackend {
    /// Stable environment string for this durable storage backend.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Local => "local",
            Self::S3 => "s3",
        }
    }
}

impl FromStr for StorageBackend {
    type Err = anyhow::Error;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value.trim() {
            "local" => Ok(Self::Local),
            "s3" => Ok(Self::S3),
            other => anyhow::bail!(
                "{ENV_AGENTICS_STORAGE_BACKEND} must be either `local` or `s3`; got `{other}`"
            ),
        }
    }
}

/// Validate durable object storage configuration.
pub(crate) fn validate_object_storage_config(config: &Config) -> anyhow::Result<()> {
    crate::validation::validate_report(&config.storage)?;
    match config.storage.backend {
        StorageBackend::Local => Ok(()),
        StorageBackend::S3 => {
            validate_required_trimmed(config.storage.s3_bucket.as_deref(), "AGENTICS_S3_BUCKET")?;
            validate_s3_prefix(config.storage.s3_prefix.as_deref())?;
            validate_required_trimmed(Some(&config.storage.s3_region), "AGENTICS_S3_REGION")?;
            if let Some(endpoint_url) = &config.storage.s3_endpoint_url
                && !matches!(endpoint_url.scheme(), "http" | "https")
            {
                anyhow::bail!("AGENTICS_S3_ENDPOINT_URL must start with http:// or https://");
            }
            Ok(())
        }
    }
}

/// Validate an optional S3 key prefix.
pub(crate) fn validate_s3_prefix(value: Option<&str>) -> anyhow::Result<()> {
    let Some(prefix) = value.map(str::trim).filter(|value| !value.is_empty()) else {
        return Ok(());
    };
    if prefix.starts_with('/') || prefix.ends_with('/') || prefix.contains('\\') {
        anyhow::bail!(
            "AGENTICS_S3_PREFIX must be a relative slash-separated storage prefix without leading or trailing slash"
        );
    }
    for component in prefix.split('/') {
        if component.is_empty()
            || component == "."
            || component == ".."
            || component.bytes().any(|byte| {
                byte.is_ascii_whitespace()
                    || byte.is_ascii_control()
                    || !(byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
            })
        {
            anyhow::bail!("AGENTICS_S3_PREFIX contains an unsafe path component");
        }
    }
    Ok(())
}