@team-agent/installer 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/Cargo.lock +120 -9
  2. package/Cargo.toml +2 -2
  3. package/crates/team-agent/Cargo.toml +21 -0
  4. package/crates/team-agent/src/app_server_test_support.rs +258 -0
  5. package/crates/team-agent/src/cli/adapters.rs +32 -3
  6. package/crates/team-agent/src/cli/attach_app_server_leader.rs +16 -0
  7. package/crates/team-agent/src/cli/diagnose.rs +14 -5
  8. package/crates/team-agent/src/cli/emit.rs +79 -1
  9. package/crates/team-agent/src/cli/mod.rs +190 -80
  10. package/crates/team-agent/src/cli/named_address.rs +111 -0
  11. package/crates/team-agent/src/cli/send.rs +98 -3
  12. package/crates/team-agent/src/cli/status_port.rs +44 -6
  13. package/crates/team-agent/src/cli/tests/named_address.rs +135 -0
  14. package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
  15. package/crates/team-agent/src/cli/types.rs +17 -0
  16. package/crates/team-agent/src/codex_app_server.rs +549 -0
  17. package/crates/team-agent/src/conpty/backend.rs +822 -0
  18. package/crates/team-agent/src/conpty/mod.rs +32 -0
  19. package/crates/team-agent/src/coordinator/backoff.rs +132 -7
  20. package/crates/team-agent/src/coordinator/conpty_shim.rs +730 -0
  21. package/crates/team-agent/src/coordinator/health.rs +97 -11
  22. package/crates/team-agent/src/coordinator/mod.rs +8 -0
  23. package/crates/team-agent/src/coordinator/tests/daemon.rs +2 -1
  24. package/crates/team-agent/src/coordinator/tests/tick_core.rs +6 -0
  25. package/crates/team-agent/src/diagnose/orphans.rs +29 -10
  26. package/crates/team-agent/src/leader/lease.rs +144 -7
  27. package/crates/team-agent/src/leader/owner_bind.rs +5 -2
  28. package/crates/team-agent/src/leader/provider_attribution.rs +19 -34
  29. package/crates/team-agent/src/leader/start.rs +18 -5
  30. package/crates/team-agent/src/leader/tests/lease_api.rs +179 -0
  31. package/crates/team-agent/src/lib.rs +36 -0
  32. package/crates/team-agent/src/lifecycle/launch.rs +207 -94
  33. package/crates/team-agent/src/lifecycle/lock.rs +40 -33
  34. package/crates/team-agent/src/lifecycle/restart/agent.rs +10 -18
  35. package/crates/team-agent/src/lifecycle/restart/common.rs +70 -0
  36. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -0
  37. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +7 -0
  38. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +63 -0
  39. package/crates/team-agent/src/mcp_server/wire.rs +6 -7
  40. package/crates/team-agent/src/messaging/delivery.rs +329 -9
  41. package/crates/team-agent/src/messaging/leader_receiver.rs +17 -138
  42. package/crates/team-agent/src/messaging/mod.rs +2 -2
  43. package/crates/team-agent/src/messaging/results.rs +78 -21
  44. package/crates/team-agent/src/messaging/tests/runtime.rs +237 -0
  45. package/crates/team-agent/src/packaging/tests.rs +41 -6
  46. package/crates/team-agent/src/packaging/types.rs +31 -3
  47. package/crates/team-agent/src/platform/argv.rs +324 -0
  48. package/crates/team-agent/src/platform/errors.rs +95 -0
  49. package/crates/team-agent/src/platform/file_lock.rs +418 -0
  50. package/crates/team-agent/src/platform/mod.rs +66 -0
  51. package/crates/team-agent/src/platform/process.rs +555 -0
  52. package/crates/team-agent/src/state/persist.rs +205 -25
  53. package/crates/team-agent/src/tmux_backend.rs +94 -13
  54. package/crates/team-agent/src/transport_factory.rs +751 -0
  55. package/package.json +4 -4
