@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.
- package/Cargo.lock +120 -9
- package/Cargo.toml +2 -2
- package/crates/team-agent/Cargo.toml +21 -0
- package/crates/team-agent/src/app_server_test_support.rs +258 -0
- package/crates/team-agent/src/cli/adapters.rs +32 -3
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +16 -0
- package/crates/team-agent/src/cli/diagnose.rs +14 -5
- package/crates/team-agent/src/cli/emit.rs +79 -1
- package/crates/team-agent/src/cli/mod.rs +190 -80
- package/crates/team-agent/src/cli/named_address.rs +111 -0
- package/crates/team-agent/src/cli/send.rs +98 -3
- package/crates/team-agent/src/cli/status_port.rs +44 -6
- package/crates/team-agent/src/cli/tests/named_address.rs +135 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
- package/crates/team-agent/src/cli/types.rs +17 -0
- package/crates/team-agent/src/codex_app_server.rs +549 -0
- package/crates/team-agent/src/conpty/backend.rs +822 -0
- package/crates/team-agent/src/conpty/mod.rs +32 -0
- package/crates/team-agent/src/coordinator/backoff.rs +132 -7
- 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 +29 -10
- package/crates/team-agent/src/leader/lease.rs +144 -7
- package/crates/team-agent/src/leader/owner_bind.rs +5 -2
- package/crates/team-agent/src/leader/provider_attribution.rs +19 -34
- package/crates/team-agent/src/leader/start.rs +18 -5
- package/crates/team-agent/src/leader/tests/lease_api.rs +179 -0
- package/crates/team-agent/src/lib.rs +36 -0
- package/crates/team-agent/src/lifecycle/launch.rs +207 -94
- 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/restart/common.rs +70 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +63 -0
- package/crates/team-agent/src/mcp_server/wire.rs +6 -7
- package/crates/team-agent/src/messaging/delivery.rs +329 -9
- package/crates/team-agent/src/messaging/leader_receiver.rs +17 -138
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/results.rs +78 -21
- package/crates/team-agent/src/messaging/tests/runtime.rs +237 -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 +94 -13
- package/crates/team-agent/src/transport_factory.rs +751 -0
- 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.
|
|
563
|
-
///
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|