gate4agent-types 0.3.0

Pure data contracts for gate4agent backends and shells
Documentation
use crate::{OperationId, SessionGeneration, WORKING_DIRECTORY_MAX_BYTES};
use serde::{Deserialize, Serialize};
use thiserror::Error;

pub const CAPABILITY_MODELS_MAX: usize = 512;
pub const CAPABILITY_MODEL_ID_MAX_BYTES: usize = 512;
pub const CAPABILITY_MODEL_LABEL_MAX_BYTES: usize = 1_024;

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CapabilityProbeRequest {
    pub working_directory: String,
}

impl CapabilityProbeRequest {
    pub fn validate(&self) -> Result<(), CapabilityValidationError> {
        if self.working_directory.is_empty()
            || self.working_directory.len() > WORKING_DIRECTORY_MAX_BYTES
            || self.working_directory.contains('\0')
        {
            return Err(CapabilityValidationError::InvalidWorkingDirectory);
        }
        Ok(())
    }
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CapabilityModelSummary {
    pub id: String,
    pub label: String,
}

impl CapabilityModelSummary {
    pub fn validate(&self) -> Result<(), CapabilityValidationError> {
        if self.id.is_empty()
            || self.id.len() > CAPABILITY_MODEL_ID_MAX_BYTES
            || self.id.chars().any(char::is_whitespace)
            || self.id.chars().any(char::is_control)
        {
            return Err(CapabilityValidationError::InvalidModelId);
        }
        if self.label.trim().is_empty()
            || self.label.len() > CAPABILITY_MODEL_LABEL_MAX_BYTES
            || self.label.chars().any(char::is_control)
        {
            return Err(CapabilityValidationError::InvalidModelLabel);
        }
        Ok(())
    }
}

pub fn validate_capability_models(
    models: &[CapabilityModelSummary],
) -> Result<(), CapabilityValidationError> {
    if models.len() > CAPABILITY_MODELS_MAX {
        return Err(CapabilityValidationError::TooManyModels);
    }
    let mut seen = std::collections::BTreeSet::new();
    for model in models {
        model.validate()?;
        if !seen.insert(model.id.as_str()) {
            return Err(CapabilityValidationError::DuplicateModelId);
        }
    }
    Ok(())
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum CapabilityProbeFailure {
    ExecutorUnavailable,
    SpawnUnavailable,
    TimedOut,
    OutputLimitExceeded,
    NonZeroExit { exit_code: Option<i32> },
    AuthorityRejected,
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PendingCapabilityProbe {
    pub operation_id: OperationId,
    pub generation: SessionGeneration,
    pub request: CapabilityProbeRequest,
}

#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct CapabilitySnapshot {
    pub pending: Option<PendingCapabilityProbe>,
    pub settled: bool,
    pub session_option_models: Vec<CapabilityModelSummary>,
    pub last_failure: Option<CapabilityProbeFailure>,
}

#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
pub enum CapabilityValidationError {
    #[error("capability probe working directory is empty, too large, or contains a NUL byte")]
    InvalidWorkingDirectory,
    #[error("capability model ID is empty, too large, or contains whitespace or controls")]
    InvalidModelId,
    #[error("capability model label is empty, too large, or contains controls")]
    InvalidModelLabel,
    #[error("capability model result exceeds its count bound")]
    TooManyModels,
    #[error("capability model result contains duplicate IDs")]
    DuplicateModelId,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn capability_models_are_bounded_and_unique() {
        let valid = vec![CapabilityModelSummary {
            id: "gpt-5.3-codex".to_owned(),
            label: "GPT-5.3 Codex".to_owned(),
        }];
        assert!(validate_capability_models(&valid).is_ok());

        let duplicate = vec![valid[0].clone(), valid[0].clone()];
        assert_eq!(
            validate_capability_models(&duplicate),
            Err(CapabilityValidationError::DuplicateModelId)
        );
    }
}