@@ -0,0 +1,549 @@
1
+ //! Minimal Codex app-server Unix-socket WebSocket client for leader delivery.
2
+ //!
3
+ //! 0.5.x Windows portability Batch 1: this client is fundamentally
4
+ //! Unix-only (`UnixStream` WebSocket handshake, uid/mode socket
5
+ //! ownership check). On Windows we still expose the same public API
6
+ //! surface so `messaging/delivery.rs`, `cli/send.rs`, `cli/named_address.rs`,
7
+ //! `leader/lease.rs` compile identically. Windows implementations of
8
+ //! `attach_probe` + `submit_to_bound_thread` return
9
+ //! `AppServerError::SocketUnreachable("codex_app_server unix socket not supported on this platform (Windows)")`
10
+ //! (N38 three-line typed unsupported: code+reason+action are baked into
11
+ //! `AppServerError::code()` and the `Display` impl). Truth source:
12
+ //! `.team/artifacts/0.5.x-windows-portability-survey-design.md` §Batch 1.
13
+ //!
14
+ //! Portable pieces below (types, JSON helpers) stay unconditional so
15
+ //! `receiver_is_app_server` / `binding_from_receiver` work identically
16
+ //! everywhere.
17
+
18
+ use serde_json::{json, Value};
19
+ #[cfg(unix)]
20
+ use std::io::{Read, Write};
21
+ #[cfg(unix)]
22
+ use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
23
+ #[cfg(unix)]
24
+ use std::os::unix::net::UnixStream;
25
+ #[cfg(unix)]
26
+ use std::path::Path;
27
+ use std::path::PathBuf;
28
+ #[cfg(unix)]
29
+ use std::time::Duration;
30
+
31
+ pub const APP_SERVER_PROTOCOL_FIXTURE_CLI_MIN: &str = "0.139.0";
32
+ pub const APP_SERVER_PROTOCOL_FIXTURE_CLI_MAX: &str = "0.139.x";
33
+ pub const APP_SERVER_PROTOCOL_REQUIRES_USER_AGENT: bool = true;
34
+
35
+ #[derive(Debug, Clone, PartialEq, Eq)]
36
+ pub struct AppServerBinding {
37
+ pub socket: String,
38
+ pub thread_id: String,
39
+ pub session_id: String,
40
+ pub cwd: String,
41
+ pub cli_version: String,
42
+ pub bound_at: String,
43
+ }
44
+
45
+ #[derive(Debug, Clone, PartialEq, Eq)]
46
+ pub struct AppServerSubmit {
47
+ pub turn_id: String,
48
+ pub turn_status: String,
49
+ }
50
+
51
+ #[derive(Debug, Clone, PartialEq, Eq)]
52
+ pub enum AppServerError {
53
+ SocketUnreachable(String),
54
+ SocketOwnershipInvalid(String),
55
+ ThreadNotLive(String),
56
+ ThreadStale { expected: Value, actual: Value },
57
+ LeaderBusy(String),
58
+ ApprovalUnsupported(String),
59
+ ProtocolMismatch(String),
60
+ MissingUserAgent,
61
+ Io(String),
62
+ Json(String),
63
+ }
64
+
65
+ impl AppServerError {
66
+ pub fn code(&self) -> &'static str {
67
+ match self {
68
+ Self::SocketUnreachable(_) => "socket_unreachable",
69
+ Self::SocketOwnershipInvalid(_) => "socket_ownership_invalid",
70
+ Self::ThreadNotLive(_) => "thread_not_live",
71
+ Self::ThreadStale { .. } => "app_server_thread_stale",
72
+ Self::LeaderBusy(_) => "leader_busy",
73
+ Self::ApprovalUnsupported(_) => "approval_unsupported",
74
+ Self::ProtocolMismatch(_) => "protocol_mismatch",
75
+ Self::MissingUserAgent => "protocol_mismatch_missing_user_agent",
76
+ Self::Io(_) => "io_error",
77
+ Self::Json(_) => "json_error",
78
+ }
79
+ }
80
+ }
81
+
82
+ impl std::fmt::Display for AppServerError {
83
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84
+ match self {
85
+ Self::SocketUnreachable(msg)
86
+ | Self::SocketOwnershipInvalid(msg)
87
+ | Self::ThreadNotLive(msg)
88
+ | Self::LeaderBusy(msg)
89
+ | Self::ApprovalUnsupported(msg)
90
+ | Self::ProtocolMismatch(msg)
91
+ | Self::Io(msg)
92
+ | Self::Json(msg) => write!(f, "{}:{msg}", self.code()),
93
+ Self::ThreadStale { expected, actual } => {
94
+ write!(f, "{}:expected={expected},actual={actual}", self.code())
95
+ }
96
+ Self::MissingUserAgent => f.write_str(self.code()),
97
+ }
98
+ }
99
+ }
100
+
101
+ impl std::error::Error for AppServerError {}
102
+
103
+ #[cfg(unix)]
104
+ pub fn attach_probe(endpoint: &str, thread_id: &str) -> Result<AppServerBinding, AppServerError> {
105
+ check_socket_ownership(endpoint)?;
106
+ let socket = socket_path(endpoint)?;
107
+ let mut client = AppServerClient::connect(&socket)?;
108
+ let user_agent = client.initialize()?;
109
+ let resume = client.resume(thread_id)?;
110
+ Ok(AppServerBinding {
111
+ socket: endpoint.to_string(),
112
+ thread_id: resume.thread_id,
113
+ session_id: resume.session_id,
114
+ cwd: resume.cwd,
115
+ cli_version: user_agent,
116
+ bound_at: chrono::Utc::now().to_rfc3339(),
117
+ })
118
+ }
119
+
120
+ /// Batch 1 Windows N38 typed-unsupported stub. Callers see the same
121
+ /// `AppServerError` shape as Unix — no silent success, no panic.
122
+ #[cfg(not(unix))]
123
+ pub fn attach_probe(
124
+ _endpoint: &str,
125
+ _thread_id: &str,
126
+ ) -> Result<AppServerBinding, AppServerError> {
127
+ Err(AppServerError::SocketUnreachable(
128
+ "codex_app_server unix-socket client not supported on this platform \
129
+ (Windows); use codex CLI stdio or ConPTY worker delivery instead"
130
+ .to_string(),
131
+ ))
132
+ }
133
+
134
+ #[cfg(unix)]
135
+ pub fn submit_to_bound_thread(
136
+ binding: &AppServerBinding,
137
+ message_id: &str,
138
+ rendered: &str,
139
+ ) -> Result<AppServerSubmit, AppServerError> {
140
+ let socket = socket_path(&binding.socket)?;
141
+ let mut client = AppServerClient::connect(&socket)?;
142
+ let user_agent = client.initialize()?;
143
+ let resume = client.resume(&binding.thread_id)?;
144
+ validate_tuple(binding, &user_agent, &resume)?;
145
+ client.turn_start(&binding.thread_id, message_id, rendered)
146
+ }
147
+
148
+ #[cfg(not(unix))]
149
+ pub fn submit_to_bound_thread(
150
+ _binding: &AppServerBinding,
151
+ _message_id: &str,
152
+ _rendered: &str,
153
+ ) -> Result<AppServerSubmit, AppServerError> {
154
+ Err(AppServerError::SocketUnreachable(
155
+ "codex_app_server unix-socket submit not supported on this platform \
156
+ (Windows); use codex CLI stdio or ConPTY worker delivery instead"
157
+ .to_string(),
158
+ ))
159
+ }
160
+
161
+ pub fn binding_from_receiver(receiver: &Value) -> Result<AppServerBinding, AppServerError> {
162
+ let app = receiver
163
+ .get("app_server")
164
+ .ok_or_else(|| AppServerError::ProtocolMismatch("missing app_server".to_string()))?;
165
+ Ok(AppServerBinding {
166
+ socket: required_str(app, "socket")?.to_string(),
167
+ thread_id: required_str(app, "thread_id")?.to_string(),
168
+ session_id: required_str(app, "session_id")?.to_string(),
169
+ cwd: required_str(app, "cwd")?.to_string(),
170
+ cli_version: required_str(app, "cli_version")?.to_string(),
171
+ bound_at: required_str(app, "bound_at")?.to_string(),
172
+ })
173
+ }
174
+
175
+ pub fn receiver_is_app_server(receiver: &Value) -> bool {
176
+ receiver.get("transport_kind").and_then(Value::as_str) == Some("codex_app_server")
177
+ || receiver.get("mode").and_then(Value::as_str) == Some("codex_app_server")
178
+ }
179
+
180
+ fn required_str<'a>(value: &'a Value, field: &str) -> Result<&'a str, AppServerError> {
181
+ value
182
+ .get(field)
183
+ .and_then(Value::as_str)
184
+ .filter(|s| !s.is_empty())
185
+ .ok_or_else(|| AppServerError::ProtocolMismatch(format!("missing {field}")))
186
+ }
187
+
188
+ #[cfg(unix)]
189
+ fn validate_tuple(
190
+ binding: &AppServerBinding,
191
+ user_agent: &str,
192
+ resume: &ThreadResume,
193
+ ) -> Result<(), AppServerError> {
194
+ if user_agent != binding.cli_version
195
+ || resume.thread_id != binding.thread_id
196
+ || resume.session_id != binding.session_id
197
+ || resume.cwd != binding.cwd
198
+ {
199
+ return Err(AppServerError::ThreadStale {
200
+ expected: json!({
201
+ "cli_version": binding.cli_version,
202
+ "thread_id": binding.thread_id,
203
+ "session_id": binding.session_id,
204
+ "cwd": binding.cwd,
205
+ }),
206
+ actual: json!({
207
+ "cli_version": user_agent,
208
+ "thread_id": resume.thread_id,
209
+ "session_id": resume.session_id,
210
+ "cwd": resume.cwd,
211
+ }),
212
+ });
213
+ }
214
+ Ok(())
215
+ }
216
+
217
+ #[cfg(unix)]
218
+ fn check_socket_ownership(endpoint: &str) -> Result<(), AppServerError> {
219
+ let path = socket_path(endpoint)?;
220
+ let meta = std::fs::metadata(&path)
221
+ .map_err(|e| AppServerError::SocketUnreachable(format!("{}:{e}", path.display())))?;
222
+ if !meta.file_type().is_socket() {
223
+ return Err(AppServerError::SocketOwnershipInvalid(format!(
224
+ "not a unix socket: {}",
225
+ path.display()
226
+ )));
227
+ }
228
+ if meta.uid() != unsafe { libc::geteuid() } {
229
+ return Err(AppServerError::SocketOwnershipInvalid(format!(
230
+ "socket owner uid {} != current uid {}",
231
+ meta.uid(),
232
+ unsafe { libc::geteuid() }
233
+ )));
234
+ }
235
+ if meta.permissions().mode() & 0o002 != 0 {
236
+ return Err(AppServerError::SocketOwnershipInvalid(format!(
237
+ "socket is world-writable: {}",
238
+ path.display()
239
+ )));
240
+ }
241
+ Ok(())
242
+ }
243
+
244
+ #[cfg(unix)]
245
+ fn socket_path(endpoint: &str) -> Result<PathBuf, AppServerError> {
246
+ let raw = endpoint.strip_prefix("unix://").unwrap_or(endpoint);
247
+ if raw.is_empty() {
248
+ return Err(AppServerError::SocketUnreachable(
249
+ "missing unix socket path".to_string(),
250
+ ));
251
+ }
252
+ Ok(PathBuf::from(raw))
253
+ }
254
+
255
+ #[cfg(unix)]
256
+ struct ThreadResume {
257
+ thread_id: String,
258
+ session_id: String,
259
+ cwd: String,
260
+ }
261
+
262
+ #[cfg(unix)]
263
+ struct AppServerClient {
264
+ stream: UnixStream,
265
+ next_id: u64,
266
+ }
267
+
268
+ #[cfg(unix)]
269
+ impl AppServerClient {
270
+ fn connect(path: &Path) -> Result<Self, AppServerError> {
271
+ let mut stream = UnixStream::connect(path)
272
+ .map_err(|e| AppServerError::SocketUnreachable(format!("{}:{e}", path.display())))?;
273
+ stream
274
+ .set_read_timeout(Some(Duration::from_secs(10)))
275
+ .map_err(|e| AppServerError::Io(e.to_string()))?;
276
+ stream
277
+ .set_write_timeout(Some(Duration::from_secs(10)))
278
+ .map_err(|e| AppServerError::Io(e.to_string()))?;
279
+ let request = concat!(
280
+ "GET / HTTP/1.1\r\n",
281
+ "Host: localhost\r\n",
282
+ "Upgrade: websocket\r\n",
283
+ "Connection: Upgrade\r\n",
284
+ "Sec-WebSocket-Key: dGVhbS1hZ2VudC1jb2RleA==\r\n",
285
+ "Sec-WebSocket-Version: 13\r\n",
286
+ "\r\n"
287
+ );
288
+ stream
289
+ .write_all(request.as_bytes())
290
+ .map_err(|e| AppServerError::Io(e.to_string()))?;
291
+ let response = read_http_response(&mut stream)?;
292
+ if !response.starts_with("HTTP/1.1 101") && !response.starts_with("HTTP/1.0 101") {
293
+ return Err(AppServerError::ProtocolMismatch(format!(
294
+ "websocket upgrade failed: {}",
295
+ response.lines().next().unwrap_or("")
296
+ )));
297
+ }
298
+ Ok(Self { stream, next_id: 1 })
299
+ }
300
+
301
+ fn initialize(&mut self) -> Result<String, AppServerError> {
302
+ let result = self.request(json!({
303
+ "method": "initialize",
304
+ "params": {
305
+ "clientInfo": {
306
+ "name": "codex-appserver-team-agent",
307
+ "title": "Team Agent",
308
+ "version": env!("CARGO_PKG_VERSION")
309
+ },
310
+ "capabilities": {
311
+ "experimentalApi": true,
312
+ "requestAttestation": false
313
+ }
314
+ }
315
+ }))?;
316
+ let _ = self.notify(json!({"method": "initialized"}));
317
+ let user_agent = result
318
+ .get("userAgent")
319
+ .and_then(Value::as_str)
320
+ .filter(|value| !value.is_empty())
321
+ .ok_or(AppServerError::MissingUserAgent)?;
322
+ if !user_agent.to_ascii_lowercase().contains("codex") {
323
+ return Err(AppServerError::ProtocolMismatch(format!(
324
+ "unexpected userAgent: {user_agent}"
325
+ )));
326
+ }
327
+ Ok(user_agent.to_string())
328
+ }
329
+
330
+ fn resume(&mut self, thread_id: &str) -> Result<ThreadResume, AppServerError> {
331
+ let result = self.request(json!({
332
+ "method": "thread/resume",
333
+ "params": {"threadId": thread_id}
334
+ }))?;
335
+ let thread = result
336
+ .get("thread")
337
+ .ok_or_else(|| AppServerError::ProtocolMismatch("missing thread".to_string()))?;
338
+ let actual_thread_id = required_str(thread, "id")?.to_string();
339
+ let session_id = required_str(thread, "sessionId")?.to_string();
340
+ let cwd = result
341
+ .get("cwd")
342
+ .and_then(Value::as_str)
343
+ .or_else(|| thread.get("cwd").and_then(Value::as_str))
344
+ .filter(|s| !s.is_empty())
345
+ .ok_or_else(|| AppServerError::ProtocolMismatch("missing cwd".to_string()))?
346
+ .to_string();
347
+ Ok(ThreadResume {
348
+ thread_id: actual_thread_id,
349
+ session_id,
350
+ cwd,
351
+ })
352
+ }
353
+
354
+ fn turn_start(
355
+ &mut self,
356
+ thread_id: &str,
357
+ message_id: &str,
358
+ rendered: &str,
359
+ ) -> Result<AppServerSubmit, AppServerError> {
360
+ let result = self.request(json!({
361
+ "method": "turn/start",
362
+ "params": {
363
+ "threadId": thread_id,
364
+ "clientUserMessageId": message_id,
365
+ "input": [{
366
+ "type": "text",
367
+ "text": rendered,
368
+ "text_elements": []
369
+ }]
370
+ }
371
+ }))?;
372
+ let turn = result
373
+ .get("turn")
374
+ .ok_or_else(|| AppServerError::ProtocolMismatch("missing turn".to_string()))?;
375
+ let turn_id = required_str(turn, "id")?.to_string();
376
+ let status = required_str(turn, "status")?.to_string();
377
+ if status != "inProgress" {
378
+ return Err(AppServerError::ProtocolMismatch(format!(
379
+ "turn/start returned status {status}"
380
+ )));
381
+ }
382
+ Ok(AppServerSubmit {
383
+ turn_id,
384
+ turn_status: status,
385
+ })
386
+ }
387
+
388
+ fn notify(&mut self, mut payload: Value) -> Result<(), AppServerError> {
389
+ if let Some(obj) = payload.as_object_mut() {
390
+ obj.remove("id");
391
+ }
392
+ write_ws_text(&mut self.stream, &payload.to_string())
393
+ }
394
+
395
+ fn request(&mut self, mut payload: Value) -> Result<Value, AppServerError> {
396
+ let method = payload
397
+ .get("method")
398
+ .and_then(Value::as_str)
399
+ .unwrap_or("unknown")
400
+ .to_string();
401
+ let id = self.next_id;
402
+ self.next_id = self.next_id.saturating_add(1);
403
+ if let Some(obj) = payload.as_object_mut() {
404
+ obj.insert("id".to_string(), json!(id));
405
+ }
406
+ write_ws_text(&mut self.stream, &payload.to_string())?;
407
+ loop {
408
+ let frame = read_ws_text(&mut self.stream)?;
409
+ let value: Value = serde_json::from_str(&frame)
410
+ .map_err(|e| AppServerError::Json(format!("{e}: {frame}")))?;
411
+ if let Some(method) = value.get("method").and_then(Value::as_str) {
412
+ if is_approval_method(method) {
413
+ return Err(AppServerError::ApprovalUnsupported(method.to_string()));
414
+ }
415
+ continue;
416
+ }
417
+ if value.get("id").and_then(Value::as_u64) != Some(id) {
418
+ continue;
419
+ }
420
+ if let Some(error) = value.get("error") {
421
+ let message = error
422
+ .get("message")
423
+ .and_then(Value::as_str)
424
+ .unwrap_or("app-server request failed")
425
+ .to_string();
426
+ return match method.as_str() {
427
+ "thread/resume" => Err(AppServerError::ThreadNotLive(message)),
428
+ "turn/start" if looks_busy(&message) => {
429
+ Err(AppServerError::LeaderBusy(message))
430
+ }
431
+ "turn/start" => Err(AppServerError::ThreadNotLive(message)),
432
+ _ => Err(AppServerError::ProtocolMismatch(format!(
433
+ "{method} failed: {message}"
434
+ ))),
435
+ }
436
+ }
437
+ return value
438
+ .get("result")
439
+ .cloned()
440
+ .ok_or_else(|| AppServerError::ProtocolMismatch("missing result".to_string()));
441
+ }
442
+ }
443
+ }
444
+
445
+ #[cfg(unix)]
446
+ fn is_approval_method(method: &str) -> bool {
447
+ method.contains("requestApproval")
448
+ || method.contains("requestUserInput")
449
+ || method.contains("elicitation/request")
450
+ }
451
+
452
+ #[cfg(unix)]
453
+ fn looks_busy(message: &str) -> bool {
454
+ let lower = message.to_ascii_lowercase();
455
+ lower.contains("active turn") || lower.contains("already has") || lower.contains("busy")
456
+ }
457
+
458
+ #[cfg(unix)]
459
+ fn read_http_response(stream: &mut UnixStream) -> Result<String, AppServerError> {
460
+ let mut data = Vec::new();
461
+ let mut buf = [0u8; 1];
462
+ while !data.ends_with(b"\r\n\r\n") {
463
+ stream
464
+ .read_exact(&mut buf)
465
+ .map_err(|e| AppServerError::Io(e.to_string()))?;
466
+ data.push(buf[0]);
467
+ if data.len() > 64 * 1024 {
468
+ return Err(AppServerError::ProtocolMismatch(
469
+ "oversized websocket handshake".to_string(),
470
+ ));
471
+ }
472
+ }
473
+ Ok(String::from_utf8_lossy(&data).to_string())
474
+ }
475
+
476
+ #[cfg(unix)]
477
+ fn write_ws_text(stream: &mut UnixStream, text: &str) -> Result<(), AppServerError> {
478
+ let payload = text.as_bytes();
479
+ let mut frame = vec![0x81];
480
+ if payload.len() < 126 {
481
+ frame.push(0x80 | payload.len() as u8);
482
+ } else if payload.len() <= u16::MAX as usize {
483
+ frame.push(0x80 | 126);
484
+ frame.extend_from_slice(&(payload.len() as u16).to_be_bytes());
485
+ } else {
486
+ frame.push(0x80 | 127);
487
+ frame.extend_from_slice(&(payload.len() as u64).to_be_bytes());
488
+ }
489
+ let mask = [0x54, 0x41, 0x47, 0x54];
490
+ frame.extend_from_slice(&mask);
491
+ for (idx, byte) in payload.iter().enumerate() {
492
+ frame.push(*byte ^ mask[idx % 4]);
493
+ }
494
+ stream
495
+ .write_all(&frame)
496
+ .map_err(|e| AppServerError::Io(e.to_string()))
497
+ }
498
+
499
+ #[cfg(unix)]
500
+ fn read_ws_text(stream: &mut UnixStream) -> Result<String, AppServerError> {
501
+ loop {
502
+ let mut header = [0u8; 2];
503
+ stream
504
+ .read_exact(&mut header)
505
+ .map_err(|e| AppServerError::Io(e.to_string()))?;
506
+ let opcode = header[0] & 0x0f;
507
+ if opcode == 0x8 {
508
+ return Err(AppServerError::ProtocolMismatch(
509
+ "websocket closed".to_string(),
510
+ ));
511
+ }
512
+ let masked = header[1] & 0x80 != 0;
513
+ let mut len = u64::from(header[1] & 0x7f);
514
+ if len == 126 {
515
+ let mut ext = [0u8; 2];
516
+ stream
517
+ .read_exact(&mut ext)
518
+ .map_err(|e| AppServerError::Io(e.to_string()))?;
519
+ len = u64::from(u16::from_be_bytes(ext));
520
+ } else if len == 127 {
521
+ let mut ext = [0u8; 8];
522
+ stream
523
+ .read_exact(&mut ext)
524
+ .map_err(|e| AppServerError::Io(e.to_string()))?;
525
+ len = u64::from_be_bytes(ext);
526
+ }
527
+ let mut mask = [0u8; 4];
528
+ if masked {
529
+ stream
530
+ .read_exact(&mut mask)
531
+ .map_err(|e| AppServerError::Io(e.to_string()))?;
532
+ }
533
+ let size = usize::try_from(len).map_err(|_| {
534
+ AppServerError::ProtocolMismatch("websocket frame too large".to_string())
535
+ })?;
536
+ let mut payload = vec![0u8; size];
537
+ stream
538
+ .read_exact(&mut payload)
539
+ .map_err(|e| AppServerError::Io(e.to_string()))?;
540
+ if masked {
541
+ for (idx, byte) in payload.iter_mut().enumerate() {
542
+ *byte ^= mask[idx % 4];
543
+ }
544
+ }
545
+ if opcode == 0x1 {
546
+ return Ok(String::from_utf8_lossy(&payload).to_string());
547
+ }
548
+ }
549
+ }