@team-agent/installer 0.5.77 → 0.5.78
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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +161 -29
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +24 -8
- package/crates/team-agent/src/cli/mod.rs +543 -98
- package/crates/team-agent/src/cli/models.rs +362 -32
- package/crates/team-agent/src/cli/named_address.rs +19 -5
- package/crates/team-agent/src/cli/profile.rs +35 -4
- package/crates/team-agent/src/cli/spec.rs +7 -2
- package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
- package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
- package/crates/team-agent/src/cli/status_port/store.rs +1 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +492 -33
- package/crates/team-agent/src/leader/owner_bind.rs +40 -12
- package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
- package/crates/team-agent/src/leader/rediscover.rs +36 -6
- package/crates/team-agent/src/leader/start.rs +151 -14
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +65 -18
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
- package/crates/team-agent/src/lifecycle/launch.rs +30 -7
- package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
- package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
- package/crates/team-agent/src/lifecycle/restart.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +140 -11
- package/crates/team-agent/src/messaging/delivery.rs +67 -8
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/state/persist.rs +97 -1
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
- package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
- package/crates/team-agent/src/tmux_backend.rs +208 -118
- package/crates/team-agent/src/transport.rs +72 -0
- package/crates/team-agent/src/transport_factory.rs +53 -0
- package/package.json +4 -5
- package/skills/team-agent/SKILL.md +45 -17
- package/skills/team-agent/command-coverage.json +4 -29
- package/npm/check-skill-links.mjs +0 -88
- package/skills/team-agent/references/team-agent-operator.md +0 -672
|
@@ -136,8 +136,15 @@ fn run_catalog(program: &Path, timeout: Duration, max_bytes: u64) -> Result<Vec<
|
|
|
136
136
|
#[cfg(test)]
|
|
137
137
|
mod tests {
|
|
138
138
|
use super::*;
|
|
139
|
+
#[cfg(unix)]
|
|
140
|
+
use std::io::Write;
|
|
141
|
+
#[cfg(unix)]
|
|
142
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
139
143
|
use std::time::Instant;
|
|
140
144
|
|
|
145
|
+
#[cfg(unix)]
|
|
146
|
+
static FIXTURE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
|
147
|
+
|
|
141
148
|
#[cfg(unix)]
|
|
142
149
|
#[test]
|
|
143
150
|
fn real_dispatcher_child_helper() {
|
|
@@ -244,12 +251,13 @@ mod tests {
|
|
|
244
251
|
#[cfg(unix)]
|
|
245
252
|
fn fixture(body: &str) -> std::path::PathBuf {
|
|
246
253
|
use std::os::unix::fs::PermissionsExt;
|
|
254
|
+
let sequence = FIXTURE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
|
|
247
255
|
let root = std::env::current_exe()
|
|
248
256
|
.unwrap()
|
|
249
257
|
.parent()
|
|
250
258
|
.unwrap()
|
|
251
259
|
.join(format!(
|
|
252
|
-
"team-agent-pi-fixture-{}-{}",
|
|
260
|
+
"team-agent-pi-fixture-{}-{}-{sequence}",
|
|
253
261
|
std::process::id(),
|
|
254
262
|
std::time::SystemTime::now()
|
|
255
263
|
.duration_since(std::time::UNIX_EPOCH)
|
|
@@ -269,6 +277,18 @@ mod tests {
|
|
|
269
277
|
path
|
|
270
278
|
}
|
|
271
279
|
|
|
280
|
+
#[cfg(unix)]
|
|
281
|
+
fn fixture_with_sidecars(body: &str) -> std::path::PathBuf {
|
|
282
|
+
let path = fixture(body);
|
|
283
|
+
let script = std::fs::read_to_string(&path).unwrap();
|
|
284
|
+
let script = script.strip_prefix("#!/bin/sh\n").unwrap();
|
|
285
|
+
let script = format!(
|
|
286
|
+
"#!/bin/sh\nprintf '1\\n' > \"$0.entered\" || :\n{script}body_status=$?\nprintf '1\\n' > \"$0.completed\" || :\nexit \"$body_status\"\n"
|
|
287
|
+
);
|
|
288
|
+
std::fs::write(&path, script).unwrap();
|
|
289
|
+
path
|
|
290
|
+
}
|
|
291
|
+
|
|
272
292
|
#[cfg(unix)]
|
|
273
293
|
fn cleanup_fixture(path: &Path) {
|
|
274
294
|
let _ = std::fs::remove_dir_all(path.parent().unwrap());
|
|
@@ -294,15 +314,22 @@ mod tests {
|
|
|
294
314
|
}
|
|
295
315
|
|
|
296
316
|
#[cfg(unix)]
|
|
297
|
-
fn
|
|
317
|
+
fn native_timeout_stage_receipt(receipt: &Path) -> std::path::PathBuf {
|
|
318
|
+
receipt.with_extension("stage")
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#[cfg(unix)]
|
|
322
|
+
fn parse_native_timeout_receipt(path: &Path) -> Option<BTreeMap<String, String>> {
|
|
298
323
|
let mut fields = BTreeMap::new();
|
|
299
|
-
for line in std::fs::read_to_string(path).
|
|
300
|
-
let (key, value) = line.split_once('=')
|
|
301
|
-
|
|
324
|
+
for line in std::fs::read_to_string(path).ok()?.lines() {
|
|
325
|
+
let (key, value) = line.split_once('=')?;
|
|
326
|
+
if fields.insert(key.to_string(), value.to_string()).is_some() {
|
|
327
|
+
return None;
|
|
328
|
+
}
|
|
302
329
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
vec![
|
|
330
|
+
let keys = fields.keys().map(String::as_str).collect::<Vec<_>>();
|
|
331
|
+
(keys
|
|
332
|
+
== vec![
|
|
306
333
|
"child_argv_exact",
|
|
307
334
|
"child_pid",
|
|
308
335
|
"child_spawn_attempts",
|
|
@@ -312,9 +339,22 @@ mod tests {
|
|
|
312
339
|
"parent_argv_exact",
|
|
313
340
|
"parent_pid",
|
|
314
341
|
"parent_started_once",
|
|
315
|
-
]
|
|
316
|
-
)
|
|
317
|
-
|
|
342
|
+
])
|
|
343
|
+
.then_some(fields)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
#[cfg(unix)]
|
|
347
|
+
fn wait_native_timeout_receipt(path: &Path) -> BTreeMap<String, String> {
|
|
348
|
+
let deadline = Instant::now() + Duration::from_millis(500);
|
|
349
|
+
loop {
|
|
350
|
+
if let Some(fields) = parse_native_timeout_receipt(path) {
|
|
351
|
+
return fields;
|
|
352
|
+
}
|
|
353
|
+
if Instant::now() >= deadline {
|
|
354
|
+
panic!("native timeout receipt did not become complete after runner return");
|
|
355
|
+
}
|
|
356
|
+
std::thread::sleep(Duration::from_millis(1));
|
|
357
|
+
}
|
|
318
358
|
}
|
|
319
359
|
|
|
320
360
|
#[cfg(unix)]
|
|
@@ -334,15 +374,112 @@ mod tests {
|
|
|
334
374
|
result
|
|
335
375
|
}
|
|
336
376
|
|
|
377
|
+
#[cfg(unix)]
|
|
378
|
+
fn safe_fixture_sidecar(path: &Path, expected: &str) -> &'static str {
|
|
379
|
+
match std::fs::read_to_string(path) {
|
|
380
|
+
Ok(text) if text == expected => "present_expected",
|
|
381
|
+
Ok(_) => "present_unexpected",
|
|
382
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => "missing",
|
|
383
|
+
Err(_) => "unavailable",
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
#[cfg(unix)]
|
|
388
|
+
fn safe_runner_fixture_observation(path: &Path) -> String {
|
|
389
|
+
format!(
|
|
390
|
+
"fixture_entered={} fixture_completed={} fixture_count={}",
|
|
391
|
+
safe_fixture_sidecar(&path.with_extension("entered"), "1\n"),
|
|
392
|
+
safe_fixture_sidecar(&path.with_extension("completed"), "1\n"),
|
|
393
|
+
safe_fixture_sidecar(&path.with_extension("count"), "1:--list-models\n"),
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
#[cfg(unix)]
|
|
398
|
+
fn safe_runner_observation(
|
|
399
|
+
elapsed: Duration,
|
|
400
|
+
observation: &crate::lifecycle::launch::pi_mcp::PiCatalogTestObservation,
|
|
401
|
+
) -> String {
|
|
402
|
+
format!(
|
|
403
|
+
"elapsed_ms={} spawn_count={} argv={:?} parent_pid={:?} parent_exit_success={:?} parent_exit_code={:?} reader_timeout={}",
|
|
404
|
+
elapsed.as_millis(),
|
|
405
|
+
observation.spawn_count,
|
|
406
|
+
observation.argv,
|
|
407
|
+
observation.parent_pid,
|
|
408
|
+
observation.parent_exit_success,
|
|
409
|
+
observation.parent_exit_code,
|
|
410
|
+
observation.reader_timeout,
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#[cfg(unix)]
|
|
415
|
+
fn safe_models_result_class(result: &CmdResult) -> &'static str {
|
|
416
|
+
let CmdOutput::Human(text) = &result.output else {
|
|
417
|
+
return "non_human_output";
|
|
418
|
+
};
|
|
419
|
+
let Ok(value) = serde_json::from_str::<Value>(text) else {
|
|
420
|
+
return "non_json_output";
|
|
421
|
+
};
|
|
422
|
+
match value.get("ok").and_then(Value::as_bool) {
|
|
423
|
+
Some(true) => "success_json",
|
|
424
|
+
Some(false) => match value.get("error").and_then(Value::as_str) {
|
|
425
|
+
Some(error) if error.contains("unavailable") => "runner_unavailable",
|
|
426
|
+
Some(error) if error.contains("timed out") => "runner_timeout",
|
|
427
|
+
Some(error) if error.contains("command failed") => "runner_exit",
|
|
428
|
+
Some(error) if error.contains("could not be read") => "reader_error",
|
|
429
|
+
Some(error) if error.contains("exceeds") => "reader_oversize",
|
|
430
|
+
Some(error) if error.contains("catalog") => "parser_catalog_error",
|
|
431
|
+
Some(_) => "failure_json",
|
|
432
|
+
None => "failure_json_missing_error",
|
|
433
|
+
},
|
|
434
|
+
None => "json_missing_ok",
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#[cfg(unix)]
|
|
439
|
+
fn safe_models_observation(
|
|
440
|
+
elapsed: Duration,
|
|
441
|
+
result: &CmdResult,
|
|
442
|
+
observation: &crate::lifecycle::launch::pi_mcp::PiCatalogTestObservation,
|
|
443
|
+
) -> String {
|
|
444
|
+
format!(
|
|
445
|
+
"class={} elapsed_ms={} parent_pid={:?} parent_exit_success={:?} parent_exit_code={:?} reader_timeout={}",
|
|
446
|
+
safe_models_result_class(result),
|
|
447
|
+
elapsed.as_millis(),
|
|
448
|
+
observation.parent_pid,
|
|
449
|
+
observation.parent_exit_success,
|
|
450
|
+
observation.parent_exit_code,
|
|
451
|
+
observation.reader_timeout,
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
337
455
|
#[cfg(unix)]
|
|
338
456
|
#[test]
|
|
339
457
|
fn runner_invokes_exact_argv_once_and_drains() {
|
|
340
|
-
let path =
|
|
458
|
+
let path = fixture_with_sidecars(
|
|
341
459
|
"test \"$1\" = --list-models && echo provider model && echo openai-codex gpt-5.6-sol",
|
|
342
460
|
);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
461
|
+
let receipt = native_timeout_receipt();
|
|
462
|
+
assert!(!receipt.exists());
|
|
463
|
+
let started = Instant::now();
|
|
464
|
+
let (result, observation) =
|
|
465
|
+
crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
|
|
466
|
+
&receipt,
|
|
467
|
+
None,
|
|
468
|
+
|| run_catalog(&path, Duration::from_secs(1), 1024),
|
|
469
|
+
);
|
|
470
|
+
let elapsed = started.elapsed();
|
|
471
|
+
let fixture_observation = safe_runner_fixture_observation(&path);
|
|
472
|
+
assert!(
|
|
473
|
+
result
|
|
474
|
+
.as_ref()
|
|
475
|
+
.is_ok_and(|bytes| bytes.starts_with(b"provider model")),
|
|
476
|
+
"runner observation: {}; {}",
|
|
477
|
+
safe_runner_observation(elapsed, &observation),
|
|
478
|
+
fixture_observation
|
|
479
|
+
);
|
|
480
|
+
let _ = result.unwrap();
|
|
481
|
+
let _ = std::fs::remove_file(&receipt);
|
|
482
|
+
let _ = std::fs::remove_file(format!("{}.sock", receipt.display()));
|
|
346
483
|
assert_eq!(
|
|
347
484
|
std::fs::read_to_string(path.with_extension("count")).unwrap(),
|
|
348
485
|
"1:--list-models\n"
|
|
@@ -350,28 +487,160 @@ mod tests {
|
|
|
350
487
|
cleanup_fixture(&path);
|
|
351
488
|
}
|
|
352
489
|
|
|
490
|
+
#[cfg(unix)]
|
|
491
|
+
const NATIVE_STAGE_KEYS: [&str; 5] = [
|
|
492
|
+
"parent_entry_ms",
|
|
493
|
+
"parent_before_child_spawn_ms",
|
|
494
|
+
"parent_after_child_spawn_ms",
|
|
495
|
+
"parent_before_exit_ms",
|
|
496
|
+
"test_spawn_return_ms",
|
|
497
|
+
];
|
|
498
|
+
|
|
499
|
+
#[cfg(unix)]
|
|
500
|
+
// Match the native helper's UNIX epoch millisecond clock exactly.
|
|
501
|
+
fn append_native_stage(path: &Path, key: &str) {
|
|
502
|
+
if !NATIVE_STAGE_KEYS.contains(&key) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
let Some(milliseconds) = std::time::SystemTime::now()
|
|
506
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
507
|
+
.ok()
|
|
508
|
+
.map(|duration| duration.as_millis())
|
|
509
|
+
else {
|
|
510
|
+
return;
|
|
511
|
+
};
|
|
512
|
+
let Ok(mut file) = std::fs::OpenOptions::new()
|
|
513
|
+
.create(true)
|
|
514
|
+
.append(true)
|
|
515
|
+
.open(path)
|
|
516
|
+
else {
|
|
517
|
+
return;
|
|
518
|
+
};
|
|
519
|
+
let _ = writeln!(file, "{key}={milliseconds}");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
#[cfg(unix)]
|
|
523
|
+
fn safe_native_stage(path: &Path) -> String {
|
|
524
|
+
let (read_state, text) = match std::fs::read_to_string(path) {
|
|
525
|
+
Ok(text) => ("ok", Some(text)),
|
|
526
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => ("missing", None),
|
|
527
|
+
Err(_) => ("unavailable", None),
|
|
528
|
+
};
|
|
529
|
+
let mut fields = BTreeMap::new();
|
|
530
|
+
if let Some(ref text) = text {
|
|
531
|
+
for line in text.lines() {
|
|
532
|
+
let Some((key, value)) = line.split_once('=') else {
|
|
533
|
+
continue;
|
|
534
|
+
};
|
|
535
|
+
if NATIVE_STAGE_KEYS.contains(&key)
|
|
536
|
+
&& value.len() <= 20
|
|
537
|
+
&& value.bytes().all(|byte| byte.is_ascii_digit())
|
|
538
|
+
{
|
|
539
|
+
fields.entry(key).or_insert(value);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
let field = |key: &str| fields.get(key).copied().unwrap_or("unknown");
|
|
544
|
+
format!(
|
|
545
|
+
"stage_read={read_state} parent_entry_ms={} parent_before_child_spawn_ms={} parent_after_child_spawn_ms={} parent_before_exit_ms={} test_spawn_return_ms={}",
|
|
546
|
+
field("parent_entry_ms"),
|
|
547
|
+
field("parent_before_child_spawn_ms"),
|
|
548
|
+
field("parent_after_child_spawn_ms"),
|
|
549
|
+
field("parent_before_exit_ms"),
|
|
550
|
+
field("test_spawn_return_ms"),
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
#[cfg(unix)]
|
|
555
|
+
fn wait_for_native_parent_exit(
|
|
556
|
+
child: &mut std::process::Child,
|
|
557
|
+
stage: &Path,
|
|
558
|
+
) -> std::process::ExitStatus {
|
|
559
|
+
let deadline = Instant::now() + Duration::from_millis(500);
|
|
560
|
+
loop {
|
|
561
|
+
match child.try_wait() {
|
|
562
|
+
Ok(Some(status)) => return status,
|
|
563
|
+
Ok(None) if Instant::now() < deadline => {
|
|
564
|
+
std::thread::sleep(Duration::from_millis(1));
|
|
565
|
+
}
|
|
566
|
+
Ok(None) => {
|
|
567
|
+
let before_kill = safe_native_stage(stage);
|
|
568
|
+
let _ = child.kill();
|
|
569
|
+
let _ = child.wait();
|
|
570
|
+
let after_wait = safe_native_stage(stage);
|
|
571
|
+
panic!(
|
|
572
|
+
"native parent did not exit before test setup deadline; stage_before_kill={before_kill}; stage_after_wait={after_wait}"
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
Err(_) => {
|
|
576
|
+
let before_kill = safe_native_stage(stage);
|
|
577
|
+
let _ = child.kill();
|
|
578
|
+
let _ = child.wait();
|
|
579
|
+
let after_wait = safe_native_stage(stage);
|
|
580
|
+
panic!(
|
|
581
|
+
"native parent status could not be observed; stage_before_kill={before_kill}; stage_after_wait={after_wait}"
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/// Read/drain seam only: public `run_catalog` spawn coverage remains in the
|
|
589
|
+
/// normal success and fail-closed tests.
|
|
353
590
|
#[cfg(unix)]
|
|
354
591
|
#[test]
|
|
355
|
-
fn
|
|
592
|
+
fn reader_deadline_is_bounded_after_parent_exit_with_descendant_pipe() {
|
|
356
593
|
let path = native_timeout_fixture();
|
|
357
594
|
let receipt = native_timeout_receipt();
|
|
595
|
+
let stage = native_timeout_stage_receipt(&receipt);
|
|
358
596
|
assert!(!receipt.exists());
|
|
597
|
+
assert!(!stage.exists());
|
|
598
|
+
let mut parent = std::process::Command::new(&path)
|
|
599
|
+
.arg("--list-models")
|
|
600
|
+
.env("TEAM_AGENT_MODELS_TIMEOUT_RECEIPT", &receipt)
|
|
601
|
+
.env("TEAM_AGENT_MODELS_TIMEOUT_STAGE_RECEIPT", &stage)
|
|
602
|
+
.env("TEAM_AGENT_MODELS_TIMEOUT_MODE", "descendant")
|
|
603
|
+
.stdin(std::process::Stdio::null())
|
|
604
|
+
.stdout(std::process::Stdio::piped())
|
|
605
|
+
.stderr(std::process::Stdio::null())
|
|
606
|
+
.spawn()
|
|
607
|
+
.unwrap();
|
|
608
|
+
append_native_stage(&stage, "test_spawn_return_ms");
|
|
609
|
+
let parent_pid = parent.id();
|
|
610
|
+
let parent_status = wait_for_native_parent_exit(&mut parent, &stage);
|
|
611
|
+
let stdout = parent.stdout.take().expect("native parent stdout pipe");
|
|
359
612
|
let started = Instant::now();
|
|
360
613
|
let (result, observation) =
|
|
361
614
|
crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
|
|
362
615
|
&receipt,
|
|
363
616
|
Some("descendant"),
|
|
364
|
-
||
|
|
617
|
+
|| {
|
|
618
|
+
crate::lifecycle::launch::pi_mcp::run_pi_catalog_after_parent_exit_for_test(
|
|
619
|
+
stdout,
|
|
620
|
+
parent_pid,
|
|
621
|
+
parent_status,
|
|
622
|
+
Duration::from_millis(40),
|
|
623
|
+
1024,
|
|
624
|
+
)
|
|
625
|
+
},
|
|
365
626
|
);
|
|
366
|
-
|
|
627
|
+
let elapsed = started.elapsed();
|
|
628
|
+
assert!(
|
|
629
|
+
elapsed < Duration::from_millis(500),
|
|
630
|
+
"runner observation: {}",
|
|
631
|
+
safe_runner_observation(elapsed, &observation)
|
|
632
|
+
);
|
|
367
633
|
assert!(result.unwrap_err().contains("timed out"));
|
|
368
|
-
assert_eq!(
|
|
369
|
-
|
|
370
|
-
|
|
634
|
+
assert_eq!(
|
|
635
|
+
observation.parent_exit_success,
|
|
636
|
+
Some(true),
|
|
637
|
+
"runner observation: {}",
|
|
638
|
+
safe_runner_observation(elapsed, &observation)
|
|
639
|
+
);
|
|
371
640
|
assert_eq!(observation.parent_exit_code, Some(0));
|
|
372
641
|
assert!(observation.reader_timeout);
|
|
373
642
|
|
|
374
|
-
let fields =
|
|
643
|
+
let fields = wait_native_timeout_receipt(&receipt);
|
|
375
644
|
assert_eq!(fields["parent_started_once"], "1");
|
|
376
645
|
assert_eq!(fields["parent_argv_exact"], "1");
|
|
377
646
|
assert_eq!(fields["child_spawn_attempts"], "1");
|
|
@@ -384,6 +653,7 @@ mod tests {
|
|
|
384
653
|
);
|
|
385
654
|
assert_eq!(fields["child_pid"], fields["descendant_pid"]);
|
|
386
655
|
let _ = std::fs::remove_file(&receipt);
|
|
656
|
+
let _ = std::fs::remove_file(&stage);
|
|
387
657
|
let _ = std::fs::remove_file(format!("{}.sock", receipt.display()));
|
|
388
658
|
}
|
|
389
659
|
|
|
@@ -398,16 +668,72 @@ mod tests {
|
|
|
398
668
|
search: Some("sol".into()),
|
|
399
669
|
json: true,
|
|
400
670
|
};
|
|
401
|
-
let
|
|
402
|
-
|
|
403
|
-
|
|
671
|
+
let receipt = native_timeout_receipt();
|
|
672
|
+
assert!(!receipt.exists());
|
|
673
|
+
let started = Instant::now();
|
|
674
|
+
let (result, observation) =
|
|
675
|
+
crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
|
|
676
|
+
&receipt,
|
|
677
|
+
None,
|
|
678
|
+
|| cmd_models_with(&args, &path, Duration::from_secs(1), 1024),
|
|
679
|
+
);
|
|
680
|
+
let elapsed = started.elapsed();
|
|
681
|
+
let result = match result {
|
|
682
|
+
Ok(result) => result,
|
|
683
|
+
Err(_) => panic!(
|
|
684
|
+
"command boundary returned a CLI error; class=cli_error elapsed_ms={} parent_pid={:?} parent_exit_success={:?} parent_exit_code={:?} reader_timeout={}",
|
|
685
|
+
elapsed.as_millis(),
|
|
686
|
+
observation.parent_pid,
|
|
687
|
+
observation.parent_exit_success,
|
|
688
|
+
observation.parent_exit_code,
|
|
689
|
+
observation.reader_timeout,
|
|
690
|
+
),
|
|
404
691
|
};
|
|
405
|
-
let
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
692
|
+
let CmdOutput::Human(text) = &result.output else {
|
|
693
|
+
panic!(
|
|
694
|
+
"expected JSON projection; {}",
|
|
695
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
696
|
+
)
|
|
697
|
+
};
|
|
698
|
+
let value: Value = serde_json::from_str(text).unwrap_or_else(|_| {
|
|
699
|
+
panic!(
|
|
700
|
+
"expected valid JSON projection; {}",
|
|
701
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
702
|
+
)
|
|
703
|
+
});
|
|
704
|
+
assert_eq!(
|
|
705
|
+
value["schema_version"],
|
|
706
|
+
"models.v1",
|
|
707
|
+
"{}",
|
|
708
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
709
|
+
);
|
|
710
|
+
assert_eq!(
|
|
711
|
+
value["models"][0]["role_model"],
|
|
712
|
+
"openai-codex/gpt-5.6-sol",
|
|
713
|
+
"{}",
|
|
714
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
715
|
+
);
|
|
716
|
+
assert_eq!(
|
|
717
|
+
value["models"][0]["current"],
|
|
718
|
+
false,
|
|
719
|
+
"{}",
|
|
720
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
721
|
+
);
|
|
722
|
+
assert_eq!(
|
|
723
|
+
value["auth"],
|
|
724
|
+
"ok",
|
|
725
|
+
"{}",
|
|
726
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
727
|
+
);
|
|
728
|
+
let model_count = value["models"].as_array().map(|models| models.len());
|
|
729
|
+
assert_eq!(
|
|
730
|
+
model_count,
|
|
731
|
+
Some(1),
|
|
732
|
+
"{}",
|
|
733
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
734
|
+
);
|
|
735
|
+
let _ = std::fs::remove_file(&receipt);
|
|
736
|
+
let _ = std::fs::remove_file(format!("{}.sock", receipt.display()));
|
|
411
737
|
let human = cmd_models_with(
|
|
412
738
|
&ModelsArgs {
|
|
413
739
|
provider: "pi".into(),
|
|
@@ -441,12 +767,16 @@ mod tests {
|
|
|
441
767
|
#[test]
|
|
442
768
|
fn runner_fails_closed_for_nonzero_oversize_timeout_and_unavailable() {
|
|
443
769
|
let fail = native_timeout_fixture();
|
|
770
|
+
let started = Instant::now();
|
|
444
771
|
let (failed_result, fail_observation) = with_native_mode("exit7", || {
|
|
445
772
|
run_catalog(&fail, Duration::from_secs(1), 1024)
|
|
446
773
|
});
|
|
774
|
+
let elapsed = started.elapsed();
|
|
447
775
|
assert_eq!(
|
|
448
776
|
failed_result.unwrap_err(),
|
|
449
|
-
"Pi model catalog command failed"
|
|
777
|
+
"Pi model catalog command failed",
|
|
778
|
+
"runner observation: {}",
|
|
779
|
+
safe_runner_observation(elapsed, &fail_observation)
|
|
450
780
|
);
|
|
451
781
|
assert_eq!(fail_observation.spawn_count, 1);
|
|
452
782
|
assert_eq!(fail_observation.argv, vec!["--list-models"]);
|
|
@@ -694,9 +694,23 @@ fn resolve_worker(
|
|
|
694
694
|
}
|
|
695
695
|
};
|
|
696
696
|
let matches = matching_session_window(&targets, session, window);
|
|
697
|
-
match matches.len() {
|
|
698
|
-
1 =>
|
|
699
|
-
|
|
697
|
+
let selected_pane = match matches.len() {
|
|
698
|
+
1 => matches.first().copied(),
|
|
699
|
+
n if n > 1 => {
|
|
700
|
+
let state_pane_id =
|
|
701
|
+
string_field(agent_entry, "pane_id").filter(|pane_id| !pane_id.is_empty());
|
|
702
|
+
let mut state_matches = matches.iter().copied().filter(|pane| {
|
|
703
|
+
state_pane_id.is_some_and(|pane_id| pane.pane_id.as_str() == pane_id)
|
|
704
|
+
});
|
|
705
|
+
match (state_matches.next(), state_matches.next()) {
|
|
706
|
+
(Some(pane), None) => Some(pane),
|
|
707
|
+
_ => None,
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
_ => None,
|
|
711
|
+
};
|
|
712
|
+
match selected_pane {
|
|
713
|
+
Some(pane) => {
|
|
700
714
|
let state_pane_id = string_field(agent_entry, "pane_id").map(str::to_string);
|
|
701
715
|
let state_pane_stale = state_pane_id
|
|
702
716
|
.as_deref()
|
|
@@ -727,7 +741,7 @@ fn resolve_worker(
|
|
|
727
741
|
warning,
|
|
728
742
|
})
|
|
729
743
|
}
|
|
730
|
-
|
|
744
|
+
None if matches.is_empty() => Ok(resolved_worker_without_live(
|
|
731
745
|
sender_workspace,
|
|
732
746
|
target_workspace,
|
|
733
747
|
team,
|
|
@@ -738,7 +752,7 @@ fn resolve_worker(
|
|
|
738
752
|
window,
|
|
739
753
|
transport.tmux_endpoint(),
|
|
740
754
|
)),
|
|
741
|
-
|
|
755
|
+
None => Err(name_ambiguous(
|
|
742
756
|
"multiple live panes match the same session/window",
|
|
743
757
|
matches
|
|
744
758
|
.into_iter()
|
|
@@ -20,17 +20,20 @@ pub fn cmd_profile(args: &ProfileArgs) -> Result<CmdResult, CliError> {
|
|
|
20
20
|
fn init_profile(args: &ProfileArgs) -> Result<Value, CliError> {
|
|
21
21
|
let auth_mode = args.auth_mode.as_deref().unwrap_or("subscription");
|
|
22
22
|
validate_auth_mode(auth_mode)?;
|
|
23
|
+
let proxy_mode = args.proxy_mode.as_deref().unwrap_or("inherit");
|
|
24
|
+
validate_proxy_mode(proxy_mode)?;
|
|
23
25
|
let dir = profile_dir(args);
|
|
24
26
|
std::fs::create_dir_all(&dir)?;
|
|
25
27
|
let path = dir.join(format!("{}.env", args.name));
|
|
26
28
|
let template_path = dir.join(format!("{}.example.env", args.name));
|
|
27
|
-
let body = profile_template(&args.name, auth_mode);
|
|
29
|
+
let body = profile_template(&args.name, auth_mode, proxy_mode);
|
|
28
30
|
let created_profile = write_new_file(&path, &body)?;
|
|
29
31
|
if created_profile {
|
|
30
32
|
set_owner_only_permissions(&path)?;
|
|
31
33
|
}
|
|
32
34
|
let created_template = write_new_file(&template_path, &body)?;
|
|
33
35
|
write_boundary_files(&dir)?;
|
|
36
|
+
let actual_proxy_mode = display_proxy_mode(&read_profile_env(&path)?);
|
|
34
37
|
|
|
35
38
|
let mut obj = Map::new();
|
|
36
39
|
obj.insert("ok".to_string(), Value::Bool(true));
|
|
@@ -39,6 +42,14 @@ fn init_profile(args: &ProfileArgs) -> Result<Value, CliError> {
|
|
|
39
42
|
"auth_mode".to_string(),
|
|
40
43
|
Value::String(auth_mode.to_string()),
|
|
41
44
|
);
|
|
45
|
+
obj.insert(
|
|
46
|
+
"proxy_mode".to_string(),
|
|
47
|
+
Value::String(actual_proxy_mode),
|
|
48
|
+
);
|
|
49
|
+
obj.insert(
|
|
50
|
+
"proxy_mode_changed".to_string(),
|
|
51
|
+
Value::Bool(created_profile),
|
|
52
|
+
);
|
|
42
53
|
obj.insert("path".to_string(), path_value(&path));
|
|
43
54
|
obj.insert("template_path".to_string(), path_value(&template_path));
|
|
44
55
|
obj.insert("created_profile".to_string(), Value::Bool(created_profile));
|
|
@@ -72,6 +83,7 @@ fn doctor_profile(args: &ProfileArgs) -> Result<Value, CliError> {
|
|
|
72
83
|
let values = read_profile_env(&path)?;
|
|
73
84
|
let auth_mode =
|
|
74
85
|
profile_value(&values, "AUTH_MODE").unwrap_or_else(|| "subscription".to_string());
|
|
86
|
+
let proxy_mode = display_proxy_mode(&values);
|
|
75
87
|
let secret_keys = secret_keys_present(&values);
|
|
76
88
|
let keys = sorted_keys(&values);
|
|
77
89
|
let mut obj = Map::new();
|
|
@@ -83,6 +95,7 @@ fn doctor_profile(args: &ProfileArgs) -> Result<Value, CliError> {
|
|
|
83
95
|
Value::Bool(credential_present(&values)),
|
|
84
96
|
);
|
|
85
97
|
obj.insert("auth_mode".to_string(), Value::String(auth_mode));
|
|
98
|
+
obj.insert("proxy_mode".to_string(), Value::String(proxy_mode));
|
|
86
99
|
obj.insert("keys_present".to_string(), string_array(keys));
|
|
87
100
|
obj.insert(
|
|
88
101
|
"raw_file_read_allowed_for_agents".to_string(),
|
|
@@ -112,6 +125,7 @@ fn show_profile(args: &ProfileArgs) -> Result<Value, CliError> {
|
|
|
112
125
|
let values = read_profile_env(&path)?;
|
|
113
126
|
let auth_mode =
|
|
114
127
|
profile_value(&values, "AUTH_MODE").unwrap_or_else(|| "subscription".to_string());
|
|
128
|
+
let proxy_mode = display_proxy_mode(&values);
|
|
115
129
|
let secret_keys = secret_keys_present(&values);
|
|
116
130
|
let missing_common = missing_common_keys(&values, &auth_mode);
|
|
117
131
|
let mut obj = Map::new();
|
|
@@ -122,6 +136,7 @@ fn show_profile(args: &ProfileArgs) -> Result<Value, CliError> {
|
|
|
122
136
|
Value::Bool(credential_present(&values)),
|
|
123
137
|
);
|
|
124
138
|
obj.insert("auth_mode".to_string(), Value::String(auth_mode));
|
|
139
|
+
obj.insert("proxy_mode".to_string(), Value::String(proxy_mode));
|
|
125
140
|
obj.insert("values".to_string(), redacted_values(&values));
|
|
126
141
|
obj.insert(
|
|
127
142
|
"keys_present".to_string(),
|
|
@@ -157,12 +172,21 @@ fn validate_auth_mode(auth_mode: &str) -> Result<(), CliError> {
|
|
|
157
172
|
}
|
|
158
173
|
}
|
|
159
174
|
|
|
160
|
-
fn
|
|
175
|
+
fn validate_proxy_mode(proxy_mode: &str) -> Result<(), CliError> {
|
|
176
|
+
match proxy_mode {
|
|
177
|
+
"direct" | "inherit" => Ok(()),
|
|
178
|
+
other => Err(CliError::Usage(format!(
|
|
179
|
+
"invalid --proxy-mode: {other} (expected direct|inherit)"
|
|
180
|
+
))),
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn profile_template(name: &str, auth_mode: &str, proxy_mode: &str) -> String {
|
|
161
185
|
match auth_mode {
|
|
162
186
|
"compatible_api" | "official_api" => {
|
|
163
|
-
format!("AUTH_MODE={auth_mode}\nPROFILE_NAME={name}\nBASE_URL=\nAPI_KEY=\nMODEL=\n")
|
|
187
|
+
format!("AUTH_MODE={auth_mode}\nPROFILE_NAME={name}\nPROXY_MODE={proxy_mode}\nBASE_URL=\nAPI_KEY=\nMODEL=\n")
|
|
164
188
|
}
|
|
165
|
-
_ => format!("AUTH_MODE=subscription\nPROFILE_NAME={name}\n"),
|
|
189
|
+
_ => format!("AUTH_MODE=subscription\nPROFILE_NAME={name}\nPROXY_MODE={proxy_mode}\n"),
|
|
166
190
|
}
|
|
167
191
|
}
|
|
168
192
|
|
|
@@ -235,6 +259,13 @@ fn profile_value(values: &Map<String, Value>, key: &str) -> Option<String> {
|
|
|
235
259
|
.map(ToString::to_string)
|
|
236
260
|
}
|
|
237
261
|
|
|
262
|
+
fn display_proxy_mode(values: &Map<String, Value>) -> String {
|
|
263
|
+
profile_value(values, "PROXY_MODE")
|
|
264
|
+
.or_else(|| profile_value(values, "NETWORK_MODE"))
|
|
265
|
+
.map(|value| value.trim().to_ascii_lowercase())
|
|
266
|
+
.unwrap_or_else(|| "inherit".to_string())
|
|
267
|
+
}
|
|
268
|
+
|
|
238
269
|
fn missing_profile_value(args: &ProfileArgs, path: &Path, template_path: Option<&Path>) -> Value {
|
|
239
270
|
let mut obj = Map::new();
|
|
240
271
|
obj.insert("ok".to_string(), Value::Bool(false));
|