Skip to main content

agents_kit/config/
codex.rs

1use std::net::SocketAddr;
2use std::path::PathBuf;
3use std::time::Duration;
4
5use crate::KitError;
6
7/// Selects how an agent connects to a Codex app server.
8///
9/// # Examples
10///
11/// ```
12/// use std::time::Duration;
13/// use agents_kit::{
14///     config::{BackendConfig, CodexConfig},
15///     KitError,
16/// };
17///
18/// let spawned_codex = || -> Result<BackendConfig, KitError> {
19///     let codex = CodexConfig::spawn(
20///         Duration::from_secs(15),
21///         Duration::from_secs(60),
22///     )?;
23///     Ok(codex.into())
24/// };
25/// let backend = spawned_codex()?;
26/// assert!(matches!(backend, BackendConfig::Codex(_)));
27/// # Ok::<(), KitError>(())
28/// ```
29#[derive(Clone, Debug)]
30pub struct CodexConfig {
31    mode: CodexMode,
32}
33
34#[derive(Clone, Debug)]
35pub(crate) enum CodexMode {
36    Spawn {
37        startup_timeout: Duration,
38        request_timeout: Duration,
39    },
40    Tcp {
41        address: SocketAddr,
42        connect_timeout: Duration,
43        request_timeout: Duration,
44    },
45    Unix {
46        path: PathBuf,
47        connect_timeout: Duration,
48        request_timeout: Duration,
49    },
50}
51
52impl CodexConfig {
53    /// Configures a Kit-owned Codex child process.
54    ///
55    /// Configuration is passive; the child starts during Agent build.
56    ///
57    /// # Errors
58    /// Returns [`crate::error::ErrorKind::InvalidConfig`] when:
59    /// Either timeout is zero.
60    ///
61    /// ```
62    /// use agents_kit::config::CodexConfig;
63    /// use std::time::Duration;
64    /// let config = CodexConfig::spawn(Duration::from_secs(15), Duration::from_secs(60))?;
65    /// assert!(CodexConfig::spawn(Duration::ZERO, Duration::from_secs(60)).is_err());
66    /// # Ok::<(), agents_kit::KitError>(())
67    /// ```
68    pub fn spawn(startup_timeout: Duration, request_timeout: Duration) -> Result<Self, KitError> {
69        Self::validate_timeout("Codex startup timeout", startup_timeout)?;
70        Self::validate_timeout("Codex request timeout", request_timeout)?;
71        Ok(Self {
72            mode: CodexMode::Spawn {
73                startup_timeout,
74                request_timeout,
75            },
76        })
77    }
78
79    /// Configures exclusive attachment to a caller-managed TCP app server.
80    ///
81    /// Constructing this value does not open a socket.
82    ///
83    /// # Errors
84    /// Returns [`crate::error::ErrorKind::InvalidConfig`] when:
85    /// The port or either timeout is zero.
86    ///
87    /// ```
88    /// use agents_kit::config::CodexConfig;
89    /// use std::time::Duration;
90    /// let config = CodexConfig::tcp("127.0.0.1:4500".parse().unwrap(),
91    ///     Duration::from_secs(10), Duration::from_secs(60))?;
92    /// assert!(CodexConfig::tcp("127.0.0.1:0".parse().unwrap(),
93    ///     Duration::from_secs(10), Duration::from_secs(60)).is_err());
94    /// # Ok::<(), agents_kit::KitError>(())
95    /// ```
96    pub fn tcp(
97        address: SocketAddr,
98        connect_timeout: Duration,
99        request_timeout: Duration,
100    ) -> Result<Self, KitError> {
101        if address.port() == 0 {
102            return Err(KitError::invalid_config(
103                "Codex TCP address must use a nonzero port",
104            ));
105        }
106        Self::validate_timeout("Codex connect timeout", connect_timeout)?;
107        Self::validate_timeout("Codex request timeout", request_timeout)?;
108        Ok(Self {
109            mode: CodexMode::Tcp {
110                address,
111                connect_timeout,
112                request_timeout,
113            },
114        })
115    }
116
117    /// Configures exclusive attachment through a Unix-domain socket.
118    ///
119    /// The path is not opened until Agent build. Availability of Unix sockets depends on the host.
120    ///
121    /// # Errors
122    /// Returns [`crate::error::ErrorKind::InvalidConfig`] when:
123    /// The path is empty or either timeout is zero.
124    ///
125    /// ```
126    /// use agents_kit::config::CodexConfig;
127    /// use std::time::Duration;
128    /// let config = CodexConfig::unix("codex.sock".into(), Duration::from_secs(10), Duration::from_secs(60))?;
129    /// assert!(CodexConfig::unix("".into(), Duration::from_secs(10), Duration::from_secs(60)).is_err());
130    /// # Ok::<(), agents_kit::KitError>(())
131    /// ```
132    pub fn unix(
133        path: PathBuf,
134        connect_timeout: Duration,
135        request_timeout: Duration,
136    ) -> Result<Self, KitError> {
137        if path.as_os_str().is_empty() {
138            return Err(KitError::invalid_config(
139                "Codex Unix socket path must not be empty",
140            ));
141        }
142        Self::validate_timeout("Codex connect timeout", connect_timeout)?;
143        Self::validate_timeout("Codex request timeout", request_timeout)?;
144        Ok(Self {
145            mode: CodexMode::Unix {
146                path,
147                connect_timeout,
148                request_timeout,
149            },
150        })
151    }
152
153    pub(crate) fn mode(&self) -> &CodexMode {
154        &self.mode
155    }
156
157    pub(crate) fn validate(&self) -> Result<(), KitError> {
158        match &self.mode {
159            CodexMode::Spawn {
160                startup_timeout,
161                request_timeout,
162            } => {
163                Self::validate_timeout("Codex startup timeout", *startup_timeout)?;
164                Self::validate_timeout("Codex request timeout", *request_timeout)
165            }
166            CodexMode::Tcp {
167                address,
168                connect_timeout,
169                request_timeout,
170            } => {
171                if address.port() == 0 {
172                    return Err(KitError::invalid_config(
173                        "Codex TCP address must use a nonzero port",
174                    ));
175                }
176                Self::validate_timeout("Codex connect timeout", *connect_timeout)?;
177                Self::validate_timeout("Codex request timeout", *request_timeout)
178            }
179            CodexMode::Unix {
180                path,
181                connect_timeout,
182                request_timeout,
183            } => {
184                if path.as_os_str().is_empty() {
185                    return Err(KitError::invalid_config(
186                        "Codex Unix socket path must not be empty",
187                    ));
188                }
189                Self::validate_timeout("Codex connect timeout", *connect_timeout)?;
190                Self::validate_timeout("Codex request timeout", *request_timeout)
191            }
192        }
193    }
194
195    fn validate_timeout(label: &str, timeout: Duration) -> Result<(), KitError> {
196        if timeout.is_zero() {
197            return Err(KitError::invalid_config(format!(
198                "{label} must be greater than zero"
199            )));
200        }
201        Ok(())
202    }
203}
204
205impl CodexMode {
206    pub(crate) fn request_timeout(&self) -> Duration {
207        match self {
208            Self::Spawn {
209                request_timeout, ..
210            }
211            | Self::Tcp {
212                request_timeout, ..
213            }
214            | Self::Unix {
215                request_timeout, ..
216            } => *request_timeout,
217        }
218    }
219}