@team-agent/installer 0.5.2 → 0.5.4
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/diagnose.rs +191 -31
- package/crates/team-agent/src/cli/emit.rs +5 -0
- package/crates/team-agent/src/cli/mod.rs +213 -89
- 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/coordinator/tick.rs +13 -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 +89 -92
- package/crates/team-agent/src/lifecycle/lock.rs +40 -33
- package/crates/team-agent/src/lifecycle/restart/agent.rs +25 -19
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +108 -6
- package/crates/team-agent/src/lifecycle/restart/selection.rs +47 -18
- package/crates/team-agent/src/lifecycle/restart.rs +16 -6
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +287 -53
- package/crates/team-agent/src/lifecycle/tests/restart.rs +144 -5
- package/crates/team-agent/src/lifecycle/types.rs +1 -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/provider/adapter.rs +103 -41
- package/crates/team-agent/src/provider/session/capture.rs +328 -66
- package/crates/team-agent/src/provider/session/resume.rs +30 -28
- package/crates/team-agent/src/provider/session_scan/common.rs +117 -1
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +222 -25
- package/crates/team-agent/src/state/projection.rs +59 -4
- 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
|
@@ -53,6 +53,21 @@ pub fn coordinator_health(workspace: &WorkspacePath) -> HealthReport {
|
|
|
53
53
|
/// `start_coordinator`(`lifecycle.py:49-121`):幂等 — 已健康 no-op(AlreadyRunning);metadata 不兼容
|
|
54
54
|
/// 先 stop 再起;schema 不兼容拒启 + hint;否则 spawn `team-agent coordinator --workspace <ws>`。
|
|
55
55
|
pub fn start_coordinator(workspace: &WorkspacePath) -> Result<StartReport, StartError> {
|
|
56
|
+
start_coordinator_with_team(workspace, None)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// 0.5.x Windows portability Batch 9 F8 (leader msg_2a4cc1fa54c0):
|
|
60
|
+
/// forward `--team` to the spawned coord daemon so it doesn't have
|
|
61
|
+
/// to derive team_key from `state.active_team_key` at boot. The
|
|
62
|
+
/// derivation stays as fallback (see `coordinator::backoff::run_daemon`),
|
|
63
|
+
/// so Unix daemons and pre-existing test harnesses are byte-preserving.
|
|
64
|
+
///
|
|
65
|
+
/// Callers that CAN pass team_key (Batch 9 quick-start Windows path)
|
|
66
|
+
/// SHOULD — that avoids Batch 8's F8 seed-state trap.
|
|
67
|
+
pub fn start_coordinator_with_team(
|
|
68
|
+
workspace: &WorkspacePath,
|
|
69
|
+
team_key: Option<&str>,
|
|
70
|
+
) -> Result<StartReport, StartError> {
|
|
56
71
|
let health = coordinator_health(workspace);
|
|
57
72
|
if health.ok {
|
|
58
73
|
return Ok(StartReport {
|
|
@@ -101,7 +116,13 @@ pub fn start_coordinator(workspace: &WorkspacePath) -> Result<StartReport, Start
|
|
|
101
116
|
let mut command = Command::new(std::env::current_exe()?);
|
|
102
117
|
command
|
|
103
118
|
.args(["coordinator", "--workspace"])
|
|
104
|
-
.arg(workspace.as_path())
|
|
119
|
+
.arg(workspace.as_path());
|
|
120
|
+
if let Some(tk) = team_key {
|
|
121
|
+
if !tk.is_empty() {
|
|
122
|
+
command.args(["--team", tk]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
command
|
|
105
126
|
.stdin(Stdio::null())
|
|
106
127
|
.stdout(Stdio::from(log))
|
|
107
128
|
.stderr(Stdio::from(log_err));
|
|
@@ -135,7 +156,23 @@ fn detach_daemon_child(command: &mut Command) {
|
|
|
135
156
|
}
|
|
136
157
|
}
|
|
137
158
|
|
|
138
|
-
#[cfg(
|
|
159
|
+
#[cfg(windows)]
|
|
160
|
+
fn detach_daemon_child(command: &mut Command) {
|
|
161
|
+
// 0.5.x Windows portability Batch 8 F7 (leader msg_590b4dce0f68):
|
|
162
|
+
// detach the coordinator daemon on Windows via
|
|
163
|
+
// `DETACHED_PROCESS | CREATE_BREAKAWAY_FROM_JOB` creation flags,
|
|
164
|
+
// matching what `coordinator::conpty_shim::spawn_shim_and_handshake`
|
|
165
|
+
// does for the shim. Without these flags, an SSH-launched
|
|
166
|
+
// quick-start blocks waiting for the coord daemon's process
|
|
167
|
+
// tree to exit (never happens — daemon runs forever), and the
|
|
168
|
+
// quick-start caller sees a hung command.
|
|
169
|
+
use std::os::windows::process::CommandExt;
|
|
170
|
+
const DETACHED_PROCESS: u32 = 0x00000008;
|
|
171
|
+
const CREATE_BREAKAWAY_FROM_JOB: u32 = 0x01000000;
|
|
172
|
+
command.creation_flags(DETACHED_PROCESS | CREATE_BREAKAWAY_FROM_JOB);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[cfg(not(any(unix, windows)))]
|
|
139
176
|
fn detach_daemon_child(_command: &mut Command) {}
|
|
140
177
|
|
|
141
178
|
/// `stop_coordinator`(`lifecycle.py:228-247`):SIGTERM pid + 清 pid/meta → typed report。
|
|
@@ -272,16 +309,32 @@ fn coordinator_command_matches_workspace(command: &str, workspaces: &[String]) -
|
|
|
272
309
|
}
|
|
273
310
|
|
|
274
311
|
fn terminate_pid(pid: Pid) -> bool {
|
|
312
|
+
// 0.5.x Windows portability Batch 3: routes signal delivery through
|
|
313
|
+
// `platform::process::terminate_pid`. Unix keeps
|
|
314
|
+
// SIGTERM → 5s grace → SIGKILL semantics byte-for-byte
|
|
315
|
+
// (`SignalKind::TerminateGraceful` → SIGTERM,
|
|
316
|
+
// `SignalKind::TerminateForce` → SIGKILL). Windows performs
|
|
317
|
+
// `TerminateProcess` for both kinds; the `TerminationOutcome::ForceOnly`
|
|
318
|
+
// return on the graceful call is what a future audit-event
|
|
319
|
+
// emitter (CR C-6) will trigger `platform.terminate_force_only`
|
|
320
|
+
// on. For this batch the return value is discarded, matching the
|
|
321
|
+
// current inline `let _ = send_signal(...)` pattern.
|
|
275
322
|
if pid_is_running(pid).ok() == Some(false) {
|
|
276
323
|
return true;
|
|
277
324
|
}
|
|
278
325
|
let pids = process_tree_pids(pid);
|
|
279
326
|
for child in pids.iter().rev() {
|
|
280
|
-
let _ =
|
|
327
|
+
let _ = crate::platform::process::terminate_pid(
|
|
328
|
+
child.get(),
|
|
329
|
+
crate::platform::process::SignalKind::TerminateGraceful,
|
|
330
|
+
);
|
|
281
331
|
}
|
|
282
332
|
if !wait_until_all_not_running(&pids, Duration::from_secs(5)) {
|
|
283
333
|
for child in pids.iter().rev() {
|
|
284
|
-
let _ =
|
|
334
|
+
let _ = crate::platform::process::terminate_pid(
|
|
335
|
+
child.get(),
|
|
336
|
+
crate::platform::process::SignalKind::TerminateForce,
|
|
337
|
+
);
|
|
285
338
|
}
|
|
286
339
|
}
|
|
287
340
|
wait_until_all_not_running(&pids, Duration::from_secs(5))
|
|
@@ -348,16 +401,17 @@ fn wait_until_all_not_running(pids: &[Pid], timeout: Duration) -> bool {
|
|
|
348
401
|
}
|
|
349
402
|
|
|
350
403
|
fn reap_child_if_possible(pid: Pid) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
let mut status = 0;
|
|
355
|
-
unsafe {
|
|
356
|
-
libc::waitpid(pid_t, &mut status, libc::WNOHANG);
|
|
357
|
-
}
|
|
404
|
+
// Batch 3: routed through `platform::process`. Unix `waitpid
|
|
405
|
+
// (WNOHANG)`; Windows no-op (no zombie model).
|
|
406
|
+
crate::platform::process::reap_child_if_possible(pid.get());
|
|
358
407
|
}
|
|
359
408
|
|
|
409
|
+
#[cfg(unix)]
|
|
410
|
+
#[allow(dead_code)]
|
|
360
411
|
fn send_signal(pid: Pid, signal: libc::c_int) -> bool {
|
|
412
|
+
// Retained (dead code post-Batch-3) as a Unix-only helper for any
|
|
413
|
+
// future non-standard signal delivery. All product paths now use
|
|
414
|
+
// `crate::platform::process::terminate_pid` with `SignalKind`.
|
|
361
415
|
let Ok(pid_t) = libc::pid_t::try_from(pid.get()) else {
|
|
362
416
|
return false;
|
|
363
417
|
};
|
|
@@ -383,6 +437,21 @@ fn wait_until_not_running(pid: Pid, timeout: Duration) -> bool {
|
|
|
383
437
|
|
|
384
438
|
/// `pid_is_running`(`metadata.py:16-25`):`os.kill(pid, 0)` + `ps -o stat=` 查 zombie(Z* → 不算活)。
|
|
385
439
|
/// §10 fallible:进程探测 I/O 可失败 → Result。
|
|
440
|
+
///
|
|
441
|
+
/// 0.5.x Windows portability Batch 4: this function has UNIQUE
|
|
442
|
+
/// coordinator-metadata semantics — it treats `EPERM` as "not
|
|
443
|
+
/// running" (different from `platform::process::pid_liveness` which
|
|
444
|
+
/// treats `EPERM` as Live) because the coordinator only owns
|
|
445
|
+
/// processes it can signal, and uses `ps -o stat=` for zombie
|
|
446
|
+
/// detection. The `ps` shellout is Unix-only.
|
|
447
|
+
///
|
|
448
|
+
/// On Windows the coordinator-metadata identity check runs through
|
|
449
|
+
/// `platform::process::pid_liveness` instead (Windows has no zombie
|
|
450
|
+
/// state — a process is either Live or Dead — so the additional
|
|
451
|
+
/// `ps stat=` step has no analogue). This preserves the coordinator's
|
|
452
|
+
/// "am I the owner" check without silently reporting stale pids as
|
|
453
|
+
/// alive.
|
|
454
|
+
#[cfg(unix)]
|
|
386
455
|
pub fn pid_is_running(pid: Pid) -> Result<bool, std::io::Error> {
|
|
387
456
|
let Ok(pid_t) = libc::pid_t::try_from(pid.get()) else {
|
|
388
457
|
return Ok(false);
|
|
@@ -407,6 +476,23 @@ pub fn pid_is_running(pid: Pid) -> Result<bool, std::io::Error> {
|
|
|
407
476
|
Ok(!stat.is_empty() && !stat.starts_with('Z'))
|
|
408
477
|
}
|
|
409
478
|
|
|
479
|
+
/// Windows shim: no `ps stat=` zombie detection needed (Windows has
|
|
480
|
+
/// no zombie state — a process is either Live or Dead). Route through
|
|
481
|
+
/// `platform::process::pid_liveness` and map to bool. The EPERM
|
|
482
|
+
/// semantic ("we can't signal → treat as not running") maps to
|
|
483
|
+
/// Windows `ERROR_ACCESS_DENIED` which the platform layer already
|
|
484
|
+
/// treats as `Live`; so on Windows the coordinator sees a process
|
|
485
|
+
/// it can't query as still-running (safer than pretending it's gone
|
|
486
|
+
/// and losing the ownership handle).
|
|
487
|
+
#[cfg(not(unix))]
|
|
488
|
+
pub fn pid_is_running(pid: Pid) -> Result<bool, std::io::Error> {
|
|
489
|
+
match crate::platform::process::pid_liveness(pid.get())? {
|
|
490
|
+
crate::platform::process::ProcessLiveness::Live => Ok(true),
|
|
491
|
+
crate::platform::process::ProcessLiveness::Dead => Ok(false),
|
|
492
|
+
crate::platform::process::ProcessLiveness::Unknown { .. } => Ok(false),
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
410
496
|
/// `read_coordinator_metadata`(`metadata.py:28-34`)。读 `coordinator.json`;损坏/缺失/非 dict → `None`。
|
|
411
497
|
pub fn read_coordinator_metadata(workspace: &WorkspacePath) -> Option<CoordinatorMetadata> {
|
|
412
498
|
let text = std::fs::read_to_string(coordinator_meta_path(workspace)).ok()?;
|
|
@@ -62,6 +62,14 @@ use crate::provider::{ProviderAdapter, TurnId, TurnState};
|
|
|
62
62
|
use serde_json::Value;
|
|
63
63
|
|
|
64
64
|
pub mod backoff;
|
|
65
|
+
// 0.5.x Windows portability Batch 6 Option A: shim lifecycle
|
|
66
|
+
// manager. Windows-only per design §Shim Lifecycle (the shim binary
|
|
67
|
+
// exists only on Windows; Unix uses tmux). Cfg-gated at the module
|
|
68
|
+
// level so callers can `crate::coordinator::conpty_shim::...`
|
|
69
|
+
// uniformly on Windows and get a compile error (rather than a
|
|
70
|
+
// silent no-op) on Unix — Unix code paths never reach for the shim.
|
|
71
|
+
#[cfg(windows)]
|
|
72
|
+
pub mod conpty_shim;
|
|
65
73
|
pub mod health;
|
|
66
74
|
pub mod orphan;
|
|
67
75
|
pub mod runtime_detectors;
|
|
@@ -87,7 +87,7 @@ fn start_coordinator_fresh_workspace_decides_started() {
|
|
|
87
87
|
fn run_daemon_once_writes_boot_metadata_and_returns_ok() {
|
|
88
88
|
let (wp, dir) = daemon_ws();
|
|
89
89
|
crate::state::persist::save_runtime_state(&dir, &serde_json::json!({"session_name": "team-x", "agents": {}})).unwrap();
|
|
90
|
-
let r = run_daemon(DaemonArgs { workspace: wp.clone(), once: true, tick_interval_sec: None });
|
|
90
|
+
let r = run_daemon(DaemonArgs { workspace: wp.clone(), once: true, tick_interval_sec: None, team_key: None });
|
|
91
91
|
assert!(r.is_ok(), "run_daemon --once must return Ok; got {r:?}");
|
|
92
92
|
assert!(coordinator_meta_path(&wp).exists(), "run_daemon must write the coordinator boot metadata");
|
|
93
93
|
}
|
|
@@ -438,6 +438,7 @@ fn run_daemon_backs_off_on_transient_tick_err_then_recovers_without_exiting() {
|
|
|
438
438
|
workspace: WorkspacePath::new(dir.clone()),
|
|
439
439
|
once: false,
|
|
440
440
|
tick_interval_sec: Some(0.01), // tiny backoff so the test is fast
|
|
441
|
+
team_key: None,
|
|
441
442
|
};
|
|
442
443
|
let result = run_daemon_with_coordinator(&args, &coord);
|
|
443
444
|
assert!(
|
|
@@ -192,6 +192,12 @@ fn p2_tick_skips_tmux_gate_when_session_name_absent() {
|
|
|
192
192
|
|
|
193
193
|
// P1 — pid_is_running must use os.kill(pid,0) first: a pid owned by another user (pid 1 /
|
|
194
194
|
// launchd, root) is EPERM → not signalable → False. Current only `ps -p` (rc=0) → True.
|
|
195
|
+
//
|
|
196
|
+
// 0.5.x Windows portability Batch 5: this test's PREMISE is Unix
|
|
197
|
+
// specific (pid 1 = init/launchd, EPERM semantics). On Windows,
|
|
198
|
+
// pid_is_running routes through `platform::process::pid_liveness` which
|
|
199
|
+
// has different accessible-pid semantics. Unix-only test.
|
|
200
|
+
#[cfg(unix)]
|
|
195
201
|
#[test]
|
|
196
202
|
fn p2_pid_is_running_false_for_cross_user_pid() {
|
|
197
203
|
// The cross-user semantics only hold for a non-root caller (root CAN signal pid 1).
|
|
@@ -553,6 +553,19 @@ impl Coordinator {
|
|
|
553
553
|
}),
|
|
554
554
|
)?;
|
|
555
555
|
}
|
|
556
|
+
for mismatch in &report.identity_mismatches {
|
|
557
|
+
event_log.write(
|
|
558
|
+
"provider.session.identity_mismatch",
|
|
559
|
+
serde_json::json!({
|
|
560
|
+
"agent_id": mismatch.agent_id,
|
|
561
|
+
"expected_agent_id": mismatch.expected_agent_id,
|
|
562
|
+
"embedded_agent_id": mismatch.embedded_agent_id,
|
|
563
|
+
"session_id": mismatch.session_id,
|
|
564
|
+
"rollout_path": mismatch.rollout_path,
|
|
565
|
+
"spawn_cwd": mismatch.spawn_cwd,
|
|
566
|
+
}),
|
|
567
|
+
)?;
|
|
568
|
+
}
|
|
556
569
|
for ambiguous in report.ambiguous {
|
|
557
570
|
let candidate_count = report
|
|
558
571
|
.candidate_count_by_agent
|
|
@@ -342,14 +342,25 @@ fn tmux_socket_names() -> Vec<String> {
|
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
fn tmux_socket_roots() -> Vec<PathBuf> {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
345
|
+
// 0.5.x Windows portability Batch 1: tmux orphan scan is Unix-only
|
|
346
|
+
// (design §Layering Strategy — "ConPTY orphan diagnostics should be
|
|
347
|
+
// a separate shim registry command later"). On Windows return
|
|
348
|
+
// empty so the caller loops zero times honestly.
|
|
349
|
+
#[cfg(unix)]
|
|
350
|
+
{
|
|
351
|
+
let uid = unsafe { libc::geteuid() };
|
|
352
|
+
let mut roots = vec![PathBuf::from(format!("/tmp/tmux-{uid}"))];
|
|
353
|
+
if let Some(tmpdir) = std::env::var_os("TMPDIR") {
|
|
354
|
+
roots.push(PathBuf::from(tmpdir).join(format!("tmux-{uid}")));
|
|
355
|
+
}
|
|
356
|
+
roots.sort();
|
|
357
|
+
roots.dedup();
|
|
358
|
+
roots
|
|
359
|
+
}
|
|
360
|
+
#[cfg(not(unix))]
|
|
361
|
+
{
|
|
362
|
+
Vec::new()
|
|
349
363
|
}
|
|
350
|
-
roots.sort();
|
|
351
|
-
roots.dedup();
|
|
352
|
-
roots
|
|
353
364
|
}
|
|
354
365
|
|
|
355
366
|
fn tmux_list_panes(socket: &str) -> Vec<TmuxPaneRow> {
|
|
@@ -98,44 +98,29 @@ fn provider_wire_for_command_attribution(provider: Provider) -> &'static str {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
// 0.5.x Windows portability Batch 4: process command-line + environ
|
|
102
|
+
// probes route through `crate::platform::argv`. Unix keeps
|
|
103
|
+
// byte-preserving semantics (Linux `/proc/<pid>/cmdline` + `environ`;
|
|
104
|
+
// other Unix `ps -p ... -o command=`; macOS environ is `None` because
|
|
105
|
+
// the pre-batch code also returned `None` on non-Linux). Windows
|
|
106
|
+
// returns `None` per design §Batch 4 conservative anchor (unknown
|
|
107
|
+
// argv must never infer elevated approval).
|
|
108
|
+
//
|
|
109
|
+
// The command-line format expected by the caller is a
|
|
110
|
+
// whitespace-joined single string, so we join the argv tokens after
|
|
111
|
+
// the platform probe. On the non-Linux Unix `ps` fallback path the
|
|
112
|
+
// tokens come already whitespace-split from `ps -o command=`; joining
|
|
113
|
+
// them back with spaces reproduces the exact pre-batch shape.
|
|
102
114
|
fn process_command_line(pid: u32) -> Option<String> {
|
|
103
|
-
let
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.collect::<Vec<_>>();
|
|
109
|
-
(!parts.is_empty()).then(|| parts.join(" "))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
#[cfg(target_os = "linux")]
|
|
113
|
-
fn process_environment(pid: u32) -> Option<String> {
|
|
114
|
-
String::from_utf8(std::fs::read(format!("/proc/{pid}/environ")).ok()?).ok()
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
#[cfg(not(target_os = "linux"))]
|
|
118
|
-
fn process_command_line(pid: u32) -> Option<String> {
|
|
119
|
-
let output = crate::os_probe::bounded_command_output_with_probe(
|
|
120
|
-
Command::new("ps")
|
|
121
|
-
.arg("-p")
|
|
122
|
-
.arg(pid.to_string())
|
|
123
|
-
.args(["-o", "command="]),
|
|
124
|
-
"provider_argv",
|
|
125
|
-
Some(pid),
|
|
126
|
-
)
|
|
127
|
-
.ok()?;
|
|
128
|
-
output
|
|
129
|
-
.status
|
|
130
|
-
.success()
|
|
131
|
-
.then(|| String::from_utf8_lossy(&output.stdout).trim().to_string())
|
|
132
|
-
.filter(|text| !text.is_empty())
|
|
115
|
+
let argv = crate::platform::argv::argv_tokens(pid)?;
|
|
116
|
+
if argv.is_empty() {
|
|
117
|
+
return None;
|
|
118
|
+
}
|
|
119
|
+
Some(argv.join(" "))
|
|
133
120
|
}
|
|
134
121
|
|
|
135
|
-
#[cfg(not(target_os = "linux"))]
|
|
136
122
|
fn process_environment(pid: u32) -> Option<String> {
|
|
137
|
-
|
|
138
|
-
None
|
|
123
|
+
crate::platform::argv::environ_text(pid)
|
|
139
124
|
}
|
|
140
125
|
|
|
141
126
|
#[cfg(test)]
|
|
@@ -303,6 +303,11 @@ fn r8_requeued_exhausted_watchers_event_payload_golden_shape() {
|
|
|
303
303
|
assert_eq!(payload.get("trigger").and_then(|v| v.as_str()), Some("attach_leader"));
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
// 0.5.x Windows portability Batch 5: `FakeAppServer` uses UNIX domain
|
|
307
|
+
// sockets to fake the Codex app-server. Codex app-server client is
|
|
308
|
+
// Unix-only (Windows returns typed `SocketUnreachable`), so these
|
|
309
|
+
// tests are Unix-only too.
|
|
310
|
+
#[cfg(unix)]
|
|
306
311
|
#[test]
|
|
307
312
|
fn app_server_attach_writes_transport_kind_tuple_and_advances_epoch() {
|
|
308
313
|
let ws = std::env::temp_dir().join(format!(
|
|
@@ -357,6 +362,7 @@ fn app_server_attach_writes_transport_kind_tuple_and_advances_epoch() {
|
|
|
357
362
|
assert_eq!(saved["owner_epoch"], serde_json::json!(1));
|
|
358
363
|
}
|
|
359
364
|
|
|
365
|
+
#[cfg(unix)]
|
|
360
366
|
#[test]
|
|
361
367
|
fn app_server_attach_rejects_world_writable_socket_without_state_write() {
|
|
362
368
|
let ws = std::env::temp_dir().join(format!(
|
|
@@ -396,6 +402,7 @@ fn app_server_attach_rejects_world_writable_socket_without_state_write() {
|
|
|
396
402
|
);
|
|
397
403
|
}
|
|
398
404
|
|
|
405
|
+
#[cfg(unix)]
|
|
399
406
|
#[test]
|
|
400
407
|
fn app_server_attach_rejects_missing_user_agent_without_state_write() {
|
|
401
408
|
let ws = std::env::temp_dir().join(format!(
|
|
@@ -78,6 +78,13 @@ pub mod diagnose;
|
|
|
78
78
|
// 富返回类型,fn body unimplemented!(),P2 porter 落实现)。lifecycle=quick-start/restart/display;
|
|
79
79
|
// mcp_server=stdio MCP tool handlers;cli=clap 子命令;packaging=install/migrate/repair。
|
|
80
80
|
pub mod lifecycle;
|
|
81
|
+
|
|
82
|
+
// 0.5.x Windows portability Batch 0: platform abstraction layer.
|
|
83
|
+
// Truth sources:
|
|
84
|
+
// - Design: `.team/artifacts/0.5.x-windows-portability-survey-design.md`
|
|
85
|
+
// - CR verdict: `.team/artifacts/0.5.x-windows-portability-cr-verdict.md`
|
|
86
|
+
// (6 constraints anchored inside the module doc)
|
|
87
|
+
pub mod platform;
|
|
81
88
|
// 0.3.28 — unified adaptive layout manager (single source of truth for tmux
|
|
82
89
|
// topology decisions). See `.team/artifacts/adaptive-layout-full-architecture-locate.md`.
|
|
83
90
|
pub mod layout;
|
|
@@ -114,5 +121,11 @@ pub mod conpty;
|
|
|
114
121
|
// (6 constraints anchored inside the module doc)
|
|
115
122
|
pub mod transport_factory;
|
|
116
123
|
|
|
117
|
-
|
|
124
|
+
// 0.5.x Windows portability Batch 5: `app_server_test_support` is a
|
|
125
|
+
// Unix-domain-socket fake for the Codex app-server client. It is
|
|
126
|
+
// Unix-only because the code-under-test (`codex_app_server`) is
|
|
127
|
+
// Unix-only via cfg (Windows gets typed `SocketUnreachable`
|
|
128
|
+
// unsupported returns). Cfg-gating the module keeps `cargo check
|
|
129
|
+
// --tests --target x86_64-pc-windows-msvc` compilable.
|
|
130
|
+
#[cfg(all(test, unix))]
|
|
118
131
|
pub(crate) mod app_server_test_support;
|
|
@@ -374,6 +374,8 @@ fn spawn_agents(
|
|
|
374
374
|
);
|
|
375
375
|
}
|
|
376
376
|
}
|
|
377
|
+
let spawn_epoch = u64::try_from(started.len()).unwrap_or(u64::MAX);
|
|
378
|
+
let spawned_at = spawn_timestamp_for_agent(u32::try_from(spawn_epoch).unwrap_or(u32::MAX));
|
|
377
379
|
// E6 层1 实证3 + 诊断留痕:落最终 worker argv(spawn 前的真实形态)。
|
|
378
380
|
// 任何"--session-id 预定 UUID 没生效"必须能从 events.jsonl 回答:argv 里到底有没有它。
|
|
379
381
|
// 抽出 --session-id 值单列,方便和盘上 ~/.claude/projects/<cwd> 实际落的 UUID 对账。
|
|
@@ -393,6 +395,10 @@ fn spawn_agents(
|
|
|
393
395
|
"argv": plan.argv,
|
|
394
396
|
"session_id_in_argv": session_id_in_argv,
|
|
395
397
|
"expected_session_id": plan.expected_session_id.as_ref().map(|s| s.as_str()),
|
|
398
|
+
"spawn_cwd": workspace.to_string_lossy(),
|
|
399
|
+
"spawned_at": spawned_at.as_str(),
|
|
400
|
+
"source": "launch",
|
|
401
|
+
"spawn_epoch": spawn_epoch,
|
|
396
402
|
}),
|
|
397
403
|
);
|
|
398
404
|
}
|
|
@@ -473,6 +479,7 @@ fn spawn_agents(
|
|
|
473
479
|
agent_id,
|
|
474
480
|
start_mode: StartMode::Fresh,
|
|
475
481
|
target: spawn.pane_id.as_str().to_string(),
|
|
482
|
+
spawned_at,
|
|
476
483
|
session_id: None,
|
|
477
484
|
rollout_path: None,
|
|
478
485
|
pending_session_id: plan.expected_session_id.clone(),
|
|
@@ -872,7 +879,9 @@ fn persist_spawn_agent_state(
|
|
|
872
879
|
continue;
|
|
873
880
|
}
|
|
874
881
|
let pane_pid = pane_pids_by_agent.get(id).copied();
|
|
875
|
-
let spawned_at =
|
|
882
|
+
let spawned_at = started_agent
|
|
883
|
+
.map(|started| started.spawned_at.clone())
|
|
884
|
+
.unwrap_or_else(|| spawn_timestamp_for_agent(spawn_index));
|
|
876
885
|
spawn_index = spawn_index.saturating_add(1);
|
|
877
886
|
agents.insert(
|
|
878
887
|
id.to_string(),
|
|
@@ -2904,12 +2913,81 @@ pub fn quick_start_in_workspace_with_display_and_backend(
|
|
|
2904
2913
|
))
|
|
2905
2914
|
})?;
|
|
2906
2915
|
let team_key_for_factory = team_id;
|
|
2916
|
+
// 0.5.x Windows portability Batch 8 F7 (leader msg_590b4dce0f68):
|
|
2917
|
+
// shim ownership moves to the coordinator. quick-start no longer
|
|
2918
|
+
// calls `spawn_shim_and_handshake` directly. Instead we ensure
|
|
2919
|
+
// the coordinator daemon is running; the coordinator's boot
|
|
2920
|
+
// path calls `conpty_shim::ensure_shim_running` (idempotent —
|
|
2921
|
+
// spawns if no live shim, reconnects if one is recorded).
|
|
2922
|
+
//
|
|
2923
|
+
// Rationale (from Batch 7 gate report F7): quick-start is a
|
|
2924
|
+
// one-shot process; if it owns the shim, the shim dies when
|
|
2925
|
+
// quick-start exits. Moving ownership to the coord daemon
|
|
2926
|
+
// gives us the "coord can die, shim survives" invariant the
|
|
2927
|
+
// design's §Shim Lifecycle chapter requires.
|
|
2928
|
+
//
|
|
2929
|
+
// The `ensure_coordinator_running` call is idempotent per
|
|
2930
|
+
// `coordinator/health.rs::start_coordinator`. On non-Windows
|
|
2931
|
+
// this whole block is cfg'd out.
|
|
2932
|
+
#[cfg(windows)]
|
|
2933
|
+
if matches!(
|
|
2934
|
+
requested,
|
|
2935
|
+
crate::transport_factory::RequestedTransportBackend::ConPty
|
|
2936
|
+
) {
|
|
2937
|
+
let team_key_str = team_key_for_factory.ok_or_else(|| {
|
|
2938
|
+
LifecycleError::TeamSelect(
|
|
2939
|
+
"team_key required for --backend conpty on Windows (Batch 9 F8)".to_string(),
|
|
2940
|
+
)
|
|
2941
|
+
})?;
|
|
2942
|
+
// 0.5.x Windows portability Batch 9 F8 (leader msg_2a4cc1fa54c0):
|
|
2943
|
+
// Batch 8's seed-state pattern (writing active_team_key +
|
|
2944
|
+
// transport.kind to state.json before start_coordinator)
|
|
2945
|
+
// caused downstream launch code to see "existing runtime, use
|
|
2946
|
+
// restart" and skip spec compile. F8 fix: pass `--team`
|
|
2947
|
+
// directly to the coord daemon via `start_coordinator_with_team`
|
|
2948
|
+
// so state doesn't need pre-seeding.
|
|
2949
|
+
//
|
|
2950
|
+
// The coord daemon's `run_daemon_with_coordinator_and_boot_tmux`
|
|
2951
|
+
// then calls `ensure_shim_running` with the CLI-supplied
|
|
2952
|
+
// team_key. When quick-start's downstream code runs, state.json
|
|
2953
|
+
// still has whatever it had before (empty on fresh launch), so
|
|
2954
|
+
// the spec-compile + worker-spawn path runs normally.
|
|
2955
|
+
let run_ws = crate::coordinator::WorkspacePath::new(workspace.clone());
|
|
2956
|
+
let start_report =
|
|
2957
|
+
crate::coordinator::health::start_coordinator_with_team(&run_ws, Some(team_key_str))
|
|
2958
|
+
.map_err(|e| LifecycleError::TeamSelect(format!("coordinator start: {e}")))?;
|
|
2959
|
+
if !start_report.ok {
|
|
2960
|
+
return Err(LifecycleError::TeamSelect(format!(
|
|
2961
|
+
"coordinator start failed: schema_error={:?}, action={:?}",
|
|
2962
|
+
start_report.schema_error, start_report.action
|
|
2963
|
+
)));
|
|
2964
|
+
}
|
|
2965
|
+
// Give the coordinator a beat to write its transport.shim
|
|
2966
|
+
// block so the factory's `pipe_ready` gate opens on the
|
|
2967
|
+
// next resolve. The coordinator's `run_daemon` calls
|
|
2968
|
+
// `ensure_shim_running` inside its boot code path (see
|
|
2969
|
+
// `coordinator::backoff::run_daemon`).
|
|
2970
|
+
std::thread::sleep(std::time::Duration::from_millis(2500));
|
|
2971
|
+
}
|
|
2907
2972
|
let input = crate::transport_factory::TransportFactoryInput::new(
|
|
2908
2973
|
&workspace,
|
|
2909
2974
|
crate::transport_factory::TransportPurpose::Launch,
|
|
2910
2975
|
)
|
|
2911
2976
|
.with_team_key(team_key_for_factory)
|
|
2912
2977
|
.with_explicit_backend(Some(requested));
|
|
2978
|
+
// On Windows the factory needs to SEE the freshly-persisted
|
|
2979
|
+
// `state.transport.shim.pipe_ready = true` marker so its
|
|
2980
|
+
// `conpty_pipe_ready` gate opens.
|
|
2981
|
+
#[cfg(windows)]
|
|
2982
|
+
let state_value =
|
|
2983
|
+
std::fs::read_to_string(crate::state::persist::runtime_state_path(&workspace))
|
|
2984
|
+
.ok()
|
|
2985
|
+
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok());
|
|
2986
|
+
#[cfg(windows)]
|
|
2987
|
+
let input = match state_value.as_ref() {
|
|
2988
|
+
Some(v) => input.with_state(Some(v)),
|
|
2989
|
+
None => input,
|
|
2990
|
+
};
|
|
2913
2991
|
let resolved = crate::transport_factory::resolve_transport(input)
|
|
2914
2992
|
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
2915
2993
|
// Hand the boxed backend down as `&dyn Transport`.
|
|
@@ -3644,102 +3722,21 @@ fn process_ancestry_argv(pid: u32) -> Vec<Vec<String>> {
|
|
|
3644
3722
|
out
|
|
3645
3723
|
}
|
|
3646
3724
|
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
(!argv_tokens.is_empty()).then_some(argv_tokens)
|
|
3656
|
-
}
|
|
3657
|
-
|
|
3658
|
-
#[cfg(target_os = "macos")]
|
|
3659
|
-
fn process_argv_tokens(pid: u32) -> Option<Vec<String>> {
|
|
3660
|
-
use std::mem::size_of;
|
|
3661
|
-
|
|
3662
|
-
let mut mib = [
|
|
3663
|
-
libc::CTL_KERN,
|
|
3664
|
-
libc::KERN_PROCARGS2,
|
|
3665
|
-
i32::try_from(pid).ok()?,
|
|
3666
|
-
];
|
|
3667
|
-
let mut size = 0usize;
|
|
3668
|
-
let rc = unsafe {
|
|
3669
|
-
libc::sysctl(
|
|
3670
|
-
mib.as_mut_ptr(),
|
|
3671
|
-
mib.len() as u32,
|
|
3672
|
-
std::ptr::null_mut(),
|
|
3673
|
-
&mut size,
|
|
3674
|
-
std::ptr::null_mut(),
|
|
3675
|
-
0,
|
|
3676
|
-
)
|
|
3677
|
-
};
|
|
3678
|
-
if rc != 0 || size <= size_of::<libc::c_int>() {
|
|
3679
|
-
return None;
|
|
3680
|
-
}
|
|
3681
|
-
let mut buf = vec![0u8; size];
|
|
3682
|
-
let rc = unsafe {
|
|
3683
|
-
libc::sysctl(
|
|
3684
|
-
mib.as_mut_ptr(),
|
|
3685
|
-
mib.len() as u32,
|
|
3686
|
-
buf.as_mut_ptr().cast(),
|
|
3687
|
-
&mut size,
|
|
3688
|
-
std::ptr::null_mut(),
|
|
3689
|
-
0,
|
|
3690
|
-
)
|
|
3691
|
-
};
|
|
3692
|
-
if rc != 0 || size <= size_of::<libc::c_int>() {
|
|
3693
|
-
return None;
|
|
3694
|
-
}
|
|
3695
|
-
let argc = i32::from_ne_bytes(buf.get(..size_of::<libc::c_int>())?.try_into().ok()?) as usize;
|
|
3696
|
-
let mut offset = size_of::<libc::c_int>();
|
|
3697
|
-
while offset < size && buf[offset] != 0 {
|
|
3698
|
-
offset += 1;
|
|
3699
|
-
}
|
|
3700
|
-
while offset < size && buf[offset] == 0 {
|
|
3701
|
-
offset += 1;
|
|
3702
|
-
}
|
|
3703
|
-
let raw = String::from_utf8_lossy(&buf[offset..size]);
|
|
3704
|
-
let argv_tokens = raw
|
|
3705
|
-
.split('\0')
|
|
3706
|
-
.filter(|token| !token.is_empty())
|
|
3707
|
-
.take(argc)
|
|
3708
|
-
.map(str::to_string)
|
|
3709
|
-
.collect::<Vec<_>>();
|
|
3710
|
-
(!argv_tokens.is_empty()).then_some(argv_tokens)
|
|
3711
|
-
}
|
|
3725
|
+
// 0.5.x Windows portability Batch 4: `process_argv_tokens` and
|
|
3726
|
+
// `process_parent_pid` route through `crate::platform::argv`. Unix
|
|
3727
|
+
// impls are byte-preserving (Linux `/proc/<pid>/cmdline`, macOS
|
|
3728
|
+
// `sysctl(KERN_PROCARGS2)`, other Unix `ps -p ... -o command=`).
|
|
3729
|
+
// Windows returns `None` per design §Batch 4 conservative anchor:
|
|
3730
|
+
// unknown argv must never infer elevated approval; callers already
|
|
3731
|
+
// treat `None` as "no elevation inherited" via
|
|
3732
|
+
// `disabled_dangerous_approval()`.
|
|
3712
3733
|
|
|
3713
|
-
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
|
3714
3734
|
fn process_argv_tokens(pid: u32) -> Option<Vec<String>> {
|
|
3715
|
-
|
|
3716
|
-
.args(["-p", &pid.to_string(), "-o", "command="])
|
|
3717
|
-
.output()
|
|
3718
|
-
.ok()?;
|
|
3719
|
-
if !output.status.success() {
|
|
3720
|
-
return None;
|
|
3721
|
-
}
|
|
3722
|
-
let text = String::from_utf8_lossy(&output.stdout);
|
|
3723
|
-
let argv_tokens = text
|
|
3724
|
-
.split_whitespace()
|
|
3725
|
-
.filter(|token| !token.is_empty())
|
|
3726
|
-
.map(str::to_string)
|
|
3727
|
-
.collect::<Vec<_>>();
|
|
3728
|
-
(!argv_tokens.is_empty()).then_some(argv_tokens)
|
|
3735
|
+
crate::platform::argv::argv_tokens(pid)
|
|
3729
3736
|
}
|
|
3730
3737
|
|
|
3731
3738
|
fn process_parent_pid(pid: u32) -> Option<u32> {
|
|
3732
|
-
|
|
3733
|
-
.args(["-p", &pid.to_string(), "-o", "ppid="])
|
|
3734
|
-
.output()
|
|
3735
|
-
.ok()?;
|
|
3736
|
-
if !output.status.success() {
|
|
3737
|
-
return None;
|
|
3738
|
-
}
|
|
3739
|
-
String::from_utf8_lossy(&output.stdout)
|
|
3740
|
-
.trim()
|
|
3741
|
-
.parse::<u32>()
|
|
3742
|
-
.ok()
|
|
3739
|
+
crate::platform::argv::parent_pid(pid)
|
|
3743
3740
|
}
|
|
3744
3741
|
|
|
3745
3742
|
/// `add_agent(workspace, agent_id, role_file_path, open_display, team)`
|