@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,324 @@
1
+ //! Process argv / environ probe.
2
+ //!
3
+ //! ## Batch 4 real implementation (leader msg_0689a63a9e40)
4
+ //!
5
+ //! Batch 4 promotes this module from a Batch 0 signature-only
6
+ //! scaffold to a **byte-preserving migration** of the process ancestry
7
+ //! + argv/env probes used by:
8
+ //!
9
+ //! - `lifecycle/launch.rs::process_ancestry_argv` +
10
+ //! `process_argv_tokens` + `process_parent_pid` — the
11
+ //! `dangerous_auto_approve` inheritance chain (0.5.0 caller-identity
12
+ //! 钉输入 is exactly this test surface).
13
+ //! - `leader/provider_attribution.rs::process_command_line` +
14
+ //! `process_environment` — leader provider attribution.
15
+ //!
16
+ //! ## Platform matrix
17
+ //!
18
+ //! - **Linux**: `/proc/<pid>/cmdline` (NUL-separated argv) and
19
+ //! `/proc/<pid>/environ` (NUL-separated KEY=VALUE).
20
+ //! - **macOS**: `sysctl(KERN_PROCARGS2)` for argv; environ not
21
+ //! currently probed (matches the pre-batch behavior).
22
+ //! - **non-Linux non-macOS Unix**: `ps -p <pid> -o command=` for argv.
23
+ //! - **Windows**: `NtQueryInformationProcess` + PEB is intrusive; for
24
+ //! Batch 4 we return `None` for both `argv_tokens` and
25
+ //! `environ_text` (design §Batch 4 Verification anchor: "unknown
26
+ //! argv must never infer elevated approval; keep worker permission
27
+ //! at provider default or require explicit user consent"). This is
28
+ //! the honest "we don't know" branch — callers already treat
29
+ //! `None` as "no elevation inherited", so Windows leaders default
30
+ //! to safe (non-dangerous) approval mode.
31
+ //!
32
+ //! ## CR C-3 anchor
33
+ //!
34
+ //! The `Option::None` return on Windows deliberately keeps worker
35
+ //! permission at provider default. Design §Batch 4 Verification: "the
36
+ //! safe direction is to avoid elevation, not to assume bypass" —
37
+ //! `detect_dangerous_approval` in `lifecycle/launch.rs` already
38
+ //! iterates `process_ancestry_argv(...)` and defaults to
39
+ //! `disabled_dangerous_approval()` when it finds no matching flag,
40
+ //! so a Windows probe returning `None` on every step lands on the
41
+ //! same disabled default. **This is intentional**: leaking a
42
+ //! bypass via unknown argv would be a MUST-NOT-13 假绿 failure.
43
+
44
+ /// Return the argv tokens for `pid`, or `None` if unavailable on
45
+ /// this platform.
46
+ ///
47
+ /// - Linux: `/proc/<pid>/cmdline` NUL-separated (byte-preserving
48
+ /// migration of `lifecycle/launch.rs:3648-3656`).
49
+ /// - macOS: `sysctl(KERN_PROCARGS2)` (migration of
50
+ /// `lifecycle/launch.rs:3659-3711`).
51
+ /// - Other Unix: `ps -p <pid> -o command=` split on whitespace
52
+ /// (migration of `lifecycle/launch.rs:3714-3729`).
53
+ /// - Windows: `None` (design §Batch 4 conservative fallback — never
54
+ /// infer elevated approval from unknown argv).
55
+ pub fn argv_tokens(pid: u32) -> Option<Vec<String>> {
56
+ #[cfg(target_os = "linux")]
57
+ {
58
+ argv_tokens_linux(pid)
59
+ }
60
+ #[cfg(target_os = "macos")]
61
+ {
62
+ argv_tokens_macos(pid)
63
+ }
64
+ #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
65
+ {
66
+ argv_tokens_ps_fallback(pid)
67
+ }
68
+ #[cfg(windows)]
69
+ {
70
+ let _ = pid;
71
+ None
72
+ }
73
+ }
74
+
75
+ /// Return the environ text (NUL-joined) for `pid`, or `None`.
76
+ ///
77
+ /// - Linux: `/proc/<pid>/environ` verbatim (migration of
78
+ /// `leader/provider_attribution.rs:113-115`).
79
+ /// - Other platforms: `None` (matches the pre-batch
80
+ /// `#[cfg(not(target_os="linux"))]` stub in
81
+ /// `leader/provider_attribution.rs:135-139`).
82
+ pub fn environ_text(pid: u32) -> Option<String> {
83
+ #[cfg(target_os = "linux")]
84
+ {
85
+ environ_text_linux(pid)
86
+ }
87
+ #[cfg(not(target_os = "linux"))]
88
+ {
89
+ let _ = pid;
90
+ None
91
+ }
92
+ }
93
+
94
+ /// Return the parent PID of `pid`, or `None` if unavailable.
95
+ ///
96
+ /// Byte-preserving migration of `lifecycle/launch.rs::process_parent_pid`:
97
+ /// - Unix: `ps -p <pid> -o ppid=` parsed as u32.
98
+ /// - Windows: Toolhelp32 snapshot walked for the entry matching
99
+ /// `pid`. Same technique as `platform::process::current_parent_pid`.
100
+ pub fn parent_pid(pid: u32) -> Option<u32> {
101
+ #[cfg(unix)]
102
+ {
103
+ parent_pid_unix(pid)
104
+ }
105
+ #[cfg(windows)]
106
+ {
107
+ parent_pid_windows(pid)
108
+ }
109
+ #[cfg(not(any(unix, windows)))]
110
+ {
111
+ let _ = pid;
112
+ None
113
+ }
114
+ }
115
+
116
+ // ─────────────────────────────────────────────────────────────────────
117
+ // Linux implementation.
118
+ // ─────────────────────────────────────────────────────────────────────
119
+
120
+ #[cfg(target_os = "linux")]
121
+ fn argv_tokens_linux(pid: u32) -> Option<Vec<String>> {
122
+ let bytes = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;
123
+ let argv_tokens = String::from_utf8_lossy(&bytes)
124
+ .split('\0')
125
+ .filter(|token| !token.is_empty())
126
+ .map(str::to_string)
127
+ .collect::<Vec<_>>();
128
+ (!argv_tokens.is_empty()).then_some(argv_tokens)
129
+ }
130
+
131
+ #[cfg(target_os = "linux")]
132
+ fn environ_text_linux(pid: u32) -> Option<String> {
133
+ String::from_utf8(std::fs::read(format!("/proc/{pid}/environ")).ok()?).ok()
134
+ }
135
+
136
+ // ─────────────────────────────────────────────────────────────────────
137
+ // macOS implementation.
138
+ // ─────────────────────────────────────────────────────────────────────
139
+
140
+ #[cfg(target_os = "macos")]
141
+ fn argv_tokens_macos(pid: u32) -> Option<Vec<String>> {
142
+ use std::mem::size_of;
143
+ let mut mib = [
144
+ libc::CTL_KERN,
145
+ libc::KERN_PROCARGS2,
146
+ i32::try_from(pid).ok()?,
147
+ ];
148
+ let mut size = 0usize;
149
+ let rc = unsafe {
150
+ libc::sysctl(
151
+ mib.as_mut_ptr(),
152
+ mib.len() as u32,
153
+ std::ptr::null_mut(),
154
+ &mut size,
155
+ std::ptr::null_mut(),
156
+ 0,
157
+ )
158
+ };
159
+ if rc != 0 || size <= size_of::<libc::c_int>() {
160
+ return None;
161
+ }
162
+ let mut buf = vec![0u8; size];
163
+ let rc = unsafe {
164
+ libc::sysctl(
165
+ mib.as_mut_ptr(),
166
+ mib.len() as u32,
167
+ buf.as_mut_ptr().cast(),
168
+ &mut size,
169
+ std::ptr::null_mut(),
170
+ 0,
171
+ )
172
+ };
173
+ if rc != 0 || size <= size_of::<libc::c_int>() {
174
+ return None;
175
+ }
176
+ let argc = i32::from_ne_bytes(buf.get(..size_of::<libc::c_int>())?.try_into().ok()?) as usize;
177
+ let mut offset = size_of::<libc::c_int>();
178
+ while offset < size && buf[offset] != 0 {
179
+ offset += 1;
180
+ }
181
+ while offset < size && buf[offset] == 0 {
182
+ offset += 1;
183
+ }
184
+ let raw = String::from_utf8_lossy(&buf[offset..size]);
185
+ let argv_tokens = raw
186
+ .split('\0')
187
+ .filter(|token| !token.is_empty())
188
+ .take(argc)
189
+ .map(str::to_string)
190
+ .collect::<Vec<_>>();
191
+ (!argv_tokens.is_empty()).then_some(argv_tokens)
192
+ }
193
+
194
+ // ─────────────────────────────────────────────────────────────────────
195
+ // Generic Unix fallback (`ps -p <pid> -o ...`).
196
+ // ─────────────────────────────────────────────────────────────────────
197
+
198
+ #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
199
+ fn argv_tokens_ps_fallback(pid: u32) -> Option<Vec<String>> {
200
+ let output = std::process::Command::new("ps")
201
+ .args(["-p", &pid.to_string(), "-o", "command="])
202
+ .output()
203
+ .ok()?;
204
+ if !output.status.success() {
205
+ return None;
206
+ }
207
+ let text = String::from_utf8_lossy(&output.stdout);
208
+ let argv_tokens = text
209
+ .split_whitespace()
210
+ .filter(|token| !token.is_empty())
211
+ .map(str::to_string)
212
+ .collect::<Vec<_>>();
213
+ (!argv_tokens.is_empty()).then_some(argv_tokens)
214
+ }
215
+
216
+ #[cfg(unix)]
217
+ fn parent_pid_unix(pid: u32) -> Option<u32> {
218
+ let output = std::process::Command::new("ps")
219
+ .args(["-p", &pid.to_string(), "-o", "ppid="])
220
+ .output()
221
+ .ok()?;
222
+ if !output.status.success() {
223
+ return None;
224
+ }
225
+ String::from_utf8_lossy(&output.stdout)
226
+ .trim()
227
+ .parse::<u32>()
228
+ .ok()
229
+ }
230
+
231
+ // ─────────────────────────────────────────────────────────────────────
232
+ // Windows implementation.
233
+ // ─────────────────────────────────────────────────────────────────────
234
+
235
+ #[cfg(windows)]
236
+ fn parent_pid_windows(pid: u32) -> Option<u32> {
237
+ use windows::Win32::System::Diagnostics::ToolHelp::{
238
+ CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
239
+ TH32CS_SNAPPROCESS,
240
+ };
241
+ let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.ok()?;
242
+ let mut entry = PROCESSENTRY32W {
243
+ dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32,
244
+ ..Default::default()
245
+ };
246
+ let mut result = unsafe { Process32FirstW(snapshot, &mut entry) };
247
+ while result.is_ok() {
248
+ if entry.th32ProcessID == pid {
249
+ let ppid = entry.th32ParentProcessID;
250
+ unsafe {
251
+ let _ = windows::Win32::Foundation::CloseHandle(snapshot);
252
+ }
253
+ return Some(ppid);
254
+ }
255
+ result = unsafe { Process32NextW(snapshot, &mut entry) };
256
+ }
257
+ unsafe {
258
+ let _ = windows::Win32::Foundation::CloseHandle(snapshot);
259
+ }
260
+ None
261
+ }
262
+
263
+ // ─────────────────────────────────────────────────────────────────────
264
+ // Tests.
265
+ // ─────────────────────────────────────────────────────────────────────
266
+
267
+ #[cfg(test)]
268
+ mod tests {
269
+ use super::*;
270
+
271
+ #[test]
272
+ fn argv_tokens_returns_some_for_own_pid_on_unix_or_none_on_windows() {
273
+ // Byte-preserving migration of the ancestry probe. On Unix
274
+ // this must return a non-empty argv for our own process (test
275
+ // binary). On Windows the honest `None` return is intentional:
276
+ // callers see the "no elevation inherited" default branch,
277
+ // which is safer than reading unknown argv (CR C-3 anchor).
278
+ let my = std::process::id();
279
+ let argv = argv_tokens(my);
280
+ #[cfg(unix)]
281
+ {
282
+ let argv = argv.expect("unix must return Some argv for own pid");
283
+ assert!(!argv.is_empty());
284
+ // At least argv[0] should look like a test binary path.
285
+ let argv0 = &argv[0];
286
+ assert!(!argv0.is_empty());
287
+ }
288
+ #[cfg(windows)]
289
+ {
290
+ // Windows conservative fallback: None, so
291
+ // `detect_dangerous_approval` defaults to disabled.
292
+ assert!(argv.is_none());
293
+ }
294
+ }
295
+
296
+ #[test]
297
+ fn parent_pid_returns_some_on_both_platforms() {
298
+ // Batch 4: parent_pid via `ps -o ppid=` (unix) or Toolhelp32
299
+ // (windows) — both must resolve our own ppid.
300
+ let my = std::process::id();
301
+ let ppid = parent_pid(my);
302
+ assert!(ppid.is_some(), "parent_pid must resolve own pid on both unix and windows");
303
+ assert!(ppid.unwrap() > 0);
304
+ }
305
+
306
+ #[test]
307
+ fn environ_text_linux_only_returns_something_for_own_pid() {
308
+ // The environ probe is Linux-only by design (matches
309
+ // `leader/provider_attribution.rs:113-115`). Other platforms
310
+ // honestly return None; callers already treat that as
311
+ // "no env-based attribution".
312
+ let my = std::process::id();
313
+ let env = environ_text(my);
314
+ #[cfg(target_os = "linux")]
315
+ {
316
+ let env = env.expect("linux must return Some environ");
317
+ assert!(env.contains('='));
318
+ }
319
+ #[cfg(not(target_os = "linux"))]
320
+ {
321
+ assert!(env.is_none());
322
+ }
323
+ }
324
+ }
@@ -0,0 +1,95 @@
1
+ //! Retryable OS error classifier + human-readable os_error_name.
2
+ //!
3
+ //! Batch 0 signature only. Batch 2 migrates the raw
4
+ //! `libc::{EACCES, EPERM, EBUSY, ENOSPC}` matching in
5
+ //! `state/persist.rs::retryable_replace_error` onto these helpers so
6
+ //! Windows sees the same classification via
7
+ //! `io::ErrorKind` + raw-code mapping.
8
+
9
+ use std::io;
10
+
11
+ /// True when the OS error is a transient replace failure worth
12
+ /// retrying (EACCES on Windows during antivirus scan, EBUSY on
13
+ /// mounted filesystems, etc.). Batch 2 wires the callsite.
14
+ #[allow(dead_code)]
15
+ pub fn retryable_replace_error(error: &io::Error) -> bool {
16
+ // Batch 2 will migrate `state/persist.rs::retryable_replace_error`
17
+ // here. Batch 0 provides the signature only.
18
+ #[cfg(unix)]
19
+ {
20
+ matches!(
21
+ error.raw_os_error(),
22
+ Some(c)
23
+ if c == libc::EACCES
24
+ || c == libc::EPERM
25
+ || c == libc::EBUSY
26
+ || c == libc::ENOSPC
27
+ )
28
+ }
29
+ #[cfg(not(unix))]
30
+ {
31
+ // Windows: retryable on sharing violation / access denied
32
+ // during antivirus scans. Map by `io::ErrorKind` so we don't
33
+ // depend on `windows-sys` raw codes at this layer.
34
+ matches!(
35
+ error.kind(),
36
+ io::ErrorKind::PermissionDenied | io::ErrorKind::WouldBlock
37
+ )
38
+ }
39
+ }
40
+
41
+ /// Human-readable name for a raw OS error code — used in diagnostics
42
+ /// so operators see `EACCES` instead of `13`.
43
+ #[allow(dead_code)]
44
+ pub fn os_error_name(error: &io::Error) -> Option<&'static str> {
45
+ #[cfg(unix)]
46
+ {
47
+ match error.raw_os_error()? {
48
+ c if c == libc::EACCES => Some("EACCES"),
49
+ c if c == libc::EPERM => Some("EPERM"),
50
+ c if c == libc::EBUSY => Some("EBUSY"),
51
+ c if c == libc::ENOSPC => Some("ENOSPC"),
52
+ c if c == libc::EWOULDBLOCK => Some("EWOULDBLOCK"),
53
+ c if c == libc::ESRCH => Some("ESRCH"),
54
+ _ => None,
55
+ }
56
+ }
57
+ #[cfg(not(unix))]
58
+ {
59
+ // Batch 2 will map ERROR_ACCESS_DENIED / ERROR_SHARING_VIOLATION
60
+ // / ERROR_LOCK_VIOLATION here. Batch 0 leaves the mapping to
61
+ // `io::Error::kind()`.
62
+ let _ = error;
63
+ None
64
+ }
65
+ }
66
+
67
+ #[cfg(test)]
68
+ mod tests {
69
+ use super::*;
70
+
71
+ #[test]
72
+ fn retryable_replace_error_matches_eacces_on_unix() {
73
+ #[cfg(unix)]
74
+ {
75
+ let err = io::Error::from_raw_os_error(libc::EACCES);
76
+ assert!(retryable_replace_error(&err));
77
+ }
78
+ }
79
+
80
+ #[test]
81
+ fn retryable_replace_error_rejects_random_error() {
82
+ // A generic "not found" is not a replace-race condition.
83
+ let err = io::Error::from(io::ErrorKind::NotFound);
84
+ assert!(!retryable_replace_error(&err));
85
+ }
86
+
87
+ #[test]
88
+ fn os_error_name_returns_stable_wire_string_on_unix() {
89
+ #[cfg(unix)]
90
+ {
91
+ let err = io::Error::from_raw_os_error(libc::EACCES);
92
+ assert_eq!(os_error_name(&err), Some("EACCES"));
93
+ }
94
+ }
95
+ }