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}