1use std::{
17 collections::VecDeque,
18 path::{Path, PathBuf},
19 time::Duration,
20};
21
22use anyhow::{Context, bail};
23use base64::Engine;
24use serde_json::{Value, json};
25use tokio::{
26 io::{AsyncReadExt, AsyncWriteExt},
27 net::UnixStream,
28};
29
30const MAX_FRAME_BYTES: u64 = 64 * 1024 * 1024;
34const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
36const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
39
40pub fn default_socket() -> anyhow::Result<PathBuf> {
44 let home = match std::env::var_os("CODEX_HOME") {
45 Some(h) if !h.is_empty() => PathBuf::from(h),
46 _ => crate::admin_cli::home_dir()
47 .context("neither CODEX_HOME nor HOME is set")?
48 .join(".codex"),
49 };
50 Ok(home
51 .join("app-server-control")
52 .join("app-server-control.sock"))
53}
54
55pub struct AppServer {
57 stream: UnixStream,
58 buf: Vec<u8>,
60 partial: Vec<u8>,
64 next_id: i64,
65 pending: VecDeque<Value>,
67}
68
69#[derive(Debug, thiserror::Error)]
72#[error("{method}: {message}")]
73pub struct RpcError {
74 pub method: String,
75 pub code: i64,
76 pub message: String,
77}
78
79impl AppServer {
80 pub async fn connect(socket: &Path, client_name: &str) -> anyhow::Result<Self> {
83 tokio::time::timeout(CONNECT_TIMEOUT, Self::open(socket, client_name))
84 .await
85 .with_context(|| {
86 format!(
87 "the Codex app-server at {} did not answer within {}s",
88 socket.display(),
89 CONNECT_TIMEOUT.as_secs()
90 )
91 })?
92 }
93
94 async fn open(socket: &Path, client_name: &str) -> anyhow::Result<Self> {
95 let stream = UnixStream::connect(socket).await.with_context(|| {
96 format!(
97 "could not reach the Codex app-server at {}; is it running? \
98 (`codex app-server daemon start`)",
99 socket.display()
100 )
101 })?;
102 let mut client = Self {
103 stream,
104 buf: Vec::new(),
105 partial: Vec::new(),
106 next_id: 1,
107 pending: VecDeque::new(),
108 };
109 client.handshake().await?;
110 client
111 .request(
112 "initialize",
113 json!({"clientInfo": {"name": client_name, "version": env!("CARGO_PKG_VERSION")}}),
114 )
115 .await?;
116 client
117 .send_json(&json!({"jsonrpc": "2.0", "method": "initialized"}))
118 .await?;
119 Ok(client)
120 }
121
122 async fn handshake(&mut self) -> anyhow::Result<()> {
123 let key = base64::engine::general_purpose::STANDARD.encode(rand::random::<[u8; 16]>());
124 let request = format!(
125 "GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\n\
126 Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n\
127 Sec-WebSocket-Version: 13\r\n\r\n"
128 );
129 self.stream.write_all(request.as_bytes()).await?;
130 let mut head = Vec::new();
131 let mut byte = [0u8; 1];
132 while !head.ends_with(b"\r\n\r\n") {
135 if head.len() > 16 * 1024 {
136 bail!("the Codex app-server sent an oversized handshake response");
137 }
138 let n = self.stream.read(&mut byte).await?;
139 if n == 0 {
140 bail!("the Codex app-server closed the connection during the handshake");
141 }
142 head.push(byte[0]);
143 }
144 let status = String::from_utf8_lossy(&head);
145 if !status.starts_with("HTTP/1.1 101") {
146 bail!(
147 "the Codex app-server refused the WebSocket upgrade: {}",
148 status.lines().next().unwrap_or("")
149 );
150 }
151 Ok(())
152 }
153
154 async fn send_json(&mut self, value: &Value) -> anyhow::Result<()> {
156 let payload = serde_json::to_vec(value)?;
157 self.send_frame(0x1, &payload).await
158 }
159
160 async fn send_frame(&mut self, opcode: u8, payload: &[u8]) -> anyhow::Result<()> {
161 let mut frame = Vec::with_capacity(payload.len() + 14);
162 frame.push(0x80 | opcode);
163 let len = payload.len();
164 if len < 126 {
165 frame.push(0x80 | len as u8);
166 } else if len <= u16::MAX as usize {
167 frame.push(0x80 | 126);
168 frame.extend_from_slice(&(len as u16).to_be_bytes());
169 } else {
170 frame.push(0x80 | 127);
171 frame.extend_from_slice(&(len as u64).to_be_bytes());
172 }
173 let mask: [u8; 4] = rand::random();
174 frame.extend_from_slice(&mask);
175 frame.extend(payload.iter().enumerate().map(|(i, b)| b ^ mask[i % 4]));
176 self.stream.write_all(&frame).await?;
177 Ok(())
178 }
179
180 async fn fill(&mut self, want: usize) -> anyhow::Result<()> {
181 let mut chunk = [0u8; 16 * 1024];
182 while self.buf.len() < want {
183 let n = self.stream.read(&mut chunk).await?;
184 if n == 0 {
185 bail!("the Codex app-server closed the connection");
186 }
187 self.buf.extend_from_slice(&chunk[..n]);
188 }
189 Ok(())
190 }
191
192 async fn read_message(&mut self) -> anyhow::Result<Value> {
196 loop {
197 self.fill(2).await?;
198 let fin = self.buf[0] & 0x80 != 0;
199 let opcode = self.buf[0] & 0x0f;
200 let masked = self.buf[1] & 0x80 != 0;
201 let mut len = u64::from(self.buf[1] & 0x7f);
202 let mut offset = 2usize;
203 if len == 126 {
204 self.fill(4).await?;
205 len = u64::from(u16::from_be_bytes([self.buf[2], self.buf[3]]));
206 offset = 4;
207 } else if len == 127 {
208 self.fill(10).await?;
209 let mut b = [0u8; 8];
210 b.copy_from_slice(&self.buf[2..10]);
211 len = u64::from_be_bytes(b);
212 offset = 10;
213 }
214 if len > MAX_FRAME_BYTES {
215 bail!("the Codex app-server sent a {len}-byte frame; refusing it");
216 }
217 let mask_len = if masked { 4 } else { 0 };
218 let total = offset + mask_len + len as usize;
219 self.fill(total).await?;
220 let mut payload = self.buf[offset + mask_len..total].to_vec();
221 if masked {
222 let mask = [
223 self.buf[offset],
224 self.buf[offset + 1],
225 self.buf[offset + 2],
226 self.buf[offset + 3],
227 ];
228 for (i, b) in payload.iter_mut().enumerate() {
229 *b ^= mask[i % 4];
230 }
231 }
232 self.buf.drain(..total);
233 match opcode {
234 0x0 | 0x1 => {
235 if self.partial.len() as u64 + payload.len() as u64 > MAX_FRAME_BYTES {
236 bail!("the Codex app-server sent an oversized message; refusing it");
237 }
238 self.partial.extend_from_slice(&payload);
239 if fin {
240 let message = std::mem::take(&mut self.partial);
241 return serde_json::from_slice(&message)
242 .context("the Codex app-server sent a message that is not JSON");
243 }
244 }
245 0x8 => bail!("the Codex app-server closed the connection"),
246 0x9 => self.send_frame(0xA, &payload).await?,
247 _ => {}
249 }
250 }
251 }
252
253 pub async fn request(&mut self, method: &str, params: Value) -> anyhow::Result<Value> {
258 let id = self.next_id;
259 self.next_id += 1;
260 self.send_json(&json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params}))
261 .await?;
262 let deadline = tokio::time::Instant::now() + REQUEST_TIMEOUT;
263 loop {
264 let message = tokio::time::timeout_at(deadline, self.read_message())
265 .await
266 .with_context(|| format!("{method}: no answer from the Codex app-server"))??;
267 if message.get("method").is_some() {
268 self.route_incoming(message).await?;
269 continue;
270 }
271 if message.get("id").and_then(Value::as_i64) != Some(id) {
272 continue;
273 }
274 if let Some(error) = message.get("error") {
275 return Err(RpcError {
276 method: method.to_owned(),
277 code: error.get("code").and_then(Value::as_i64).unwrap_or(0),
278 message: error
279 .get("message")
280 .and_then(Value::as_str)
281 .unwrap_or("error")
282 .to_owned(),
283 }
284 .into());
285 }
286 return Ok(message.get("result").cloned().unwrap_or(Value::Null));
287 }
288 }
289
290 async fn route_incoming(&mut self, message: Value) -> anyhow::Result<()> {
292 match message.get("id") {
293 Some(id) => {
294 let id = id.clone();
295 self.send_json(&json!({
296 "jsonrpc": "2.0",
297 "id": id,
298 "error": {"code": -32601, "message": "the ai-crew-sync wake adapter approves nothing"},
299 }))
300 .await
301 }
302 None => {
303 self.pending.push_back(message);
304 Ok(())
305 }
306 }
307 }
308
309 pub async fn next_notification(&mut self, timeout: Duration) -> anyhow::Result<Option<Value>> {
311 if let Some(n) = self.pending.pop_front() {
312 return Ok(Some(n));
313 }
314 let deadline = tokio::time::Instant::now() + timeout;
315 loop {
316 let message = match tokio::time::timeout_at(deadline, self.read_message()).await {
317 Ok(m) => m?,
318 Err(_) => return Ok(None),
319 };
320 if message.get("method").is_none() {
321 continue;
322 }
323 if message.get("id").is_some() {
324 self.route_incoming(message).await?;
325 continue;
326 }
327 return Ok(Some(message));
328 }
329 }
330}
331
332pub fn thread_status(thread_or_notification: &Value) -> Option<&str> {
335 thread_or_notification
336 .get("status")
337 .and_then(|s| s.get("type"))
338 .and_then(Value::as_str)
339}