@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
|
@@ -302,3 +302,182 @@ fn r8_requeued_exhausted_watchers_event_payload_golden_shape() {
|
|
|
302
302
|
assert_eq!(payload.get("count").and_then(|v| v.as_u64()), Some(1), "count == number of requeued watchers");
|
|
303
303
|
assert_eq!(payload.get("trigger").and_then(|v| v.as_str()), Some("attach_leader"));
|
|
304
304
|
}
|
|
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)]
|
|
311
|
+
#[test]
|
|
312
|
+
fn app_server_attach_writes_transport_kind_tuple_and_advances_epoch() {
|
|
313
|
+
let ws = std::env::temp_dir().join(format!(
|
|
314
|
+
"ta_rs_app_attach_{}_{}",
|
|
315
|
+
std::process::id(),
|
|
316
|
+
chrono::Utc::now().timestamp_millis()
|
|
317
|
+
));
|
|
318
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
319
|
+
crate::state::persist::save_runtime_state(
|
|
320
|
+
&ws,
|
|
321
|
+
&serde_json::json!({
|
|
322
|
+
"active_team_key": "team-a",
|
|
323
|
+
"teams": {"team-a": {"team_key": "team-a", "agents": {}}},
|
|
324
|
+
}),
|
|
325
|
+
)
|
|
326
|
+
.unwrap();
|
|
327
|
+
let fake = crate::app_server_test_support::FakeAppServer::start(
|
|
328
|
+
"attach-ok",
|
|
329
|
+
crate::app_server_test_support::FakeAppServerScript::happy(
|
|
330
|
+
"thread-live",
|
|
331
|
+
"session-live",
|
|
332
|
+
ws.to_str().unwrap(),
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
let out = attach_app_server_leader(&ws, Some("team-a"), fake.endpoint(), "thread-live")
|
|
337
|
+
.expect("app-server attach should succeed");
|
|
338
|
+
|
|
339
|
+
assert_eq!(out["ok"], serde_json::json!(true));
|
|
340
|
+
assert_eq!(out["leader_receiver"]["transport_kind"], serde_json::json!("codex_app_server"));
|
|
341
|
+
assert_eq!(out["leader_receiver"]["mode"], serde_json::json!("codex_app_server"));
|
|
342
|
+
assert_eq!(
|
|
343
|
+
out["leader_receiver"]["app_server"]["thread_id"],
|
|
344
|
+
serde_json::json!("thread-live")
|
|
345
|
+
);
|
|
346
|
+
assert_eq!(
|
|
347
|
+
out["leader_receiver"]["app_server"]["session_id"],
|
|
348
|
+
serde_json::json!("session-live")
|
|
349
|
+
);
|
|
350
|
+
assert_eq!(
|
|
351
|
+
out["leader_receiver"]["app_server"]["cwd"],
|
|
352
|
+
serde_json::json!(ws.to_str().unwrap())
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
let saved = crate::state::projection::select_runtime_state(&ws, Some("team-a")).unwrap();
|
|
356
|
+
assert_eq!(
|
|
357
|
+
saved["leader_receiver"]["app_server"]["thread_id"],
|
|
358
|
+
serde_json::json!("thread-live")
|
|
359
|
+
);
|
|
360
|
+
assert_eq!(saved["leader_receiver"].get("pane_id"), None);
|
|
361
|
+
assert_eq!(saved["team_owner"]["transport_kind"], serde_json::json!("codex_app_server"));
|
|
362
|
+
assert_eq!(saved["owner_epoch"], serde_json::json!(1));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
#[cfg(unix)]
|
|
366
|
+
#[test]
|
|
367
|
+
fn app_server_attach_rejects_world_writable_socket_without_state_write() {
|
|
368
|
+
let ws = std::env::temp_dir().join(format!(
|
|
369
|
+
"ta_rs_app_attach_badmode_{}_{}",
|
|
370
|
+
std::process::id(),
|
|
371
|
+
chrono::Utc::now().timestamp_millis()
|
|
372
|
+
));
|
|
373
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
374
|
+
crate::state::persist::save_runtime_state(
|
|
375
|
+
&ws,
|
|
376
|
+
&serde_json::json!({"active_team_key": "team-a", "teams": {"team-a": {"agents": {}}}}),
|
|
377
|
+
)
|
|
378
|
+
.unwrap();
|
|
379
|
+
let fake = crate::app_server_test_support::FakeAppServer::start(
|
|
380
|
+
"attach-world",
|
|
381
|
+
crate::app_server_test_support::FakeAppServerScript::happy(
|
|
382
|
+
"thread-live",
|
|
383
|
+
"session-live",
|
|
384
|
+
ws.to_str().unwrap(),
|
|
385
|
+
),
|
|
386
|
+
);
|
|
387
|
+
let mut perms = std::fs::metadata(fake.path()).unwrap().permissions();
|
|
388
|
+
use std::os::unix::fs::PermissionsExt;
|
|
389
|
+
perms.set_mode(0o777);
|
|
390
|
+
std::fs::set_permissions(fake.path(), perms).unwrap();
|
|
391
|
+
|
|
392
|
+
let err = attach_app_server_leader(&ws, Some("team-a"), fake.endpoint(), "thread-live")
|
|
393
|
+
.expect_err("world-writable app-server socket must be rejected");
|
|
394
|
+
assert!(
|
|
395
|
+
err.to_string().contains("socket_ownership_invalid"),
|
|
396
|
+
"unexpected error: {err}"
|
|
397
|
+
);
|
|
398
|
+
let saved = crate::state::projection::select_runtime_state(&ws, Some("team-a")).unwrap();
|
|
399
|
+
assert!(
|
|
400
|
+
saved.get("leader_receiver").is_none(),
|
|
401
|
+
"failed attach must not write leader_receiver: {saved}"
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
#[cfg(unix)]
|
|
406
|
+
#[test]
|
|
407
|
+
fn app_server_attach_rejects_missing_user_agent_without_state_write() {
|
|
408
|
+
let ws = std::env::temp_dir().join(format!(
|
|
409
|
+
"ta_rs_app_attach_no_ua_{}_{}",
|
|
410
|
+
std::process::id(),
|
|
411
|
+
chrono::Utc::now().timestamp_millis()
|
|
412
|
+
));
|
|
413
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
414
|
+
crate::state::persist::save_runtime_state(
|
|
415
|
+
&ws,
|
|
416
|
+
&serde_json::json!({"active_team_key": "team-a", "teams": {"team-a": {"agents": {}}}}),
|
|
417
|
+
)
|
|
418
|
+
.unwrap();
|
|
419
|
+
let mut script = crate::app_server_test_support::FakeAppServerScript::happy(
|
|
420
|
+
"thread-live",
|
|
421
|
+
"session-live",
|
|
422
|
+
ws.to_str().unwrap(),
|
|
423
|
+
);
|
|
424
|
+
script.user_agent = None;
|
|
425
|
+
let fake = crate::app_server_test_support::FakeAppServer::start("attach-no-ua", script);
|
|
426
|
+
|
|
427
|
+
let err = attach_app_server_leader(&ws, Some("team-a"), fake.endpoint(), "thread-live")
|
|
428
|
+
.expect_err("missing initialize.userAgent must fail closed");
|
|
429
|
+
assert!(
|
|
430
|
+
err.to_string()
|
|
431
|
+
.contains("protocol_mismatch_missing_user_agent"),
|
|
432
|
+
"unexpected error: {err}"
|
|
433
|
+
);
|
|
434
|
+
let saved = crate::state::projection::select_runtime_state(&ws, Some("team-a")).unwrap();
|
|
435
|
+
assert!(
|
|
436
|
+
saved.get("leader_receiver").is_none(),
|
|
437
|
+
"failed attach must not write leader_receiver: {saved}"
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#[test]
|
|
442
|
+
fn app_server_delivery_paths_are_read_only_and_binding_entry_is_explicit() {
|
|
443
|
+
let delivery = include_str!("../../messaging/delivery.rs");
|
|
444
|
+
assert!(
|
|
445
|
+
!delivery.contains("write_owner(")
|
|
446
|
+
&& !delivery.contains("with_leader_receiver(")
|
|
447
|
+
&& !delivery.contains("with_team_owner("),
|
|
448
|
+
"MUST-12/I-RN-1: delivery may read typed leader_receiver fields but must not write ownership"
|
|
449
|
+
);
|
|
450
|
+
let lease = include_str!("../lease.rs");
|
|
451
|
+
assert!(
|
|
452
|
+
lease.contains("fn write_leader_receiver_transport("),
|
|
453
|
+
"C-5: mode and transport_kind must be stamped by a single receiver transport helper"
|
|
454
|
+
);
|
|
455
|
+
let cli = include_str!("../../cli/attach_app_server_leader.rs");
|
|
456
|
+
assert!(
|
|
457
|
+
cli.contains("attach_app_server_leader("),
|
|
458
|
+
"I-RN-2: app-server ownership mutation must be reachable through the explicit CLI entry"
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#[test]
|
|
463
|
+
fn messaging_leader_receiver_module_exports_no_ownership_writer() {
|
|
464
|
+
let leader_receiver = include_str!("../../messaging/leader_receiver.rs");
|
|
465
|
+
for forbidden in [
|
|
466
|
+
"pub fn claim_leader_receiver",
|
|
467
|
+
"write_owner(",
|
|
468
|
+
"with_leader_receiver(",
|
|
469
|
+
"with_team_owner(",
|
|
470
|
+
"save_runtime_state",
|
|
471
|
+
] {
|
|
472
|
+
assert!(
|
|
473
|
+
!leader_receiver.contains(forbidden),
|
|
474
|
+
"MUST-12/I-RN-1: messaging/leader_receiver.rs may read receiver fields but must not expose ownership writes; forbidden={forbidden}"
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
let messaging_mod = include_str!("../../messaging/mod.rs");
|
|
479
|
+
assert!(
|
|
480
|
+
!messaging_mod.contains("claim_leader_receiver"),
|
|
481
|
+
"MUST-12/I-RN-2: messaging/mod.rs must not re-export ownership writer APIs"
|
|
482
|
+
);
|
|
483
|
+
}
|
|
@@ -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;
|
|
@@ -92,4 +99,33 @@ pub mod fake_worker;
|
|
|
92
99
|
// tmux_backend — concrete tmux Transport executor (Command::new tmux via a CommandRunner seam);
|
|
93
100
|
// the real spawn/capture/inject/has_session backend the daemon + launch use (step 9 shipped only
|
|
94
101
|
// the trait + argv-builders). Real subprocess execution is the #[ignore] real-machine boundary.
|
|
102
|
+
pub mod codex_app_server;
|
|
95
103
|
pub mod tmux_backend;
|
|
104
|
+
|
|
105
|
+
// 0.5.x Windows-native transport Phase 1: ConPTY backend + named-pipe
|
|
106
|
+
// protocol. `protocol` is portable (pure logic, tested on all hosts);
|
|
107
|
+
// `backend` compiles on all hosts and returns typed MuxUnavailable when
|
|
108
|
+
// no pipe client is wired (honest degradation, not silent success).
|
|
109
|
+
//
|
|
110
|
+
// Truth source:
|
|
111
|
+
// - Design: `.team/artifacts/0.5.x-windows-native-transport-design.md`
|
|
112
|
+
// §Transport Boundary + §Named Pipe Control Protocol + §Phase 1
|
|
113
|
+
// - CR verdict: `.team/artifacts/0.5.x-windows-transport-cr-verdict.md`
|
|
114
|
+
// (7 constraints; C-1/C-2/C-3/C-5 anchor into this module)
|
|
115
|
+
pub mod conpty;
|
|
116
|
+
|
|
117
|
+
// 0.5.x Phase 1d Batch 0: backend assembly factory.
|
|
118
|
+
// Truth sources:
|
|
119
|
+
// - Design: `.team/artifacts/0.5.x-backend-assembly-factory-design.md`
|
|
120
|
+
// - CR verdict: `.team/artifacts/0.5.x-backend-factory-cr-verdict.md`
|
|
121
|
+
// (6 constraints anchored inside the module doc)
|
|
122
|
+
pub mod transport_factory;
|
|
123
|
+
|
|
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))]
|
|
131
|
+
pub(crate) mod app_server_test_support;
|
|
@@ -2835,16 +2835,161 @@ pub fn quick_start_in_workspace_with_display(
|
|
|
2835
2835
|
yes: bool,
|
|
2836
2836
|
team_id: Option<&str>,
|
|
2837
2837
|
open_display: bool,
|
|
2838
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
2839
|
+
// Legacy entrypoint: no `--backend` override. Preserves the exact
|
|
2840
|
+
// tmux behavior we shipped in Phase 1c (byte-equivalent tmux path
|
|
2841
|
+
// when no explicit backend is asked for).
|
|
2842
|
+
quick_start_in_workspace_with_display_and_backend(
|
|
2843
|
+
workspace,
|
|
2844
|
+
agents_dir,
|
|
2845
|
+
name,
|
|
2846
|
+
yes,
|
|
2847
|
+
team_id,
|
|
2848
|
+
open_display,
|
|
2849
|
+
None,
|
|
2850
|
+
)
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
/// 0.5.x Phase 1d Batch 2: quick-start with an optional
|
|
2854
|
+
/// `--backend <tmux|conpty>` override. When `backend` is `None` or
|
|
2855
|
+
/// `Some("tmux")` the transport is the same one the legacy entrypoint
|
|
2856
|
+
/// built (byte-equivalent to Phase 1c), so tmux users see no
|
|
2857
|
+
/// behavioral change.
|
|
2858
|
+
///
|
|
2859
|
+
/// When `backend` is `Some("conpty")`, this call routes through the
|
|
2860
|
+
/// factory. On a host without a live shim client (i.e. every
|
|
2861
|
+
/// non-Windows host today), the resulting `ConPtyBackend` degrades
|
|
2862
|
+
/// its spawn/inject/capture calls to `TransportError::MuxUnavailable`
|
|
2863
|
+
/// honestly — MUST-NOT-13 + CR C-1 ①. Users see a real "conpty
|
|
2864
|
+
/// unavailable" error rather than a silent tmux fallback.
|
|
2865
|
+
pub fn quick_start_in_workspace_with_display_and_backend(
|
|
2866
|
+
workspace: &Path,
|
|
2867
|
+
agents_dir: &Path,
|
|
2868
|
+
name: Option<&str>,
|
|
2869
|
+
yes: bool,
|
|
2870
|
+
team_id: Option<&str>,
|
|
2871
|
+
open_display: bool,
|
|
2872
|
+
backend: Option<&str>,
|
|
2838
2873
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
2839
2874
|
let workspace = explicit_quick_start_workspace(workspace);
|
|
2840
|
-
|
|
2875
|
+
// Default / `tmux` literal: preserve the existing tmux path
|
|
2876
|
+
// BYTE-FOR-BYTE. This is the CR §Batch 2 Verification anchor:
|
|
2877
|
+
// `quick-start` without `--backend` produces byte-equivalent tmux
|
|
2878
|
+
// behavior.
|
|
2879
|
+
let literal = backend.map(str::trim);
|
|
2880
|
+
let is_tmux_or_default = matches!(literal, None | Some("") | Some("tmux") | Some("TMUX"));
|
|
2881
|
+
if is_tmux_or_default {
|
|
2882
|
+
let transport = quick_start_tmux_backend(&workspace);
|
|
2883
|
+
return quick_start_with_transport_in_workspace_with_display(
|
|
2884
|
+
&workspace,
|
|
2885
|
+
agents_dir,
|
|
2886
|
+
name,
|
|
2887
|
+
yes,
|
|
2888
|
+
team_id,
|
|
2889
|
+
&transport,
|
|
2890
|
+
open_display,
|
|
2891
|
+
);
|
|
2892
|
+
}
|
|
2893
|
+
// Explicit non-tmux backend: route through the factory. Parse the
|
|
2894
|
+
// literal, then delegate. On unsupported literals or refused
|
|
2895
|
+
// preconditions we return `LifecycleError` — never silently pick
|
|
2896
|
+
// tmux (CR C-1 ①/②).
|
|
2897
|
+
let requested = crate::transport_factory::RequestedTransportBackend::parse_literal(
|
|
2898
|
+
literal.unwrap_or_default(),
|
|
2899
|
+
)
|
|
2900
|
+
.ok_or_else(|| {
|
|
2901
|
+
LifecycleError::TeamSelect(format!(
|
|
2902
|
+
"unsupported --backend literal {literal:?}; expected `tmux` or `conpty` \
|
|
2903
|
+
(Phase 1d does not auto-map `pty` to conpty — CR C-1 ②)"
|
|
2904
|
+
))
|
|
2905
|
+
})?;
|
|
2906
|
+
let team_key_for_factory = team_id;
|
|
2907
|
+
// 0.5.x Windows portability Batch 8 F7 (leader msg_590b4dce0f68):
|
|
2908
|
+
// shim ownership moves to the coordinator. quick-start no longer
|
|
2909
|
+
// calls `spawn_shim_and_handshake` directly. Instead we ensure
|
|
2910
|
+
// the coordinator daemon is running; the coordinator's boot
|
|
2911
|
+
// path calls `conpty_shim::ensure_shim_running` (idempotent —
|
|
2912
|
+
// spawns if no live shim, reconnects if one is recorded).
|
|
2913
|
+
//
|
|
2914
|
+
// Rationale (from Batch 7 gate report F7): quick-start is a
|
|
2915
|
+
// one-shot process; if it owns the shim, the shim dies when
|
|
2916
|
+
// quick-start exits. Moving ownership to the coord daemon
|
|
2917
|
+
// gives us the "coord can die, shim survives" invariant the
|
|
2918
|
+
// design's §Shim Lifecycle chapter requires.
|
|
2919
|
+
//
|
|
2920
|
+
// The `ensure_coordinator_running` call is idempotent per
|
|
2921
|
+
// `coordinator/health.rs::start_coordinator`. On non-Windows
|
|
2922
|
+
// this whole block is cfg'd out.
|
|
2923
|
+
#[cfg(windows)]
|
|
2924
|
+
if matches!(
|
|
2925
|
+
requested,
|
|
2926
|
+
crate::transport_factory::RequestedTransportBackend::ConPty
|
|
2927
|
+
) {
|
|
2928
|
+
let team_key_str = team_key_for_factory.ok_or_else(|| {
|
|
2929
|
+
LifecycleError::TeamSelect(
|
|
2930
|
+
"team_key required for --backend conpty on Windows (Batch 9 F8)".to_string(),
|
|
2931
|
+
)
|
|
2932
|
+
})?;
|
|
2933
|
+
// 0.5.x Windows portability Batch 9 F8 (leader msg_2a4cc1fa54c0):
|
|
2934
|
+
// Batch 8's seed-state pattern (writing active_team_key +
|
|
2935
|
+
// transport.kind to state.json before start_coordinator)
|
|
2936
|
+
// caused downstream launch code to see "existing runtime, use
|
|
2937
|
+
// restart" and skip spec compile. F8 fix: pass `--team`
|
|
2938
|
+
// directly to the coord daemon via `start_coordinator_with_team`
|
|
2939
|
+
// so state doesn't need pre-seeding.
|
|
2940
|
+
//
|
|
2941
|
+
// The coord daemon's `run_daemon_with_coordinator_and_boot_tmux`
|
|
2942
|
+
// then calls `ensure_shim_running` with the CLI-supplied
|
|
2943
|
+
// team_key. When quick-start's downstream code runs, state.json
|
|
2944
|
+
// still has whatever it had before (empty on fresh launch), so
|
|
2945
|
+
// the spec-compile + worker-spawn path runs normally.
|
|
2946
|
+
let run_ws = crate::coordinator::WorkspacePath::new(workspace.clone());
|
|
2947
|
+
let start_report =
|
|
2948
|
+
crate::coordinator::health::start_coordinator_with_team(&run_ws, Some(team_key_str))
|
|
2949
|
+
.map_err(|e| LifecycleError::TeamSelect(format!("coordinator start: {e}")))?;
|
|
2950
|
+
if !start_report.ok {
|
|
2951
|
+
return Err(LifecycleError::TeamSelect(format!(
|
|
2952
|
+
"coordinator start failed: schema_error={:?}, action={:?}",
|
|
2953
|
+
start_report.schema_error, start_report.action
|
|
2954
|
+
)));
|
|
2955
|
+
}
|
|
2956
|
+
// Give the coordinator a beat to write its transport.shim
|
|
2957
|
+
// block so the factory's `pipe_ready` gate opens on the
|
|
2958
|
+
// next resolve. The coordinator's `run_daemon` calls
|
|
2959
|
+
// `ensure_shim_running` inside its boot code path (see
|
|
2960
|
+
// `coordinator::backoff::run_daemon`).
|
|
2961
|
+
std::thread::sleep(std::time::Duration::from_millis(2500));
|
|
2962
|
+
}
|
|
2963
|
+
let input = crate::transport_factory::TransportFactoryInput::new(
|
|
2964
|
+
&workspace,
|
|
2965
|
+
crate::transport_factory::TransportPurpose::Launch,
|
|
2966
|
+
)
|
|
2967
|
+
.with_team_key(team_key_for_factory)
|
|
2968
|
+
.with_explicit_backend(Some(requested));
|
|
2969
|
+
// On Windows the factory needs to SEE the freshly-persisted
|
|
2970
|
+
// `state.transport.shim.pipe_ready = true` marker so its
|
|
2971
|
+
// `conpty_pipe_ready` gate opens.
|
|
2972
|
+
#[cfg(windows)]
|
|
2973
|
+
let state_value = std::fs::read_to_string(crate::state::persist::runtime_state_path(
|
|
2974
|
+
&workspace,
|
|
2975
|
+
))
|
|
2976
|
+
.ok()
|
|
2977
|
+
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok());
|
|
2978
|
+
#[cfg(windows)]
|
|
2979
|
+
let input = match state_value.as_ref() {
|
|
2980
|
+
Some(v) => input.with_state(Some(v)),
|
|
2981
|
+
None => input,
|
|
2982
|
+
};
|
|
2983
|
+
let resolved = crate::transport_factory::resolve_transport(input)
|
|
2984
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
2985
|
+
// Hand the boxed backend down as `&dyn Transport`.
|
|
2841
2986
|
quick_start_with_transport_in_workspace_with_display(
|
|
2842
2987
|
&workspace,
|
|
2843
2988
|
agents_dir,
|
|
2844
2989
|
name,
|
|
2845
2990
|
yes,
|
|
2846
2991
|
team_id,
|
|
2847
|
-
|
|
2992
|
+
&*resolved.backend,
|
|
2848
2993
|
open_display,
|
|
2849
2994
|
)
|
|
2850
2995
|
}
|
|
@@ -3068,7 +3213,16 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
3068
3213
|
let resolved_spec_path =
|
|
3069
3214
|
std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
|
|
3070
3215
|
let mut state = initial_runtime_state(&spec, &resolved_spec_path, &workspace, agents_dir);
|
|
3071
|
-
|
|
3216
|
+
// 0.5.x Phase 1d hot-path 接线(裁决1 msg_76e1d98202b8): use the
|
|
3217
|
+
// generic annotator that writes `state.transport = { kind, source }`
|
|
3218
|
+
// for every backend AND (for tmux) preserves the existing
|
|
3219
|
+
// `tmux_endpoint`/`tmux_socket`/`tmux_socket_source` fields via the
|
|
3220
|
+
// inner `annotate_runtime_tmux_endpoint` call. Source is threaded
|
|
3221
|
+
// from the caller-side factory selection when available; the
|
|
3222
|
+
// legacy quick-start entrypoint currently passes `None` (defaults
|
|
3223
|
+
// to "unknown" in the payload) — Phase 2 refactor will thread
|
|
3224
|
+
// ResolvedTransport.source through the launch signature.
|
|
3225
|
+
annotate_runtime_transport(&mut state, transport, &workspace, None);
|
|
3072
3226
|
save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
|
|
3073
3227
|
annotate_persisted_team_depth(
|
|
3074
3228
|
&workspace,
|
|
@@ -3164,6 +3318,46 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
3164
3318
|
/// the persisted endpoint synchronized with the transport they actually used,
|
|
3165
3319
|
/// closing the silent socket-drift gap** (single state-save path; no parallel
|
|
3166
3320
|
/// "annotate after spawn" race with coordinator).
|
|
3321
|
+
/// 0.5.x Phase 1d Batch 2: generic runtime-transport annotator.
|
|
3322
|
+
///
|
|
3323
|
+
/// Writes `state.transport = { kind, source }` for every backend
|
|
3324
|
+
/// (kind = wire string `"tmux"` | `"conpty"`; source = wire string
|
|
3325
|
+
/// from `ResolvedTransport.source` when known, else `"unknown"`).
|
|
3326
|
+
///
|
|
3327
|
+
/// For tmux, ALSO forwards to `annotate_runtime_tmux_endpoint` so the
|
|
3328
|
+
/// existing `tmux_endpoint` / `tmux_socket` / `tmux_socket_source`
|
|
3329
|
+
/// fields remain populated byte-equivalent to today's shape.
|
|
3330
|
+
///
|
|
3331
|
+
/// The tmux-specific fields are NOT written for ConPTY (CR C-4: no
|
|
3332
|
+
/// tmux-only fields under a conpty state; keeps compact status
|
|
3333
|
+
/// stable). ConPTY discovery fields (`pipe_name`, `shim_pid`) are
|
|
3334
|
+
/// added in Batch 3 when the shim boot path lands; this function just
|
|
3335
|
+
/// pins the top-level `transport.kind`/`source`.
|
|
3336
|
+
pub fn annotate_runtime_transport(
|
|
3337
|
+
state: &mut serde_json::Value,
|
|
3338
|
+
transport: &dyn Transport,
|
|
3339
|
+
workspace: &Path,
|
|
3340
|
+
source: Option<&str>,
|
|
3341
|
+
) {
|
|
3342
|
+
use crate::transport::BackendKind;
|
|
3343
|
+
let kind_wire = match transport.kind() {
|
|
3344
|
+
BackendKind::Tmux => "tmux",
|
|
3345
|
+
BackendKind::WezTerm => "wezterm",
|
|
3346
|
+
BackendKind::ConPty => "conpty",
|
|
3347
|
+
};
|
|
3348
|
+
if let Some(obj) = state.as_object_mut() {
|
|
3349
|
+
let transport_block = serde_json::json!({
|
|
3350
|
+
"kind": kind_wire,
|
|
3351
|
+
"source": source.unwrap_or("unknown"),
|
|
3352
|
+
});
|
|
3353
|
+
obj.insert("transport".to_string(), transport_block);
|
|
3354
|
+
}
|
|
3355
|
+
// Tmux: preserve the existing tmux endpoint annotation shape.
|
|
3356
|
+
if matches!(transport.kind(), BackendKind::Tmux) {
|
|
3357
|
+
annotate_runtime_tmux_endpoint(state, transport, workspace);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3167
3361
|
pub(crate) fn annotate_runtime_tmux_endpoint(
|
|
3168
3362
|
state: &mut serde_json::Value,
|
|
3169
3363
|
transport: &dyn Transport,
|
|
@@ -3520,102 +3714,21 @@ fn process_ancestry_argv(pid: u32) -> Vec<Vec<String>> {
|
|
|
3520
3714
|
out
|
|
3521
3715
|
}
|
|
3522
3716
|
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
(!argv_tokens.is_empty()).then_some(argv_tokens)
|
|
3532
|
-
}
|
|
3717
|
+
// 0.5.x Windows portability Batch 4: `process_argv_tokens` and
|
|
3718
|
+
// `process_parent_pid` route through `crate::platform::argv`. Unix
|
|
3719
|
+
// impls are byte-preserving (Linux `/proc/<pid>/cmdline`, macOS
|
|
3720
|
+
// `sysctl(KERN_PROCARGS2)`, other Unix `ps -p ... -o command=`).
|
|
3721
|
+
// Windows returns `None` per design §Batch 4 conservative anchor:
|
|
3722
|
+
// unknown argv must never infer elevated approval; callers already
|
|
3723
|
+
// treat `None` as "no elevation inherited" via
|
|
3724
|
+
// `disabled_dangerous_approval()`.
|
|
3533
3725
|
|
|
3534
|
-
#[cfg(target_os = "macos")]
|
|
3535
3726
|
fn process_argv_tokens(pid: u32) -> Option<Vec<String>> {
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
let mut mib = [
|
|
3539
|
-
libc::CTL_KERN,
|
|
3540
|
-
libc::KERN_PROCARGS2,
|
|
3541
|
-
i32::try_from(pid).ok()?,
|
|
3542
|
-
];
|
|
3543
|
-
let mut size = 0usize;
|
|
3544
|
-
let rc = unsafe {
|
|
3545
|
-
libc::sysctl(
|
|
3546
|
-
mib.as_mut_ptr(),
|
|
3547
|
-
mib.len() as u32,
|
|
3548
|
-
std::ptr::null_mut(),
|
|
3549
|
-
&mut size,
|
|
3550
|
-
std::ptr::null_mut(),
|
|
3551
|
-
0,
|
|
3552
|
-
)
|
|
3553
|
-
};
|
|
3554
|
-
if rc != 0 || size <= size_of::<libc::c_int>() {
|
|
3555
|
-
return None;
|
|
3556
|
-
}
|
|
3557
|
-
let mut buf = vec![0u8; size];
|
|
3558
|
-
let rc = unsafe {
|
|
3559
|
-
libc::sysctl(
|
|
3560
|
-
mib.as_mut_ptr(),
|
|
3561
|
-
mib.len() as u32,
|
|
3562
|
-
buf.as_mut_ptr().cast(),
|
|
3563
|
-
&mut size,
|
|
3564
|
-
std::ptr::null_mut(),
|
|
3565
|
-
0,
|
|
3566
|
-
)
|
|
3567
|
-
};
|
|
3568
|
-
if rc != 0 || size <= size_of::<libc::c_int>() {
|
|
3569
|
-
return None;
|
|
3570
|
-
}
|
|
3571
|
-
let argc = i32::from_ne_bytes(buf.get(..size_of::<libc::c_int>())?.try_into().ok()?) as usize;
|
|
3572
|
-
let mut offset = size_of::<libc::c_int>();
|
|
3573
|
-
while offset < size && buf[offset] != 0 {
|
|
3574
|
-
offset += 1;
|
|
3575
|
-
}
|
|
3576
|
-
while offset < size && buf[offset] == 0 {
|
|
3577
|
-
offset += 1;
|
|
3578
|
-
}
|
|
3579
|
-
let raw = String::from_utf8_lossy(&buf[offset..size]);
|
|
3580
|
-
let argv_tokens = raw
|
|
3581
|
-
.split('\0')
|
|
3582
|
-
.filter(|token| !token.is_empty())
|
|
3583
|
-
.take(argc)
|
|
3584
|
-
.map(str::to_string)
|
|
3585
|
-
.collect::<Vec<_>>();
|
|
3586
|
-
(!argv_tokens.is_empty()).then_some(argv_tokens)
|
|
3587
|
-
}
|
|
3588
|
-
|
|
3589
|
-
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
|
3590
|
-
fn process_argv_tokens(pid: u32) -> Option<Vec<String>> {
|
|
3591
|
-
let output = Command::new("ps")
|
|
3592
|
-
.args(["-p", &pid.to_string(), "-o", "command="])
|
|
3593
|
-
.output()
|
|
3594
|
-
.ok()?;
|
|
3595
|
-
if !output.status.success() {
|
|
3596
|
-
return None;
|
|
3597
|
-
}
|
|
3598
|
-
let text = String::from_utf8_lossy(&output.stdout);
|
|
3599
|
-
let argv_tokens = text
|
|
3600
|
-
.split_whitespace()
|
|
3601
|
-
.filter(|token| !token.is_empty())
|
|
3602
|
-
.map(str::to_string)
|
|
3603
|
-
.collect::<Vec<_>>();
|
|
3604
|
-
(!argv_tokens.is_empty()).then_some(argv_tokens)
|
|
3727
|
+
crate::platform::argv::argv_tokens(pid)
|
|
3605
3728
|
}
|
|
3606
3729
|
|
|
3607
3730
|
fn process_parent_pid(pid: u32) -> Option<u32> {
|
|
3608
|
-
|
|
3609
|
-
.args(["-p", &pid.to_string(), "-o", "ppid="])
|
|
3610
|
-
.output()
|
|
3611
|
-
.ok()?;
|
|
3612
|
-
if !output.status.success() {
|
|
3613
|
-
return None;
|
|
3614
|
-
}
|
|
3615
|
-
String::from_utf8_lossy(&output.stdout)
|
|
3616
|
-
.trim()
|
|
3617
|
-
.parse::<u32>()
|
|
3618
|
-
.ok()
|
|
3731
|
+
crate::platform::argv::parent_pid(pid)
|
|
3619
3732
|
}
|
|
3620
3733
|
|
|
3621
3734
|
/// `add_agent(workspace, agent_id, role_file_path, open_display, team)`
|
|
@@ -85,6 +85,16 @@ fn acquire_agent_lifecycle_lock_with_deadlines(
|
|
|
85
85
|
timeout: Duration,
|
|
86
86
|
held_long: Duration,
|
|
87
87
|
) -> Result<LifecycleLockGuard, LifecycleError> {
|
|
88
|
+
// 0.5.x Windows portability Batch 2: migrated to
|
|
89
|
+
// `crate::platform::file_lock::{try_lock_once_nonblocking, unlock}`
|
|
90
|
+
// so the same polling loop + waiter file + 5s `lock_held_long`
|
|
91
|
+
// event + 30s N38 timeout error shape works on both Unix (`flock`)
|
|
92
|
+
// and Windows (`LockFileEx`). The Batch 0 non-Unix stub that
|
|
93
|
+
// returned `lock_timeout_error` unconditionally is now gone —
|
|
94
|
+
// Windows callers get real lock behavior. Byte-preserving on Unix:
|
|
95
|
+
// the polling cadence, waiter file writes, `lock_held_long_event`
|
|
96
|
+
// emission, and error shape are all unchanged relative to the
|
|
97
|
+
// pre-Batch-2 unix branch.
|
|
88
98
|
let lock_path = agent_lifecycle_lock_path(request.workspace);
|
|
89
99
|
if let Some(parent) = lock_path.parent() {
|
|
90
100
|
std::fs::create_dir_all(parent)
|
|
@@ -100,42 +110,38 @@ fn acquire_agent_lifecycle_lock_with_deadlines(
|
|
|
100
110
|
let started = Instant::now();
|
|
101
111
|
let mut long_event_written = false;
|
|
102
112
|
let mut waiter = None;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
let fd = file.as_raw_fd();
|
|
107
|
-
loop {
|
|
108
|
-
let rc = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
|
|
109
|
-
if rc == 0 {
|
|
113
|
+
loop {
|
|
114
|
+
match crate::platform::file_lock::try_lock_once_nonblocking(&file) {
|
|
115
|
+
Ok(true) => {
|
|
110
116
|
write_lock_metadata(&mut file, &request, &lock_path)?;
|
|
111
117
|
return Ok(LifecycleLockGuard { file });
|
|
112
118
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
Ok(false) => {}
|
|
120
|
+
Err(e) => {
|
|
121
|
+
return Err(LifecycleError::StatePersist(format!(
|
|
122
|
+
"lifecycle lock acquire io error at {}: {e}",
|
|
123
|
+
lock_path.display()
|
|
124
|
+
)));
|
|
116
125
|
}
|
|
117
|
-
if let Some(waiter) = waiter.as_ref() {
|
|
118
|
-
let _ = waiter.write(&request, elapsed);
|
|
119
|
-
}
|
|
120
|
-
if !long_event_written && elapsed >= held_long {
|
|
121
|
-
let _ =
|
|
122
|
-
write_lock_held_long_event(&request, &lock_path, elapsed, held_long, timeout);
|
|
123
|
-
long_event_written = true;
|
|
124
|
-
}
|
|
125
|
-
if elapsed >= timeout {
|
|
126
|
-
return Err(lock_timeout_error(&request, &lock_path, elapsed));
|
|
127
|
-
}
|
|
128
|
-
std::thread::sleep(std::cmp::min(
|
|
129
|
-
Duration::from_millis(50),
|
|
130
|
-
timeout.saturating_sub(elapsed),
|
|
131
|
-
));
|
|
132
126
|
}
|
|
133
|
-
}
|
|
134
|
-
#[cfg(not(unix))]
|
|
135
|
-
{
|
|
136
|
-
let _ = file;
|
|
137
127
|
let elapsed = started.elapsed();
|
|
138
|
-
|
|
128
|
+
if waiter.is_none() {
|
|
129
|
+
waiter = WaiterFile::create(&request, &lock_path).ok();
|
|
130
|
+
}
|
|
131
|
+
if let Some(waiter) = waiter.as_ref() {
|
|
132
|
+
let _ = waiter.write(&request, elapsed);
|
|
133
|
+
}
|
|
134
|
+
if !long_event_written && elapsed >= held_long {
|
|
135
|
+
let _ = write_lock_held_long_event(&request, &lock_path, elapsed, held_long, timeout);
|
|
136
|
+
long_event_written = true;
|
|
137
|
+
}
|
|
138
|
+
if elapsed >= timeout {
|
|
139
|
+
return Err(lock_timeout_error(&request, &lock_path, elapsed));
|
|
140
|
+
}
|
|
141
|
+
std::thread::sleep(std::cmp::min(
|
|
142
|
+
Duration::from_millis(50),
|
|
143
|
+
timeout.saturating_sub(elapsed),
|
|
144
|
+
));
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
|
|
@@ -293,10 +299,11 @@ impl Drop for WaiterFile {
|
|
|
293
299
|
}
|
|
294
300
|
}
|
|
295
301
|
|
|
296
|
-
#[cfg(unix)]
|
|
297
302
|
impl Drop for LifecycleLockGuard {
|
|
298
303
|
fn drop(&mut self) {
|
|
299
|
-
|
|
300
|
-
|
|
304
|
+
// Batch 2: unlock via platform primitive. Best-effort — OS
|
|
305
|
+
// releases when the file handle closes anyway. Uniform on
|
|
306
|
+
// both `flock(LOCK_UN)` (unix) and `UnlockFileEx` (windows).
|
|
307
|
+
let _ = crate::platform::file_lock::unlock(&self.file);
|
|
301
308
|
}
|
|
302
309
|
}
|