Skip to main content

gate4agent_types/
capability.rs

1use crate::{OperationId, SessionGeneration, WORKING_DIRECTORY_MAX_BYTES};
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4
5pub const CAPABILITY_MODELS_MAX: usize = 512;
6pub const CAPABILITY_MODEL_ID_MAX_BYTES: usize = 512;
7pub const CAPABILITY_MODEL_LABEL_MAX_BYTES: usize = 1_024;
8
9#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
10pub struct CapabilityProbeRequest {
11    pub working_directory: String,
12}
13
14impl CapabilityProbeRequest {
15    pub fn validate(&self) -> Result<(), CapabilityValidationError> {
16        if self.working_directory.is_empty()
17            || self.working_directory.len() > WORKING_DIRECTORY_MAX_BYTES
18            || self.working_directory.contains('\0')
19        {
20            return Err(CapabilityValidationError::InvalidWorkingDirectory);
21        }
22        Ok(())
23    }
24}
25
26#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
27pub struct CapabilityModelSummary {
28    pub id: String,
29    pub label: String,
30}
31
32impl CapabilityModelSummary {
33    pub fn validate(&self) -> Result<(), CapabilityValidationError> {
34        if self.id.is_empty()
35            || self.id.len() > CAPABILITY_MODEL_ID_MAX_BYTES
36            || self.id.chars().any(char::is_whitespace)
37            || self.id.chars().any(char::is_control)
38        {
39            return Err(CapabilityValidationError::InvalidModelId);
40        }
41        if self.label.trim().is_empty()
42            || self.label.len() > CAPABILITY_MODEL_LABEL_MAX_BYTES
43            || self.label.chars().any(char::is_control)
44        {
45            return Err(CapabilityValidationError::InvalidModelLabel);
46        }
47        Ok(())
48    }
49}
50
51pub fn validate_capability_models(
52    models: &[CapabilityModelSummary],
53) -> Result<(), CapabilityValidationError> {
54    if models.len() > CAPABILITY_MODELS_MAX {
55        return Err(CapabilityValidationError::TooManyModels);
56    }
57    let mut seen = std::collections::BTreeSet::new();
58    for model in models {
59        model.validate()?;
60        if !seen.insert(model.id.as_str()) {
61            return Err(CapabilityValidationError::DuplicateModelId);
62        }
63    }
64    Ok(())
65}
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
68#[serde(tag = "kind", rename_all = "kebab-case")]
69pub enum CapabilityProbeFailure {
70    ExecutorUnavailable,
71    SpawnUnavailable,
72    TimedOut,
73    OutputLimitExceeded,
74    NonZeroExit { exit_code: Option<i32> },
75    AuthorityRejected,
76}
77
78#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
79pub struct PendingCapabilityProbe {
80    pub operation_id: OperationId,
81    pub generation: SessionGeneration,
82    pub request: CapabilityProbeRequest,
83}
84
85#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
86pub struct CapabilitySnapshot {
87    pub pending: Option<PendingCapabilityProbe>,
88    pub settled: bool,
89    pub session_option_models: Vec<CapabilityModelSummary>,
90    pub last_failure: Option<CapabilityProbeFailure>,
91}
92
93#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
94pub enum CapabilityValidationError {
95    #[error("capability probe working directory is empty, too large, or contains a NUL byte")]
96    InvalidWorkingDirectory,
97    #[error("capability model ID is empty, too large, or contains whitespace or controls")]
98    InvalidModelId,
99    #[error("capability model label is empty, too large, or contains controls")]
100    InvalidModelLabel,
101    #[error("capability model result exceeds its count bound")]
102    TooManyModels,
103    #[error("capability model result contains duplicate IDs")]
104    DuplicateModelId,
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn capability_models_are_bounded_and_unique() {
113        let valid = vec![CapabilityModelSummary {
114            id: "gpt-5.3-codex".to_owned(),
115            label: "GPT-5.3 Codex".to_owned(),
116        }];
117        assert!(validate_capability_models(&valid).is_ok());
118
119        let duplicate = vec![valid[0].clone(), valid[0].clone()];
120        assert_eq!(
121            validate_capability_models(&duplicate),
122            Err(CapabilityValidationError::DuplicateModelId)
123        );
124    }
125}