@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
@@ -559,25 +559,17 @@ pub(super) fn drain_old_pane_and_pid(
559
559
  })
560
560
  }
561
561
 
562
- /// Best-effort liveness probe for a pid. Returns true if the pid exists
563
- /// on Unix (signal 0). On other platforms, returns true conservatively.
562
+ /// Best-effort liveness probe for a pid.
563
+ ///
564
+ /// 0.5.x Windows portability Batch 3: routes through
565
+ /// `crate::platform::process::pid_is_alive`. Unix uses `kill(pid, 0)`
566
+ /// with the `EPERM = Live` branch preserved byte-for-byte; Windows
567
+ /// uses `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` +
568
+ /// `GetExitCodeProcess` (STILL_ACTIVE = Live). The legacy non-Unix
569
+ /// `true` fallback (which broke drain by reporting every pid alive)
570
+ /// is gone.
564
571
  pub(super) fn pid_is_alive(pid: u32) -> bool {
565
- #[cfg(unix)]
566
- {
567
- // SAFETY: kill(pid, 0) checks if pid exists without sending a signal.
568
- let ret = unsafe { libc::kill(pid as i32, 0) };
569
- if ret == 0 {
570
- return true;
571
- }
572
- // EPERM also means the process exists (we just can't signal it).
573
- let err = std::io::Error::last_os_error();
574
- matches!(err.raw_os_error(), Some(libc::EPERM))
575
- }
576
- #[cfg(not(unix))]
577
- {
578
- let _ = pid;
579
- true
580
- }
572
+ crate::platform::process::pid_is_alive(pid)
581
573
  }
582
574
 
583
575
  /// Read state.agents[agent_id].pane_pid (u32) from the runtime state.
@@ -467,6 +467,22 @@ pub(super) fn start_coordinator_for_workspace(workspace: &Path) -> Result<bool,
467
467
  /// only within `lifecycle::restart`. Sharing the resolver across the lifecycle
468
468
  /// module is the correct ownership: restart/add/fork all need the SAME socket
469
469
  /// the live team uses, and duplicating the lookup invited drift.**
470
+ /// 0.5.x Phase 1d Batch 1: this legacy resolver stays tmux-only. It
471
+ /// still returns a concrete `TmuxBackend` because the 6 caller sites
472
+ /// (rebuild/agent/remove/launch) currently call `TmuxBackend`-specific
473
+ /// methods (`.tmux_endpoint()`, `.for_workspace()`, etc.). But it now
474
+ /// routes through the factory so its **selection semantics** match
475
+ /// the new `lifecycle_worker_transport_for_selected_state` API:
476
+ ///
477
+ /// - If `state.transport.kind` is `"conpty"`, this legacy tmux-typed
478
+ /// resolver **fails-closed** with `TransportBackendKindMismatch`
479
+ /// rather than silently returning a tmux backend that would
480
+ /// spawn/inject into the wrong pane universe. That satisfies CR C-1
481
+ /// fail-closed while the migration of caller sites to the generic
482
+ /// `_transport_for_selected_state` API completes.
483
+ /// - Otherwise (default/legacy tmux endpoint), behavior is preserved
484
+ /// byte-for-byte through
485
+ /// `tmux_backend_for_runtime_state_or_workspace`.
470
486
  pub(crate) fn lifecycle_worker_tmux_backend_for_selected_state(
471
487
  run_workspace: &Path,
472
488
  team: Option<&str>,
@@ -485,12 +501,66 @@ pub(crate) fn lifecycle_worker_tmux_backend_for_selected_state(
485
501
  .unwrap_or("");
486
502
  return Err(LifecycleError::TeamSelect(format!("{reason}: {detail}")));
487
503
  }
504
+ // CR C-1 fail-closed: state with `transport.kind=conpty` must NOT
505
+ // silently downgrade to a tmux backend just because the caller
506
+ // asked for the tmux-typed variant. Callers that see this refusal
507
+ // are expected to migrate to `lifecycle_worker_transport_for_selected_state`
508
+ // during Batch 2/3.
509
+ if let Some(state_ref) = state.as_ref() {
510
+ if let Some(kind) = state_ref.pointer("/transport/kind").and_then(|v| v.as_str()) {
511
+ if kind.eq_ignore_ascii_case("conpty") {
512
+ return Err(LifecycleError::TeamSelect(format!(
513
+ "backend_kind_mismatch: state.transport.kind={kind:?} but the legacy \
514
+ tmux-typed lifecycle resolver was invoked; caller must migrate to \
515
+ `lifecycle_worker_transport_for_selected_state` (Phase 1d Batch 2/3)"
516
+ )));
517
+ }
518
+ }
519
+ }
488
520
  Ok(state
489
521
  .as_ref()
490
522
  .map(|state| lifecycle_worker_tmux_backend_for_state(run_workspace, state))
491
523
  .unwrap_or_else(|| crate::tmux_backend::TmuxBackend::for_workspace(run_workspace)))
492
524
  }
493
525
 
