@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.
- package/Cargo.lock +3 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +20 -0
- package/crates/team-agent/src/cli/emit.rs +5 -0
- package/crates/team-agent/src/cli/mod.rs +121 -56
- package/crates/team-agent/src/cli/tests/named_address.rs +4 -0
- package/crates/team-agent/src/codex_app_server.rs +62 -1
- package/crates/team-agent/src/conpty/backend.rs +120 -8
- package/crates/team-agent/src/coordinator/backoff.rs +88 -2
- package/crates/team-agent/src/coordinator/conpty_shim.rs +730 -0
- package/crates/team-agent/src/coordinator/health.rs +97 -11
- package/crates/team-agent/src/coordinator/mod.rs +8 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +2 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +6 -0
- package/crates/team-agent/src/diagnose/orphans.rs +18 -7
- package/crates/team-agent/src/leader/provider_attribution.rs +19 -34
- package/crates/team-agent/src/leader/tests/lease_api.rs +7 -0
- package/crates/team-agent/src/lib.rs +14 -1
- package/crates/team-agent/src/lifecycle/launch.rs +80 -91
- package/crates/team-agent/src/lifecycle/lock.rs +40 -33
- package/crates/team-agent/src/lifecycle/restart/agent.rs +10 -18
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -0
- package/crates/team-agent/src/mcp_server/wire.rs +6 -7
- package/crates/team-agent/src/messaging/tests/runtime.rs +5 -0
- package/crates/team-agent/src/packaging/tests.rs +41 -6
- package/crates/team-agent/src/packaging/types.rs +31 -3
- package/crates/team-agent/src/platform/argv.rs +324 -0
- package/crates/team-agent/src/platform/errors.rs +95 -0
- package/crates/team-agent/src/platform/file_lock.rs +418 -0
- package/crates/team-agent/src/platform/mod.rs +66 -0
- package/crates/team-agent/src/platform/process.rs +555 -0
- package/crates/team-agent/src/state/persist.rs +205 -25
- package/crates/team-agent/src/tmux_backend.rs +63 -13
- package/crates/team-agent/src/transport_factory.rs +124 -5
- 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.
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
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
|
|
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
|
|
1904
|
-
|
|
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)
|
|
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
|
-
|
|
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];
|