zynk 1.1.0

Portable protocol and helper CLI for multi-agent collaboration.
use crate::{CliError, CliResult};
use serde::Deserialize;
use serde_norway::Value;
use std::collections::HashMap;
use std::fs;
use std::path::Path;

const DEFAULT_PROFILE: &str = include_str!("../tools/message-profile.yaml");

#[derive(Debug, Deserialize, Default)]
pub struct Profile {
    #[serde(default)]
    pub transport: TransportProfile,
    #[serde(default)]
    pub message: MessageProfile,
    #[serde(default)]
    pub message_types: MessageTypesProfile,
    #[serde(default)]
    pub operator_interface: OperatorInterfaceProfile,
    #[serde(default)]
    pub audit: AuditProfile,
    #[serde(default)]
    pub shorthand: HashMap<String, ShorthandProfile>,
}

#[derive(Debug, Deserialize)]
pub struct TransportProfile {
    #[serde(default = "default_transport")]
    pub default: String,
}

impl Default for TransportProfile {
    fn default() -> Self {
        Self {
            default: default_transport(),
        }
    }
}

#[derive(Debug, Deserialize)]
pub struct MessageProfile {
    #[serde(default = "default_human_prefix_template")]
    pub human_prefix_template: String,
    #[serde(default = "default_structured_header_opener")]
    pub structured_header_opener: String,
    #[serde(default = "default_required_header_fields")]
    pub required_header_fields: Vec<String>,
}

impl Default for MessageProfile {
    fn default() -> Self {
        Self {
            human_prefix_template: default_human_prefix_template(),
            structured_header_opener: default_structured_header_opener(),
            required_header_fields: default_required_header_fields(),
        }
    }
}

#[derive(Debug, Deserialize, Default)]
pub struct MessageTypesProfile {
    #[serde(default)]
    pub per_type_required_fields: HashMap<String, Value>,
}

#[derive(Debug, Deserialize)]
pub struct OperatorInterfaceProfile {
    #[serde(default = "default_workflow_status_enum")]
    pub workflow_status_enum: Vec<String>,
}

impl Default for OperatorInterfaceProfile {
    fn default() -> Self {
        Self {
            workflow_status_enum: default_workflow_status_enum(),
        }
    }
}

#[derive(Debug, Deserialize)]
pub struct AuditProfile {
    #[serde(default = "default_redaction_policy_enum")]
    pub redaction_policy_enum: Vec<String>,
    #[serde(default = "default_delivery_status_enum")]
    pub delivery_status_enum: Vec<String>,
    #[serde(default = "default_force_hash_only_categories")]
    pub force_hash_only_categories: Vec<String>,
}

impl Default for AuditProfile {
    fn default() -> Self {
        Self {
            redaction_policy_enum: default_redaction_policy_enum(),
            delivery_status_enum: default_delivery_status_enum(),
            force_hash_only_categories: default_force_hash_only_categories(),
        }
    }
}

#[derive(Debug, Deserialize)]
pub struct ShorthandProfile {
    #[serde(default)]
    pub r#type: Option<String>,
    #[serde(default)]
    pub mode: Option<String>,
    pub body_template: String,
}

fn default_transport() -> String {
    "herdr".to_string()
}

fn default_human_prefix_template() -> String {
    "[from-{agent_id} via {transport}]".to_string()
}

fn default_structured_header_opener() -> String {
    "herdr".to_string()
}

fn default_required_header_fields() -> Vec<String> {
    ["from", "to", "mid", "type"]
        .into_iter()
        .map(str::to_string)
        .collect()
}

fn default_workflow_status_enum() -> Vec<String> {
    ["idle", "working", "blocked", "waiting-for-operator", "done"]
        .into_iter()
        .map(str::to_string)
        .collect()
}

fn default_redaction_policy_enum() -> Vec<String> {
    ["hash-only", "excerpt", "full"]
        .into_iter()
        .map(str::to_string)
        .collect()
}

fn default_delivery_status_enum() -> Vec<String> {
    ["drafted", "sent", "observed", "failed", "unknown"]
        .into_iter()
        .map(str::to_string)
        .collect()
}

fn default_force_hash_only_categories() -> Vec<String> {
    [
        "secrets",
        "keys",
        "tokens",
        "private_credentials",
        "sensitive_operator_text",
    ]
    .into_iter()
    .map(str::to_string)
    .collect()
}

pub fn load_profile(path: Option<&Path>) -> CliResult<Profile> {
    let content = match path {
        Some(path) => fs::read_to_string(path).map_err(|error| {
            CliError::failure(format!(
                "failed to read profile {}: {error}",
                path.display()
            ))
        })?,
        None => DEFAULT_PROFILE.to_string(),
    };
    serde_norway::from_str(&content)
        .map_err(|error| CliError::failure(format!("profile must be valid YAML: {error}")))
}