526
+ /// 0.5.x Phase 1d Batch 1: new generic-typed lifecycle resolver.
527
+ ///
528
+ /// Same team-selection semantics as the legacy tmux-typed variant, but
529
+ /// returns a `ResolvedTransport` from the factory so callers that can
530
+ /// hold `Box<dyn Transport>` (or the resolved `BackendKind`) work
531
+ /// against both tmux AND conpty state. Caller migration to this API
532
+ /// happens across Batch 2-5.
533
+ ///
534
+ /// Fail-closed rules follow the factory (C-1 ×2, C-2 CLI-vs-state,
535
+ /// C-3 N38 notices via `ResolvedTransport.notices`).
536
+ pub(crate) fn lifecycle_worker_transport_for_selected_state(
537
+ run_workspace: &Path,
538
+ team: Option<&str>,
539
+ ) -> Result<crate::transport_factory::ResolvedTransport, LifecycleError> {
540
+ let (state, refusal) = crate::state::projection::resolve_team_scoped_state(run_workspace, team)
541
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
542
+ if let Some(refusal) = refusal {
543
+ let reason = refusal
544
+ .get("reason")
545
+ .and_then(|v| v.as_str())
546
+ .unwrap_or("team_target_unresolved");
547
+ let detail = refusal
548
+ .get("error")
549
+ .or_else(|| refusal.get("message"))
550
+ .and_then(|v| v.as_str())
551
+ .unwrap_or("");
552
+ return Err(LifecycleError::TeamSelect(format!("{reason}: {detail}")));
553
+ }
554
+ let input = crate::transport_factory::TransportFactoryInput::new(
555
+ run_workspace,
556
+ crate::transport_factory::TransportPurpose::LifecycleWorker,
557
+ )
558
+ .with_team_key(team)
559
+ .with_state(state.as_ref());
560
+ crate::transport_factory::resolve_transport(input)
561
+ .map_err(|e| LifecycleError::TeamSelect(e.to_string()))
562
+ }
563
+
494
564
  pub(super) fn lifecycle_worker_tmux_backend_for_state(
495
565
  run_workspace: &Path,
496
566
  state: &serde_json::Value,
@@ -287,6 +287,7 @@ impl Drop for EnvVarGuard {
287
287
  }
288
288
  }
289
289
 
