simple-agents-workflow 0.5.2

Workflow IR and validation for SimpleAgents
Documentation
use std::path::{Path, PathBuf};

use super::contracts::{YamlWorkflow, YamlWorkflowRunError};

const MAX_WORKFLOW_YAML_BYTES: u64 = 1024 * 1024;
const MAX_WORKFLOW_YAML_DEPTH: usize = 64;

pub(crate) fn yaml_value_depth(value: &serde_yaml::Value, depth: usize) -> usize {
    match value {
        serde_yaml::Value::Sequence(items) => items
            .iter()
            .map(|item| yaml_value_depth(item, depth + 1))
            .max()
            .unwrap_or(depth),
        serde_yaml::Value::Mapping(map) => map
            .values()
            .map(|item| yaml_value_depth(item, depth + 1))
            .max()
            .unwrap_or(depth),
        _ => depth,
    }
}

pub(crate) fn is_yaml_file(path: &Path) -> bool {
    matches!(
        path.extension().and_then(|ext| ext.to_str()),
        Some("yaml") | Some("yml")
    )
}

pub(crate) fn load_workflow_yaml_file(
    workflow_path: &Path,
) -> Result<(PathBuf, YamlWorkflow), YamlWorkflowRunError> {
    let canonical_path =
        std::fs::canonicalize(workflow_path).map_err(|source| YamlWorkflowRunError::Read {
            path: workflow_path.display().to_string(),
            source,
        })?;

    let metadata =
        std::fs::metadata(&canonical_path).map_err(|source| YamlWorkflowRunError::Read {
            path: canonical_path.display().to_string(),
            source,
        })?;

    if !metadata.is_file() {
        return Err(YamlWorkflowRunError::FileRejected {
            path: canonical_path.display().to_string(),
            reason: "path must reference a regular file".to_string(),
        });
    }

    if !is_yaml_file(&canonical_path) {
        return Err(YamlWorkflowRunError::FileRejected {
            path: canonical_path.display().to_string(),
            reason: "workflow file extension must be .yaml or .yml".to_string(),
        });
    }

    if metadata.len() > MAX_WORKFLOW_YAML_BYTES {
        return Err(YamlWorkflowRunError::FileRejected {
            path: canonical_path.display().to_string(),
            reason: format!(
                "workflow yaml is too large ({} bytes > {} bytes)",
                metadata.len(),
                MAX_WORKFLOW_YAML_BYTES
            ),
        });
    }

    let contents =
        std::fs::read_to_string(&canonical_path).map_err(|source| YamlWorkflowRunError::Read {
            path: canonical_path.display().to_string(),
            source,
        })?;

    let yaml_value: serde_yaml::Value =
        serde_yaml::from_str(&contents).map_err(|source| YamlWorkflowRunError::Parse {
            path: canonical_path.display().to_string(),
            source,
        })?;

    let depth = yaml_value_depth(&yaml_value, 1);
    if depth > MAX_WORKFLOW_YAML_DEPTH {
        return Err(YamlWorkflowRunError::FileRejected {
            path: canonical_path.display().to_string(),
            reason: format!(
                "workflow yaml nesting depth {} exceeds limit {}",
                depth, MAX_WORKFLOW_YAML_DEPTH
            ),
        });
    }

    let workflow: YamlWorkflow =
        serde_yaml::from_value(yaml_value).map_err(|source| YamlWorkflowRunError::Parse {
            path: canonical_path.display().to_string(),
            source,
        })?;

    Ok((canonical_path, workflow))
}