Skip to main content

gate4agent_node/
spawn_spec.rs

1use crate::protocol::{
2    AgentId, SessionMode, SpawnProfileDefaults, SpawnProfileId, SpawnProfileRevision,
3};
4use gate4agent_types::TerminalSize;
5use std::collections::BTreeMap;
6use thiserror::Error;
7
8pub const DEFAULT_SPAWN_PROFILE_ID: &str = "default";
9pub use crate::protocol::MAX_SPAWN_PROFILES;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct SpawnProfileRegistry {
13    profiles: BTreeMap<SpawnProfileId, SpawnProfileDefaults>,
14}
15
16impl SpawnProfileRegistry {
17    pub fn new(
18        profiles: impl IntoIterator<Item = SpawnProfileDefaults>,
19    ) -> Result<Self, SpawnProfileRegistryError> {
20        let mut bounded = BTreeMap::new();
21        for profile in profiles {
22            if bounded.len() == MAX_SPAWN_PROFILES {
23                return Err(SpawnProfileRegistryError::TooMany {
24                    max: MAX_SPAWN_PROFILES,
25                });
26            }
27            if !profile.terminal_size.is_valid() {
28                return Err(SpawnProfileRegistryError::InvalidTerminalSize {
29                    profile_id: profile.profile_id,
30                });
31            }
32            let profile_id = profile.profile_id.clone();
33            if bounded.insert(profile_id.clone(), profile).is_some() {
34                return Err(SpawnProfileRegistryError::Duplicate { profile_id });
35            }
36        }
37        if bounded.is_empty() {
38            return Err(SpawnProfileRegistryError::Empty);
39        }
40        Ok(Self { profiles: bounded })
41    }
42
43    pub fn get(&self, profile_id: &SpawnProfileId) -> Option<&SpawnProfileDefaults> {
44        self.profiles.get(profile_id)
45    }
46
47    pub fn iter(&self) -> impl ExactSizeIterator<Item = &SpawnProfileDefaults> {
48        self.profiles.values()
49    }
50}
51
52impl Default for SpawnProfileRegistry {
53    fn default() -> Self {
54        Self::new([SpawnProfileDefaults {
55            profile_id: SpawnProfileId::new(DEFAULT_SPAWN_PROFILE_ID)
56                .expect("the built-in spawn profile ID is valid"),
57            revision: SpawnProfileRevision::new("builtin-v1")
58                .expect("the built-in spawn profile revision is valid"),
59            provider: AgentId::new("claude")
60                .expect("the built-in spawn profile provider is valid"),
61            mode: SessionMode::Pty,
62            terminal_size: TerminalSize {
63                rows: 24,
64                columns: 80,
65            },
66            prompt: None,
67            bundle_id: None,
68            context_id: None,
69            environment_profile_id: None,
70        }])
71        .expect("the built-in spawn profile registry is valid")
72    }
73}
74
75#[derive(Clone, Debug, Eq, Error, PartialEq)]
76pub enum SpawnProfileRegistryError {
77    #[error("spawn profile registry cannot be empty")]
78    Empty,
79    #[error("spawn profile registry exceeds the {max}-profile limit")]
80    TooMany { max: usize },
81    #[error("spawn profile registry contains duplicate profile {profile_id}")]
82    Duplicate { profile_id: SpawnProfileId },
83    #[error("spawn profile {profile_id} has an invalid terminal size")]
84    InvalidTerminalSize { profile_id: SpawnProfileId },
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn default_spawn_profile_is_bounded_and_safe() {
93        let registry = SpawnProfileRegistry::default();
94        let profile_id = SpawnProfileId::new(DEFAULT_SPAWN_PROFILE_ID).unwrap();
95        let profile = registry.get(&profile_id).unwrap();
96
97        assert_eq!(profile.provider.as_str(), "claude");
98        assert_eq!(profile.mode, SessionMode::Pty);
99        assert!(profile.terminal_size.is_valid());
100        assert!(profile.prompt.is_none());
101        assert!(profile.bundle_id.is_none());
102        assert!(profile.context_id.is_none());
103        assert!(profile.environment_profile_id.is_none());
104    }
105}