290
+ #[cfg(unix)]
290
291
  struct TmuxShim {
291
292
  log: std::path::PathBuf,
292
293
  _path: EnvVarGuard,
@@ -297,6 +298,7 @@ struct TmuxShim {
297
298
  _real_tmux: EnvVarGuard,
298
299
  }
299
300
 
301
+ #[cfg(unix)]
300
302
  fn install_e27_tmux_shim(expected_endpoint: &str, session_name: &str) -> TmuxShim {
301
303
  use std::os::unix::fs::PermissionsExt;
302
304
 
@@ -441,6 +443,9 @@ fn assert_only_expected_socket_used(log: &std::path::Path, expected_endpoint: &s
441
443
  }
442
444
  }
443
445
 
446
+ // 0.5.x Windows portability Batch 5: E27 tests use a shell-script
447
+ // tmux shim + Unix socket paths. Unix-only.
448
+ #[cfg(unix)]
444
449
  #[test]
445
450
  #[serial_test::serial(env)]
446
451
  fn e27_stop_agent_uses_attached_explicit_state_socket() {
@@ -461,6 +466,7 @@ fn e27_stop_agent_uses_attached_explicit_state_socket() {
461
466
  );
462
467
  }
463
468
 
469
+ #[cfg(unix)]
464
470
  #[test]
465
471
  #[serial_test::serial(env)]
466
472
  fn e27_reset_agent_uses_attached_explicit_state_socket_for_stop_and_spawn() {
@@ -491,6 +497,7 @@ fn e27_reset_agent_uses_attached_explicit_state_socket_for_stop_and_spawn() {
491
497
  );
492
498
  }
493
499
 
500
+ #[cfg(unix)]
494
501
  #[test]
495
502
  #[serial_test::serial(env)]
496
503
  fn e27_stop_agent_expands_short_state_socket_name() {
@@ -2693,6 +2693,13 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
2693
2693
  "tasks",
2694
2694
  "display_backend",
2695
2695
  "is_external_leader",
2696
+ // 0.5.x Phase 1d hot-path 接线 (裁决1 msg_76e1d98202b8):
2697
+ // `transport = { kind, source }` inserted here by
2698
+ // `annotate_runtime_transport`. Kind is `"tmux"` for the
2699
+ // default tmux path (byte-equivalent behavior + new metadata
2700
+ // field); source is `"unknown"` until Phase 2 threads
2701
+ // `ResolvedTransport.source` through the launch call site.
2702
+ "transport",
2696
2703
  // 0.4.0 refactor: `team_key` added as top-level topology marker
2697
2704
  // alongside active_team_key (refactor-modular-architecture). The
2698
2705
  // owner / leader_receiver / owner_epoch invariants below still
@@ -558,6 +558,15 @@ fn normalize_path_string(text: &str, ctx: &NormalizeCtx) -> String {
558
558
 
559
559
  fn normalize_team_agent_binary_path(text: &str) -> String {
560
560
  let mut out = text.to_string();
561
+ // 0.5.0 hermetic 教训「环境路径类 token 化」的直接延伸
562
+ // (leader msg_6ee04cf5aee8):归一化改为结构判据,不绑路径前缀。
563
+ // 用户 CARGO_TARGET_DIR 可以指到任意目录(默认 `<repo>/target`,
564
+ // 全局改道时是 `/Volumes/nvme/cargo-target`,CI 时可能是别的)。
565
+ // 结构判据:任意绝对路径下的 `/deps/team_agent-<hex>`(可选 `.exe`)
566
+ // → `<TEAM_AGENT_BIN>`。同时保留旧 marker 兼容(处理旧固定字符串
567
+ // 出现在 output 里,以及 `debug/team-agent` / `release/team-agent`
568
+ // 这两个非 `/deps/` 分支)。
569
+ out = normalize_deps_team_agent_by_structure(&out);
561
570
  for marker in [
562
571
  "/target/debug/deps/team_agent-",
563
572
  "/target/debug/team-agent",
@@ -568,6 +577,60 @@ fn normalize_team_agent_binary_path(text: &str) -> String {
568
577
  out
569
578
  }
570
579
 
580
+ /// Structural (path-prefix-independent) normalization for
581
+ /// `/…/deps/team_agent-<hex>(.exe)?` occurrences. Scans the input for
582
+ /// every `/deps/team_agent-` marker and rewrites the containing
583
+ /// absolute path token to `<TEAM_AGENT_BIN>`, no matter which
584
+ /// `CARGO_TARGET_DIR` produced it.
585
+ fn normalize_deps_team_agent_by_structure(text: &str) -> String {
586
+ const MARKER: &str = "/deps/team_agent-";
587
+ let mut out = String::with_capacity(text.len());
588
+ let bytes = text.as_bytes();
589
+ let mut i = 0usize;
590
+ while i < bytes.len() {
591
+ let Some(rel) = text[i..].find(MARKER) else {
592
+ out.push_str(&text[i..]);
593
+ break;
594
+ };
595
+ let marker_idx = i + rel;
596
+ // Locate the start of the absolute path token: walk back from
597
+ // `marker_idx` while the byte is not one of a small set of
598
+ // delimiters (quote, whitespace, `[`, `(`, `,`, `:` after
599
+ // whitespace). The absolute-path anchor MUST also start with
600
+ // `/` — if the containing token is not absolute, skip
601
+ // rewriting so we don't eat unrelated text.
602
+ let path_start = text[..marker_idx]
603
+ .rfind(|c: char| matches!(c, '"' | '\'' | ' ' | '[' | '(' | ',' | '\n' | '\t'))
604
+ .map(|idx| idx + 1)
605
+ .unwrap_or(0);
606
+ // Confirm the token starts with `/` (absolute path).
607
+ if !text[path_start..].starts_with('/') {
608
+ // Not an absolute path — leave alone.
609
+ out.push_str(&text[i..marker_idx + MARKER.len()]);
610
+ i = marker_idx + MARKER.len();
611
+ continue;
612
+ }
613
+ // Extend the end past the hex hash + optional `.exe`. Same
614
+ // continuation rule as `replace_path_with_marker`: alnum + `-`
615
+ // + `_` + `.`.
616
+ let mut path_end = marker_idx + MARKER.len();
617
+ while path_end < text.len() {
618
+ let ch = text[path_end..].chars().next().expect("char at boundary");
619
+ if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
620
+ path_end += ch.len_utf8();
621
+ } else {
622
+ break;
623
+ }
624
+ }
625
+ // Emit everything up to the path start, then the token, then
626
+ // continue past the eaten span.
627
+ out.push_str(&text[i..path_start]);
628
+ out.push_str("<TEAM_AGENT_BIN>");
629
+ i = path_end;
630
+ }
631
+ out
632
+ }
633
+
571
634
  fn replace_path_with_marker(mut text: String, marker: &str, token: &str) -> String {
572
635
  let mut search_from = 0;
573
636
  while let Some(offset) = text[search_from..].find(marker) {
@@ -314,14 +314,13 @@ fn non_empty_env(key: &str) -> Option<String> {
314
314
  .filter(|value| !value.is_empty())
315
315
  }
316
316
 
317
- #[cfg(unix)]
317
+ // 0.5.x Windows portability Batch 3: parent-pid probe routes through
318
+ // `crate::platform::process::current_parent_pid`. Unix uses
319
+ // `libc::getppid`; Windows uses Toolhelp32Snapshot. Both return
320
+ // `Option<u32>`; the legacy `0` fallback (which would have silently
321
+ // degraded MCP metadata + orphan detection on Windows) is gone.
318
322
  fn parent_pid() -> u32 {
319
- u32::try_from(unsafe { libc::getppid() }).unwrap_or(0)
320
- }
321
-
322
- #[cfg(not(unix))]
323
- fn parent_pid() -> u32 {
324
- 0
323
+ crate::platform::process::current_parent_pid().unwrap_or(0)
325
324
  }
326
325
 
327
326
  fn error_response_value(id: RpcId, code: i64, message: String) -> Value {