gate4agent_node/
spawn_spec.rs1use 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}