@team-agent/installer 0.5.2 → 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 (35) hide show
  1. package/Cargo.lock +3 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +20 -0
  4. package/crates/team-agent/src/cli/emit.rs +5 -0
  5. package/crates/team-agent/src/cli/mod.rs +121 -56
  6. package/crates/team-agent/src/cli/tests/named_address.rs +4 -0
  7. package/crates/team-agent/src/codex_app_server.rs +62 -1
  8. package/crates/team-agent/src/conpty/backend.rs +120 -8
  9. package/crates/team-agent/src/coordinator/backoff.rs +88 -2
  10. package/crates/team-agent/src/coordinator/conpty_shim.rs +730 -0
  11. package/crates/team-agent/src/coordinator/health.rs +97 -11
  12. package/crates/team-agent/src/coordinator/mod.rs +8 -0
  13. package/crates/team-agent/src/coordinator/tests/daemon.rs +2 -1
  14. package/crates/team-agent/src/coordinator/tests/tick_core.rs +6 -0
  15. package/crates/team-agent/src/diagnose/orphans.rs +18 -7
  16. package/crates/team-agent/src/leader/provider_attribution.rs +19 -34
  17. package/crates/team-agent/src/leader/tests/lease_api.rs +7 -0
  18. package/crates/team-agent/src/lib.rs +14 -1
  19. package/crates/team-agent/src/lifecycle/launch.rs +80 -91
  20. package/crates/team-agent/src/lifecycle/lock.rs +40 -33
  21. package/crates/team-agent/src/lifecycle/restart/agent.rs +10 -18
  22. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -0
  23. package/crates/team-agent/src/mcp_server/wire.rs +6 -7
  24. package/crates/team-agent/src/messaging/tests/runtime.rs +5 -0
  25. package/crates/team-agent/src/packaging/tests.rs +41 -6
  26. package/crates/team-agent/src/packaging/types.rs +31 -3
  27. package/crates/team-agent/src/platform/argv.rs +324 -0
  28. package/crates/team-agent/src/platform/errors.rs +95 -0
  29. package/crates/team-agent/src/platform/file_lock.rs +418 -0
  30. package/crates/team-agent/src/platform/mod.rs +66 -0
  31. package/crates/team-agent/src/platform/process.rs +555 -0
  32. package/crates/team-agent/src/state/persist.rs +205 -25
  33. package/crates/team-agent/src/tmux_backend.rs +63 -13
  34. package/crates/team-agent/src/transport_factory.rs +124 -5
  35. package/package.json +4 -4
package/Cargo.lock CHANGED
@@ -100,6 +100,7 @@ version = "0.1.0"
100
100
  dependencies = [
101
101
  "serde",
102
102
  "serde_json",
103
+ "windows",
103
104
  ]
104
105
 
105
106
  [[package]]
@@ -574,7 +575,7 @@ dependencies = [
574
575
 
575
576
  [[package]]
576
577
  name = "team-agent"
577
- version = "0.5.2"
578
+ version = "0.5.3"
578
579
  dependencies = [
579
580
  "anyhow",
580
581
  "chrono",
@@ -587,6 +588,7 @@ dependencies = [
587
588
  "serial_test",
588
589
  "sha2",
589
590
  "thiserror",
591
+ "windows",
590
592
  ]
591
593
 
592
594
  [[package]]
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.2"
12
+ version = "0.5.3"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -27,6 +27,26 @@ chrono.workspace = true
27
27
  libc.workspace = true
28
28
  rusqlite.workspace = true # step 3 db(bundled SQLite,静态链接)
29
29
 
30
+ # 0.5.x Windows portability Batch 2/3: uses the same `windows = "0.61"`
31
+ # crate as `win-conpty-phase0` so the workspace shares a single
32
+ # Windows-API crate + already-locked Cargo.lock version.
33
+ #
34
+ # - Batch 2 (`platform::file_lock`): `Win32_Storage_FileSystem`
35
+ # (`LockFileEx`/`UnlockFileEx`) + `Win32_System_IO::OVERLAPPED`.
36
+ # - Batch 3 (`platform::process`): `Win32_System_Threading`
37
+ # (`OpenProcess`, `TerminateProcess`, `GetExitCodeProcess`,
38
+ # `GetCurrentProcessId`) + `Win32_System_Diagnostics_ToolHelp`
39
+ # (Toolhelp snapshot for parent-pid discovery).
40
+ [target.'cfg(windows)'.dependencies.windows]
41
+ version = "0.61"
42
+ features = [
43
+ "Win32_Foundation",
44
+ "Win32_Storage_FileSystem",
45
+ "Win32_System_IO",
46
+ "Win32_System_Threading",
47
+ "Win32_System_Diagnostics_ToolHelp",
48
+ ]
49
+
30
50
  [dev-dependencies]
31
51
  serial_test = { version = "3", features = ["file_locks"] }
32
52
 
@@ -1517,10 +1517,15 @@ fn peek_args(args: &[String], cwd: &Path) -> Result<PeekArgs, CliError> {
1517
1517
  fn run_coordinator(args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
1518
1518
  let parsed = parse_args(args);
1519
1519
  let workspace = crate::coordinator::WorkspacePath::new(workspace(&parsed, cwd));
1520
+ // 0.5.x Windows portability Batch 9 F8: pass `--team` through
1521
+ // to the daemon so it doesn't have to derive from state at
1522
+ // boot time. `parse_args` already recognizes `--team`; we just
1523
+ // thread the value into `DaemonArgs::team_key`.
1520
1524
  crate::coordinator::run_daemon(crate::coordinator::DaemonArgs {
1521
1525
  workspace,
1522
1526
  once: parsed.once,
1523
1527
  tick_interval_sec: parsed.tick_interval,
1528
+ team_key: parsed.team.clone(),
1524
1529
  })
1525
1530
  .map(|()| ExitCode::Ok)
1526
1531
  .map_err(|e| CliError::Runtime(e.to_string()))
@@ -943,7 +943,17 @@ pub mod lifecycle_port {
943
943
  }),
944
944
  )
945
945
  .map_err(|e| CliError::Runtime(e.to_string()))?;
946
- Ok(json!({
946
+ // 0.5.x Windows portability Batch 6 Option A (leader
947
+ // constraint 3): if a ConPTY shim was recorded in
948
+ // `state.transport.shim.pid`, terminate it explicitly via
949
+ // `platform::process::terminate_pid`.
950
+ //
951
+ // Batch 7 refinement: the field is emitted into the shutdown
952
+ // JSON only on Windows so Unix golden fixtures stay
953
+ // byte-preserving. The Unix behavior is a no-op (no shim
954
+ // concept there); adding a placeholder key would just noise
955
+ // up the fixtures.
956
+ let mut response = json!({
947
957
  "ok": ok,
948
958
  "status": status,
949
959
  "phase": phase,
@@ -966,8 +976,59 @@ pub mod lifecycle_port {
966
976
  "coordinator": {
967
977
  "status": coordinator_status,
968
978
  "pid": coordinator_pid,
979
+ },
980
+ });
981
+ #[cfg(windows)]
982
+ {
983
+ if let Some(obj) = response.as_object_mut() {
984
+ obj.insert(
985
+ "conpty_shim".to_string(),
986
+ shutdown_conpty_shim(&run_workspace),
987
+ );
969
988
  }
970
- }))
989
+ }
990
+ #[cfg(not(windows))]
991
+ {
992
+ let _ = &run_workspace;
993
+ }
994
+ Ok(response)
995
+ }
996
+
997
+ /// 0.5.x Windows portability Batch 6 Option A: locate and
998
+ /// terminate the ConPTY shim if `state.transport.shim.pid` is
999
+ /// recorded. Returns a JSON blob for the shutdown response so
1000
+ /// operators can distinguish "no shim was ever launched" from
1001
+ /// "shim terminated cleanly" from "shim already gone".
1002
+ ///
1003
+ /// Windows-only; caller (`shutdown_with_transport_and_state`)
1004
+ /// only invokes on Windows. Unix has no shim concept.
1005
+ #[cfg(windows)]
1006
+ fn shutdown_conpty_shim(workspace: &Path) -> serde_json::Value {
1007
+ let Some(pid) = crate::coordinator::conpty_shim::recorded_shim_pid(workspace) else {
1008
+ return json!({ "action": "no_shim_recorded" });
1009
+ };
1010
+ // Prefer graceful termination — Windows maps this to
1011
+ // `TerminationOutcome::ForceOnly` (Batch 3 anchor: no
1012
+ // SIGTERM equivalent for non-console child), which
1013
+ // surfaces the honest audit signal in the JSON.
1014
+ let outcome = crate::platform::process::terminate_pid(
1015
+ pid,
1016
+ crate::platform::process::SignalKind::TerminateForce,
1017
+ );
1018
+ match outcome {
1019
+ Ok(crate::platform::process::TerminationOutcome::Requested) => {
1020
+ json!({ "pid": pid, "action": "terminated" })
1021
+ }
1022
+ Ok(crate::platform::process::TerminationOutcome::AlreadyGone) => {
1023
+ json!({ "pid": pid, "action": "already_gone" })
1024
+ }
1025
+ Ok(crate::platform::process::TerminationOutcome::ForceOnly { reason }) => {
1026
+ json!({ "pid": pid, "action": "terminated_force_only", "reason": reason })
1027
+ }
1028
+ Err(e) => {
1029
+ json!({ "pid": pid, "action": "terminate_failed", "reason": e.to_string() })
1030
+ }
1031
+ }
971
1032
  }
972
1033
 
973
1034
  /// T5 (harvest §1 / A2): the bounded stop RETAINS the JoinHandle and reclaims the
@@ -1353,34 +1414,48 @@ pub mod lifecycle_port {
1353
1414
  return;
1354
1415
  }
1355
1416
  for pid in pids.iter().rev() {
1356
- send_process_signal(*pid, libc::SIGTERM);
1417
+ let _ = crate::platform::process::terminate_pid(
1418
+ *pid,
1419
+ crate::platform::process::SignalKind::TerminateGraceful,
1420
+ );
1357
1421
  }
1358
1422
  std::thread::sleep(std::time::Duration::from_millis(150));
1359
1423
  for pid in pids.iter().rev() {
1360
- send_process_signal(*pid, libc::SIGKILL);
1424
+ let _ = crate::platform::process::terminate_pid(
1425
+ *pid,
1426
+ crate::platform::process::SignalKind::TerminateForce,
1427
+ );
1361
1428
  }
1362
1429
  wait_for_processes_gone(&pids, std::time::Duration::from_secs(1));
1363
1430
  }
1364
1431
 
1365
1432
  fn reap_process_groups(pgids: &[u32], protected: &ShutdownProtection) {
1433
+ // 0.5.x Windows portability Batch 3: routes group termination
1434
+ // through `platform::process::terminate_group`. Unix keeps
1435
+ // `kill(-pgid, SIGTERM|SIGKILL)` semantics byte-for-byte;
1436
+ // Windows returns AlreadyGone (no pgid concept — Job Object
1437
+ // teardown is the shim-side concern per design §Route B).
1438
+ // The `pgid_t <= 1 || protected.contains_pgid(...)` filter
1439
+ // stays in place so pgid 0/1 (kernel/init) and the caller's
1440
+ // own group are never targeted.
1366
1441
  for pgid in pgids {
1367
- let Ok(pgid_t) = libc::pid_t::try_from(*pgid) else {
1368
- continue;
1369
- };
1370
- if pgid_t <= 1 || protected.contains_pgid(*pgid) {
1442
+ if *pgid <= 1 || protected.contains_pgid(*pgid) {
1371
1443
  continue;
1372
1444
  }
1373
- send_process_signal_group(pgid_t, libc::SIGTERM);
1445
+ let _ = crate::platform::process::terminate_group(
1446
+ *pgid,
1447
+ crate::platform::process::SignalKind::TerminateGraceful,
1448
+ );
1374
1449
  }
1375
1450
  std::thread::sleep(std::time::Duration::from_millis(150));
1376
1451
  for pgid in pgids {
1377
- let Ok(pgid_t) = libc::pid_t::try_from(*pgid) else {
1378
- continue;
1379
- };
1380
- if pgid_t <= 1 || protected.contains_pgid(*pgid) {
1452
+ if *pgid <= 1 || protected.contains_pgid(*pgid) {
1381
1453
  continue;
1382
1454
  }
1383
- send_process_signal_group(pgid_t, libc::SIGKILL);
1455
+ let _ = crate::platform::process::terminate_group(
1456
+ *pgid,
1457
+ crate::platform::process::SignalKind::TerminateForce,
1458
+ );
1384
1459
  }
1385
1460
  }
1386
1461
 
@@ -1705,7 +1780,10 @@ pub mod lifecycle_port {
1705
1780
  let mut protected = ShutdownProtection::default();
1706
1781
  let current = std::process::id();
1707
1782
  protected.pids.insert(current);
1708
- if let Ok(pgid) = u32::try_from(unsafe { libc::getpgrp() }) {
1783
+ // 0.5.x Windows portability Batch 3: use platform primitive.
1784
+ // Windows returns None (no pgid concept) and the branch is
1785
+ // skipped honestly.
1786
+ if let Some(pgid) = crate::platform::process::current_process_group() {
1709
1787
  protected.pgids.insert(pgid);
1710
1788
  }
1711
1789
  let mut cursor = current;
@@ -1840,56 +1918,33 @@ pub mod lifecycle_port {
1840
1918
  }
1841
1919
  }
1842
1920
 
1843
- fn send_process_signal(pid: u32, signal: libc::c_int) {
1844
- let Ok(pid_t) = libc::pid_t::try_from(pid) else {
1845
- return;
1846
- };
1847
- unsafe {
1848
- libc::kill(pid_t, signal);
1849
- }
1850
- }
1851
-
1852
- fn send_process_signal_group(pgid: libc::pid_t, signal: libc::c_int) {
1853
- unsafe {
1854
- libc::kill(-pgid, signal);
1855
- }
1856
- }
1921
+ // 0.5.x Windows portability Batch 3: the four inline helpers
1922
+ // (send_process_signal, send_process_signal_group,
1923
+ // reap_child_if_possible, process_is_live) have been replaced
1924
+ // by direct calls through `crate::platform::process::*` at the
1925
+ // shutdown callsites above. The private helpers are removed so
1926
+ // there's a single source of truth for signal/waitpid/liveness
1927
+ // semantics; Unix behavior is byte-preserving (SignalKind maps
1928
+ // 1:1 to SIGTERM/SIGKILL, ProcessLiveness::Live == the previous
1929
+ // `kill(pid, 0) == 0 || EPERM` branch).
1857
1930
 
1858
1931
  fn wait_for_processes_gone(pids: &[u32], timeout: std::time::Duration) {
1859
1932
  let start = std::time::Instant::now();
1860
1933
  loop {
1861
1934
  for pid in pids {
1862
- reap_child_if_possible(*pid);
1935
+ crate::platform::process::reap_child_if_possible(*pid);
1863
1936
  }
1864
- if !pids.iter().any(|pid| process_is_live(*pid)) || start.elapsed() >= timeout {
1937
+ if !pids
1938
+ .iter()
1939
+ .any(|pid| crate::platform::process::pid_is_alive(*pid))
1940
+ || start.elapsed() >= timeout
1941
+ {
1865
1942
  return;
1866
1943
  }
1867
1944
  std::thread::sleep(std::time::Duration::from_millis(25));
1868
1945
  }
1869
1946
  }
1870
1947
 
1871
- fn reap_child_if_possible(pid: u32) {
1872
- let Ok(pid_t) = libc::pid_t::try_from(pid) else {
1873
- return;
1874
- };
1875
- let mut status = 0;
1876
- unsafe {
1877
- libc::waitpid(pid_t, &mut status, libc::WNOHANG);
1878
- }
1879
- }
1880
-
1881
- fn process_is_live(pid: u32) -> bool {
1882
- let Ok(pid_t) = libc::pid_t::try_from(pid) else {
1883
- return false;
1884
- };
1885
- let rc = unsafe { libc::kill(pid_t, 0) };
1886
- if rc == 0 {
1887
- return true;
1888
- }
1889
- let err = std::io::Error::last_os_error();
1890
- err.raw_os_error() == Some(libc::EPERM)
1891
- }
1892
-
1893
1948
  fn process_pgids(
1894
1949
  pids: &[u32],
1895
1950
  protected: &ShutdownProtection,
@@ -1900,8 +1955,15 @@ pub mod lifecycle_port {
1900
1955
  .filter_map(|pid| table.iter().find(|process| process.pid == *pid))
1901
1956
  .filter_map(|process| process.pgid)
1902
1957
  .filter(|pgid| {
1903
- libc::pid_t::try_from(*pgid)
1904
- .map(|pgid_t| pgid_t > 1 && !protected.contains_pgid(*pgid))
1958
+ // 0.5.x Windows portability Batch 4: `libc::pid_t` was
1959
+ // used here as a signed-integer conversion gate to
1960
+ // reject values > INT_MAX. Replace with the equivalent
1961
+ // `i32::try_from` (pgid_t is `c_int` on every Unix we
1962
+ // support). Windows has no pgid concept so `pgids`
1963
+ // is empty in practice — the filter is dead code on
1964
+ // Windows but must still compile.
1965
+ i32::try_from(*pgid)
1966
+ .map(|pgid_int| pgid_int > 1 && !protected.contains_pgid(*pgid))
1905
1967
  .unwrap_or(false)
1906
1968
  })
1907
1969
  .collect::<Vec<_>>();
@@ -1927,7 +1989,10 @@ pub mod lifecycle_port {
1927
1989
  .map(|process| process.pid)
1928
1990
  .collect::<std::collections::BTreeSet<_>>();
1929
1991
  for pid in root_pids {
1930
- if !protected.contains_pid(*pid) && process_is_live(*pid) && seen.insert(*pid) {
1992
+ if !protected.contains_pid(*pid)
1993
+ && crate::platform::process::pid_is_alive(*pid)
1994
+ && seen.insert(*pid)
1995
+ {
1931
1996
  residuals.push(ProcessInfo {
1932
1997
  pid: *pid,
1933
1998
  ppid: 0,
@@ -355,6 +355,9 @@ fn resolve_leader_name_after_rebind() {
355
355
  let _ = std::fs::remove_dir_all(&ws);
356
356
  }
357
357
 
358
+ // 0.5.x Windows portability Batch 5: `FakeAppServer` uses Unix
359
+ // domain sockets — Unix-only.
360
+ #[cfg(unix)]
358
361
  #[test]
359
362
  fn resolve_app_server_leader_name_uses_typed_receiver_not_tmux_pane() {
360
363
  let ws = named_ws("appserver-leader-resolve");
@@ -419,6 +422,7 @@ fn resolve_app_server_leader_name_fails_closed_on_transport_conflict() {
419
422
  let _ = std::fs::remove_dir_all(&ws);
420
423
  }
421
424
 
425
+ #[cfg(unix)]
422
426
  #[test]
423
427
  fn send_to_name_app_server_leader_submits_turn_without_tmux_pane() {
424
428
  let ws = named_ws("appserver-leader-send");
@@ -1,10 +1,31 @@
1
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.
2
17
 
3
18
  use serde_json::{json, Value};
19
+ #[cfg(unix)]
4
20
  use std::io::{Read, Write};
21
+ #[cfg(unix)]
5
22
  use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
23
+ #[cfg(unix)]
6
24
  use std::os::unix::net::UnixStream;
7
- use std::path::{Path, PathBuf};
25
+ #[cfg(unix)]
26
+ use std::path::Path;
27
+ use std::path::PathBuf;
28
+ #[cfg(unix)]
8
29
  use std::time::Duration;
9
30
 
10
31
  pub const APP_SERVER_PROTOCOL_FIXTURE_CLI_MIN: &str = "0.139.0";
@@ -79,6 +100,7 @@ impl std::fmt::Display for AppServerError {
79
100
 
80
101
  impl std::error::Error for AppServerError {}
81
102
 
103
+ #[cfg(unix)]
82
104
  pub fn attach_probe(endpoint: &str, thread_id: &str) -> Result<AppServerBinding, AppServerError> {
83
105
  check_socket_ownership(endpoint)?;
84
106
  let socket = socket_path(endpoint)?;
@@ -95,6 +117,21 @@ pub fn attach_probe(endpoint: &str, thread_id: &str) -> Result<AppServerBinding,
95
117
  })
96
118
  }
97
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)]
98
135
  pub fn submit_to_bound_thread(
99
136
  binding: &AppServerBinding,
100
137
  message_id: &str,
@@ -108,6 +145,19 @@ pub fn submit_to_bound_thread(
108
145
  client.turn_start(&binding.thread_id, message_id, rendered)
109
146
  }
110
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
+
111
161
  pub fn binding_from_receiver(receiver: &Value) -> Result<AppServerBinding, AppServerError> {
112
162
  let app = receiver
113
163
  .get("app_server")
@@ -135,6 +185,7 @@ fn required_str<'a>(value: &'a Value, field: &str) -> Result<&'a str, AppServerE
135
185
  .ok_or_else(|| AppServerError::ProtocolMismatch(format!("missing {field}")))
136
186
  }
137
187
 
188
+ #[cfg(unix)]
138
189
  fn validate_tuple(
139
190
  binding: &AppServerBinding,
140
191
  user_agent: &str,
@@ -163,6 +214,7 @@ fn validate_tuple(
163
214
  Ok(())
164
215
  }
165
216
 
217
+ #[cfg(unix)]
166
218
  fn check_socket_ownership(endpoint: &str) -> Result<(), AppServerError> {
167
219
  let path = socket_path(endpoint)?;
168
220
  let meta = std::fs::metadata(&path)
@@ -189,6 +241,7 @@ fn check_socket_ownership(endpoint: &str) -> Result<(), AppServerError> {
189
241
  Ok(())
190
242
  }
191
243
 
244
+ #[cfg(unix)]
192
245
  fn socket_path(endpoint: &str) -> Result<PathBuf, AppServerError> {
193
246
  let raw = endpoint.strip_prefix("unix://").unwrap_or(endpoint);
194
247
  if raw.is_empty() {
@@ -199,17 +252,20 @@ fn socket_path(endpoint: &str) -> Result<PathBuf, AppServerError> {
199
252
  Ok(PathBuf::from(raw))
200
253
  }
201
254
 
255
+ #[cfg(unix)]
202
256
  struct ThreadResume {
203
257
  thread_id: String,
204
258
  session_id: String,
205
259
  cwd: String,
206
260
  }
207
261
 
262
+ #[cfg(unix)]
208
263
  struct AppServerClient {
209
264
  stream: UnixStream,
210
265
  next_id: u64,
211
266
  }
212
267
 
268
+ #[cfg(unix)]
213
269
  impl AppServerClient {
214
270
  fn connect(path: &Path) -> Result<Self, AppServerError> {
215
271
  let mut stream = UnixStream::connect(path)
@@ -386,17 +442,20 @@ impl AppServerClient {
386
442
  }
387
443
  }
388
444
 
445
+ #[cfg(unix)]
389
446
  fn is_approval_method(method: &str) -> bool {
390
447
  method.contains("requestApproval")
391
448
  || method.contains("requestUserInput")
392
449
  || method.contains("elicitation/request")
393
450
  }
394
451
 
452
+ #[cfg(unix)]
395
453
  fn looks_busy(message: &str) -> bool {
396
454
  let lower = message.to_ascii_lowercase();
397
455
  lower.contains("active turn") || lower.contains("already has") || lower.contains("busy")
398
456
  }
399
457
 
458
+ #[cfg(unix)]
400
459
  fn read_http_response(stream: &mut UnixStream) -> Result<String, AppServerError> {
401
460
  let mut data = Vec::new();
402
461
  let mut buf = [0u8; 1];
@@ -414,6 +473,7 @@ fn read_http_response(stream: &mut UnixStream) -> Result<String, AppServerError>
414
473
  Ok(String::from_utf8_lossy(&data).to_string())
415
474
  }
416
475
 
476
+ #[cfg(unix)]
417
477
  fn write_ws_text(stream: &mut UnixStream, text: &str) -> Result<(), AppServerError> {
418
478
  let payload = text.as_bytes();
419
479
  let mut frame = vec![0x81];
@@ -436,6 +496,7 @@ fn write_ws_text(stream: &mut UnixStream, text: &str) -> Result<(), AppServerErr
436
496
  .map_err(|e| AppServerError::Io(e.to_string()))
437
497
  }
438
498
 
499
+ #[cfg(unix)]
439
500
  fn read_ws_text(stream: &mut UnixStream) -> Result<String, AppServerError> {
440
501
  loop {
441
502
  let mut header = [0u8; 2];