@team-agent/installer 0.5.77 → 0.5.80
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 +393 -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/runtime.rs +3 -1
- 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/status_port/tests.rs +37 -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/coordinator/steps/abnormal.rs +122 -2
- 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 +197 -88
- 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 +86 -23
- 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/fixtures/pi_list_models_spaced_ids.stdout.txt +9 -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_catalog_red.rs +71 -0
- 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 +221 -80
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +127 -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/provider/session_scan/common/tests.rs +204 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +24 -2
- package/crates/team-agent/src/state/abnormal_watch/tests/mod.rs +248 -0
- package/crates/team-agent/src/state/abnormal_watch.rs +144 -0
- package/crates/team-agent/src/state/mod.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +102 -4
- package/crates/team-agent/src/state/projection.rs +7 -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 +149 -4
- package/crates/team-agent/src/tmux_backend.rs +239 -127
- package/crates/team-agent/src/transport.rs +78 -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() {
|
|
@@ -241,15 +248,47 @@ mod tests {
|
|
|
241
248
|
None
|
|
242
249
|
);
|
|
243
250
|
}
|
|
251
|
+
|
|
252
|
+
#[cfg(unix)]
|
|
253
|
+
#[test]
|
|
254
|
+
fn search_survives_unrelated_spaced_model_ids() {
|
|
255
|
+
let catalog = include_str!("../lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt");
|
|
256
|
+
let path = fixture(&format!("printf '%s' '{catalog}'"));
|
|
257
|
+
let result = cmd_models_with(
|
|
258
|
+
&ModelsArgs {
|
|
259
|
+
provider: "pi".into(),
|
|
260
|
+
search: Some("gpt-5.6-luna".into()),
|
|
261
|
+
json: true,
|
|
262
|
+
},
|
|
263
|
+
&path,
|
|
264
|
+
Duration::from_secs(10),
|
|
265
|
+
MAX_CATALOG_BYTES,
|
|
266
|
+
)
|
|
267
|
+
.unwrap();
|
|
268
|
+
cleanup_fixture(&path);
|
|
269
|
+
assert_eq!(result.exit, ExitCode::Ok);
|
|
270
|
+
let CmdOutput::Human(text) = result.output else {
|
|
271
|
+
panic!("expected JSON projection")
|
|
272
|
+
};
|
|
273
|
+
let value: Value = serde_json::from_str(&text).unwrap();
|
|
274
|
+
assert_eq!(value["ok"], true);
|
|
275
|
+
assert_eq!(value["auth"], "ok");
|
|
276
|
+
let models = value["models"].as_array().unwrap();
|
|
277
|
+
assert_eq!(models.len(), 2);
|
|
278
|
+
assert_eq!(models[0]["role_model"], "openai-codex/gpt-5.6-luna");
|
|
279
|
+
assert_eq!(models[1]["role_model"], "team-agent/gpt-5.6-luna");
|
|
280
|
+
}
|
|
281
|
+
|
|
244
282
|
#[cfg(unix)]
|
|
245
283
|
fn fixture(body: &str) -> std::path::PathBuf {
|
|
246
284
|
use std::os::unix::fs::PermissionsExt;
|
|
285
|
+
let sequence = FIXTURE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
|
|
247
286
|
let root = std::env::current_exe()
|
|
248
287
|
.unwrap()
|
|
249
288
|
.parent()
|
|
250
289
|
.unwrap()
|
|
251
290
|
.join(format!(
|
|
252
|
-
"team-agent-pi-fixture-{}-{}",
|
|
291
|
+
"team-agent-pi-fixture-{}-{}-{sequence}",
|
|
253
292
|
std::process::id(),
|
|
254
293
|
std::time::SystemTime::now()
|
|
255
294
|
.duration_since(std::time::UNIX_EPOCH)
|
|
@@ -269,6 +308,18 @@ mod tests {
|
|
|
269
308
|
path
|
|
270
309
|
}
|
|
271
310
|
|
|
311
|
+
#[cfg(unix)]
|
|
312
|
+
fn fixture_with_sidecars(body: &str) -> std::path::PathBuf {
|
|
313
|
+
let path = fixture(body);
|
|
314
|
+
let script = std::fs::read_to_string(&path).unwrap();
|
|
315
|
+
let script = script.strip_prefix("#!/bin/sh\n").unwrap();
|
|
316
|
+
let script = format!(
|
|
317
|
+
"#!/bin/sh\nprintf '1\\n' > \"$0.entered\" || :\n{script}body_status=$?\nprintf '1\\n' > \"$0.completed\" || :\nexit \"$body_status\"\n"
|
|
318
|
+
);
|
|
319
|
+
std::fs::write(&path, script).unwrap();
|
|
320
|
+
path
|
|
321
|
+
}
|
|
322
|
+
|
|
272
323
|
#[cfg(unix)]
|
|
273
324
|
fn cleanup_fixture(path: &Path) {
|
|
274
325
|
let _ = std::fs::remove_dir_all(path.parent().unwrap());
|
|
@@ -294,15 +345,22 @@ mod tests {
|
|
|
294
345
|
}
|
|
295
346
|
|
|
296
347
|
#[cfg(unix)]
|
|
297
|
-
fn
|
|
348
|
+
fn native_timeout_stage_receipt(receipt: &Path) -> std::path::PathBuf {
|
|
349
|
+
receipt.with_extension("stage")
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#[cfg(unix)]
|
|
353
|
+
fn parse_native_timeout_receipt(path: &Path) -> Option<BTreeMap<String, String>> {
|
|
298
354
|
let mut fields = BTreeMap::new();
|
|
299
|
-
for line in std::fs::read_to_string(path).
|
|
300
|
-
let (key, value) = line.split_once('=')
|
|
301
|
-
|
|
355
|
+
for line in std::fs::read_to_string(path).ok()?.lines() {
|
|
356
|
+
let (key, value) = line.split_once('=')?;
|
|
357
|
+
if fields.insert(key.to_string(), value.to_string()).is_some() {
|
|
358
|
+
return None;
|
|
359
|
+
}
|
|
302
360
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
vec![
|
|
361
|
+
let keys = fields.keys().map(String::as_str).collect::<Vec<_>>();
|
|
362
|
+
(keys
|
|
363
|
+
== vec![
|
|
306
364
|
"child_argv_exact",
|
|
307
365
|
"child_pid",
|
|
308
366
|
"child_spawn_attempts",
|
|
@@ -312,9 +370,22 @@ mod tests {
|
|
|
312
370
|
"parent_argv_exact",
|
|
313
371
|
"parent_pid",
|
|
314
372
|
"parent_started_once",
|
|
315
|
-
]
|
|
316
|
-
)
|
|
317
|
-
|
|
373
|
+
])
|
|
374
|
+
.then_some(fields)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#[cfg(unix)]
|
|
378
|
+
fn wait_native_timeout_receipt(path: &Path) -> BTreeMap<String, String> {
|
|
379
|
+
let deadline = Instant::now() + Duration::from_millis(500);
|
|
380
|
+
loop {
|
|
381
|
+
if let Some(fields) = parse_native_timeout_receipt(path) {
|
|
382
|
+
return fields;
|
|
383
|
+
}
|
|
384
|
+
if Instant::now() >= deadline {
|
|
385
|
+
panic!("native timeout receipt did not become complete after runner return");
|
|
386
|
+
}
|
|
387
|
+
std::thread::sleep(Duration::from_millis(1));
|
|
388
|
+
}
|
|
318
389
|
}
|
|
319
390
|
|
|
320
391
|
#[cfg(unix)]
|
|
@@ -334,15 +405,112 @@ mod tests {
|
|
|
334
405
|
result
|
|
335
406
|
}
|
|
336
407
|
|
|
408
|
+
#[cfg(unix)]
|
|
409
|
+
fn safe_fixture_sidecar(path: &Path, expected: &str) -> &'static str {
|
|
410
|
+
match std::fs::read_to_string(path) {
|
|
411
|
+
Ok(text) if text == expected => "present_expected",
|
|
412
|
+
Ok(_) => "present_unexpected",
|
|
413
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => "missing",
|
|
414
|
+
Err(_) => "unavailable",
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
#[cfg(unix)]
|
|
419
|
+
fn safe_runner_fixture_observation(path: &Path) -> String {
|
|
420
|
+
format!(
|
|
421
|
+
"fixture_entered={} fixture_completed={} fixture_count={}",
|
|
422
|
+
safe_fixture_sidecar(&path.with_extension("entered"), "1\n"),
|
|
423
|
+
safe_fixture_sidecar(&path.with_extension("completed"), "1\n"),
|
|
424
|
+
safe_fixture_sidecar(&path.with_extension("count"), "1:--list-models\n"),
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#[cfg(unix)]
|
|
429
|
+
fn safe_runner_observation(
|
|
430
|
+
elapsed: Duration,
|
|
431
|
+
observation: &crate::lifecycle::launch::pi_mcp::PiCatalogTestObservation,
|
|
432
|
+
) -> String {
|
|
433
|
+
format!(
|
|
434
|
+
"elapsed_ms={} spawn_count={} argv={:?} parent_pid={:?} parent_exit_success={:?} parent_exit_code={:?} reader_timeout={}",
|
|
435
|
+
elapsed.as_millis(),
|
|
436
|
+
observation.spawn_count,
|
|
437
|
+
observation.argv,
|
|
438
|
+
observation.parent_pid,
|
|
439
|
+
observation.parent_exit_success,
|
|
440
|
+
observation.parent_exit_code,
|
|
441
|
+
observation.reader_timeout,
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#[cfg(unix)]
|
|
446
|
+
fn safe_models_result_class(result: &CmdResult) -> &'static str {
|
|
447
|
+
let CmdOutput::Human(text) = &result.output else {
|
|
448
|
+
return "non_human_output";
|
|
449
|
+
};
|
|
450
|
+
let Ok(value) = serde_json::from_str::<Value>(text) else {
|
|
451
|
+
return "non_json_output";
|
|
452
|
+
};
|
|
453
|
+
match value.get("ok").and_then(Value::as_bool) {
|
|
454
|
+
Some(true) => "success_json",
|
|
455
|
+
Some(false) => match value.get("error").and_then(Value::as_str) {
|
|
456
|
+
Some(error) if error.contains("unavailable") => "runner_unavailable",
|
|
457
|
+
Some(error) if error.contains("timed out") => "runner_timeout",
|
|
458
|
+
Some(error) if error.contains("command failed") => "runner_exit",
|
|
459
|
+
Some(error) if error.contains("could not be read") => "reader_error",
|
|
460
|
+
Some(error) if error.contains("exceeds") => "reader_oversize",
|
|
461
|
+
Some(error) if error.contains("catalog") => "parser_catalog_error",
|
|
462
|
+
Some(_) => "failure_json",
|
|
463
|
+
None => "failure_json_missing_error",
|
|
464
|
+
},
|
|
465
|
+
None => "json_missing_ok",
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
#[cfg(unix)]
|
|
470
|
+
fn safe_models_observation(
|
|
471
|
+
elapsed: Duration,
|
|
472
|
+
result: &CmdResult,
|
|
473
|
+
observation: &crate::lifecycle::launch::pi_mcp::PiCatalogTestObservation,
|
|
474
|
+
) -> String {
|
|
475
|
+
format!(
|
|
476
|
+
"class={} elapsed_ms={} parent_pid={:?} parent_exit_success={:?} parent_exit_code={:?} reader_timeout={}",
|
|
477
|
+
safe_models_result_class(result),
|
|
478
|
+
elapsed.as_millis(),
|
|
479
|
+
observation.parent_pid,
|
|
480
|
+
observation.parent_exit_success,
|
|
481
|
+
observation.parent_exit_code,
|
|
482
|
+
observation.reader_timeout,
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
|
|
337
486
|
#[cfg(unix)]
|
|
338
487
|
#[test]
|
|
339
488
|
fn runner_invokes_exact_argv_once_and_drains() {
|
|
340
|
-
let path =
|
|
489
|
+
let path = fixture_with_sidecars(
|
|
341
490
|
"test \"$1\" = --list-models && echo provider model && echo openai-codex gpt-5.6-sol",
|
|
342
491
|
);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
492
|
+
let receipt = native_timeout_receipt();
|
|
493
|
+
assert!(!receipt.exists());
|
|
494
|
+
let started = Instant::now();
|
|
495
|
+
let (result, observation) =
|
|
496
|
+
crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
|
|
497
|
+
&receipt,
|
|
498
|
+
None,
|
|
499
|
+
|| run_catalog(&path, Duration::from_secs(1), 1024),
|
|
500
|
+
);
|
|
501
|
+
let elapsed = started.elapsed();
|
|
502
|
+
let fixture_observation = safe_runner_fixture_observation(&path);
|
|
503
|
+
assert!(
|
|
504
|
+
result
|
|
505
|
+
.as_ref()
|
|
506
|
+
.is_ok_and(|bytes| bytes.starts_with(b"provider model")),
|
|
507
|
+
"runner observation: {}; {}",
|
|
508
|
+
safe_runner_observation(elapsed, &observation),
|
|
509
|
+
fixture_observation
|
|
510
|
+
);
|
|
511
|
+
let _ = result.unwrap();
|
|
512
|
+
let _ = std::fs::remove_file(&receipt);
|
|
513
|
+
let _ = std::fs::remove_file(format!("{}.sock", receipt.display()));
|
|
346
514
|
assert_eq!(
|
|
347
515
|
std::fs::read_to_string(path.with_extension("count")).unwrap(),
|
|
348
516
|
"1:--list-models\n"
|
|
@@ -350,28 +518,160 @@ mod tests {
|
|
|
350
518
|
cleanup_fixture(&path);
|
|
351
519
|
}
|
|
352
520
|
|
|
521
|
+
#[cfg(unix)]
|
|
522
|
+
const NATIVE_STAGE_KEYS: [&str; 5] = [
|
|
523
|
+
"parent_entry_ms",
|
|
524
|
+
"parent_before_child_spawn_ms",
|
|
525
|
+
"parent_after_child_spawn_ms",
|
|
526
|
+
"parent_before_exit_ms",
|
|
527
|
+
"test_spawn_return_ms",
|
|
528
|
+
];
|
|
529
|
+
|
|
530
|
+
#[cfg(unix)]
|
|
531
|
+
// Match the native helper's UNIX epoch millisecond clock exactly.
|
|
532
|
+
fn append_native_stage(path: &Path, key: &str) {
|
|
533
|
+
if !NATIVE_STAGE_KEYS.contains(&key) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
let Some(milliseconds) = std::time::SystemTime::now()
|
|
537
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
538
|
+
.ok()
|
|
539
|
+
.map(|duration| duration.as_millis())
|
|
540
|
+
else {
|
|
541
|
+
return;
|
|
542
|
+
};
|
|
543
|
+
let Ok(mut file) = std::fs::OpenOptions::new()
|
|
544
|
+
.create(true)
|
|
545
|
+
.append(true)
|
|
546
|
+
.open(path)
|
|
547
|
+
else {
|
|
548
|
+
return;
|
|
549
|
+
};
|
|
550
|
+
let _ = writeln!(file, "{key}={milliseconds}");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
#[cfg(unix)]
|
|
554
|
+
fn safe_native_stage(path: &Path) -> String {
|
|
555
|
+
let (read_state, text) = match std::fs::read_to_string(path) {
|
|
556
|
+
Ok(text) => ("ok", Some(text)),
|
|
557
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => ("missing", None),
|
|
558
|
+
Err(_) => ("unavailable", None),
|
|
559
|
+
};
|
|
560
|
+
let mut fields = BTreeMap::new();
|
|
561
|
+
if let Some(ref text) = text {
|
|
562
|
+
for line in text.lines() {
|
|
563
|
+
let Some((key, value)) = line.split_once('=') else {
|
|
564
|
+
continue;
|
|
565
|
+
};
|
|
566
|
+
if NATIVE_STAGE_KEYS.contains(&key)
|
|
567
|
+
&& value.len() <= 20
|
|
568
|
+
&& value.bytes().all(|byte| byte.is_ascii_digit())
|
|
569
|
+
{
|
|
570
|
+
fields.entry(key).or_insert(value);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
let field = |key: &str| fields.get(key).copied().unwrap_or("unknown");
|
|
575
|
+
format!(
|
|
576
|
+
"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={}",
|
|
577
|
+
field("parent_entry_ms"),
|
|
578
|
+
field("parent_before_child_spawn_ms"),
|
|
579
|
+
field("parent_after_child_spawn_ms"),
|
|
580
|
+
field("parent_before_exit_ms"),
|
|
581
|
+
field("test_spawn_return_ms"),
|
|
582
|
+
)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
#[cfg(unix)]
|
|
586
|
+
fn wait_for_native_parent_exit(
|
|
587
|
+
child: &mut std::process::Child,
|
|
588
|
+
stage: &Path,
|
|
589
|
+
) -> std::process::ExitStatus {
|
|
590
|
+
let deadline = Instant::now() + Duration::from_millis(500);
|
|
591
|
+
loop {
|
|
592
|
+
match child.try_wait() {
|
|
593
|
+
Ok(Some(status)) => return status,
|
|
594
|
+
Ok(None) if Instant::now() < deadline => {
|
|
595
|
+
std::thread::sleep(Duration::from_millis(1));
|
|
596
|
+
}
|
|
597
|
+
Ok(None) => {
|
|
598
|
+
let before_kill = safe_native_stage(stage);
|
|
599
|
+
let _ = child.kill();
|
|
600
|
+
let _ = child.wait();
|
|
601
|
+
let after_wait = safe_native_stage(stage);
|
|
602
|
+
panic!(
|
|
603
|
+
"native parent did not exit before test setup deadline; stage_before_kill={before_kill}; stage_after_wait={after_wait}"
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
Err(_) => {
|
|
607
|
+
let before_kill = safe_native_stage(stage);
|
|
608
|
+
let _ = child.kill();
|
|
609
|
+
let _ = child.wait();
|
|
610
|
+
let after_wait = safe_native_stage(stage);
|
|
611
|
+
panic!(
|
|
612
|
+
"native parent status could not be observed; stage_before_kill={before_kill}; stage_after_wait={after_wait}"
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/// Read/drain seam only: public `run_catalog` spawn coverage remains in the
|
|
620
|
+
/// normal success and fail-closed tests.
|
|
353
621
|
#[cfg(unix)]
|
|
354
622
|
#[test]
|
|
355
|
-
fn
|
|
623
|
+
fn reader_deadline_is_bounded_after_parent_exit_with_descendant_pipe() {
|
|
356
624
|
let path = native_timeout_fixture();
|
|
357
625
|
let receipt = native_timeout_receipt();
|
|
626
|
+
let stage = native_timeout_stage_receipt(&receipt);
|
|
358
627
|
assert!(!receipt.exists());
|
|
628
|
+
assert!(!stage.exists());
|
|
629
|
+
let mut parent = std::process::Command::new(&path)
|
|
630
|
+
.arg("--list-models")
|
|
631
|
+
.env("TEAM_AGENT_MODELS_TIMEOUT_RECEIPT", &receipt)
|
|
632
|
+
.env("TEAM_AGENT_MODELS_TIMEOUT_STAGE_RECEIPT", &stage)
|
|
633
|
+
.env("TEAM_AGENT_MODELS_TIMEOUT_MODE", "descendant")
|
|
634
|
+
.stdin(std::process::Stdio::null())
|
|
635
|
+
.stdout(std::process::Stdio::piped())
|
|
636
|
+
.stderr(std::process::Stdio::null())
|
|
637
|
+
.spawn()
|
|
638
|
+
.unwrap();
|
|
639
|
+
append_native_stage(&stage, "test_spawn_return_ms");
|
|
640
|
+
let parent_pid = parent.id();
|
|
641
|
+
let parent_status = wait_for_native_parent_exit(&mut parent, &stage);
|
|
642
|
+
let stdout = parent.stdout.take().expect("native parent stdout pipe");
|
|
359
643
|
let started = Instant::now();
|
|
360
644
|
let (result, observation) =
|
|
361
645
|
crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
|
|
362
646
|
&receipt,
|
|
363
647
|
Some("descendant"),
|
|
364
|
-
||
|
|
648
|
+
|| {
|
|
649
|
+
crate::lifecycle::launch::pi_mcp::run_pi_catalog_after_parent_exit_for_test(
|
|
650
|
+
stdout,
|
|
651
|
+
parent_pid,
|
|
652
|
+
parent_status,
|
|
653
|
+
Duration::from_millis(40),
|
|
654
|
+
1024,
|
|
655
|
+
)
|
|
656
|
+
},
|
|
365
657
|
);
|
|
366
|
-
|
|
658
|
+
let elapsed = started.elapsed();
|
|
659
|
+
assert!(
|
|
660
|
+
elapsed < Duration::from_millis(500),
|
|
661
|
+
"runner observation: {}",
|
|
662
|
+
safe_runner_observation(elapsed, &observation)
|
|
663
|
+
);
|
|
367
664
|
assert!(result.unwrap_err().contains("timed out"));
|
|
368
|
-
assert_eq!(
|
|
369
|
-
|
|
370
|
-
|
|
665
|
+
assert_eq!(
|
|
666
|
+
observation.parent_exit_success,
|
|
667
|
+
Some(true),
|
|
668
|
+
"runner observation: {}",
|
|
669
|
+
safe_runner_observation(elapsed, &observation)
|
|
670
|
+
);
|
|
371
671
|
assert_eq!(observation.parent_exit_code, Some(0));
|
|
372
672
|
assert!(observation.reader_timeout);
|
|
373
673
|
|
|
374
|
-
let fields =
|
|
674
|
+
let fields = wait_native_timeout_receipt(&receipt);
|
|
375
675
|
assert_eq!(fields["parent_started_once"], "1");
|
|
376
676
|
assert_eq!(fields["parent_argv_exact"], "1");
|
|
377
677
|
assert_eq!(fields["child_spawn_attempts"], "1");
|
|
@@ -384,6 +684,7 @@ mod tests {
|
|
|
384
684
|
);
|
|
385
685
|
assert_eq!(fields["child_pid"], fields["descendant_pid"]);
|
|
386
686
|
let _ = std::fs::remove_file(&receipt);
|
|
687
|
+
let _ = std::fs::remove_file(&stage);
|
|
387
688
|
let _ = std::fs::remove_file(format!("{}.sock", receipt.display()));
|
|
388
689
|
}
|
|
389
690
|
|
|
@@ -398,16 +699,72 @@ mod tests {
|
|
|
398
699
|
search: Some("sol".into()),
|
|
399
700
|
json: true,
|
|
400
701
|
};
|
|
401
|
-
let
|
|
402
|
-
|
|
403
|
-
|
|
702
|
+
let receipt = native_timeout_receipt();
|
|
703
|
+
assert!(!receipt.exists());
|
|
704
|
+
let started = Instant::now();
|
|
705
|
+
let (result, observation) =
|
|
706
|
+
crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
|
|
707
|
+
&receipt,
|
|
708
|
+
None,
|
|
709
|
+
|| cmd_models_with(&args, &path, Duration::from_secs(1), 1024),
|
|
710
|
+
);
|
|
711
|
+
let elapsed = started.elapsed();
|
|
712
|
+
let result = match result {
|
|
713
|
+
Ok(result) => result,
|
|
714
|
+
Err(_) => panic!(
|
|
715
|
+
"command boundary returned a CLI error; class=cli_error elapsed_ms={} parent_pid={:?} parent_exit_success={:?} parent_exit_code={:?} reader_timeout={}",
|
|
716
|
+
elapsed.as_millis(),
|
|
717
|
+
observation.parent_pid,
|
|
718
|
+
observation.parent_exit_success,
|
|
719
|
+
observation.parent_exit_code,
|
|
720
|
+
observation.reader_timeout,
|
|
721
|
+
),
|
|
404
722
|
};
|
|
405
|
-
let
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
723
|
+
let CmdOutput::Human(text) = &result.output else {
|
|
724
|
+
panic!(
|
|
725
|
+
"expected JSON projection; {}",
|
|
726
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
727
|
+
)
|
|
728
|
+
};
|
|
729
|
+
let value: Value = serde_json::from_str(text).unwrap_or_else(|_| {
|
|
730
|
+
panic!(
|
|
731
|
+
"expected valid JSON projection; {}",
|
|
732
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
733
|
+
)
|
|
734
|
+
});
|
|
735
|
+
assert_eq!(
|
|
736
|
+
value["schema_version"],
|
|
737
|
+
"models.v1",
|
|
738
|
+
"{}",
|
|
739
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
740
|
+
);
|
|
741
|
+
assert_eq!(
|
|
742
|
+
value["models"][0]["role_model"],
|
|
743
|
+
"openai-codex/gpt-5.6-sol",
|
|
744
|
+
"{}",
|
|
745
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
746
|
+
);
|
|
747
|
+
assert_eq!(
|
|
748
|
+
value["models"][0]["current"],
|
|
749
|
+
false,
|
|
750
|
+
"{}",
|
|
751
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
752
|
+
);
|
|
753
|
+
assert_eq!(
|
|
754
|
+
value["auth"],
|
|
755
|
+
"ok",
|
|
756
|
+
"{}",
|
|
757
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
758
|
+
);
|
|
759
|
+
let model_count = value["models"].as_array().map(|models| models.len());
|
|
760
|
+
assert_eq!(
|
|
761
|
+
model_count,
|
|
762
|
+
Some(1),
|
|
763
|
+
"{}",
|
|
764
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
765
|
+
);
|
|
766
|
+
let _ = std::fs::remove_file(&receipt);
|
|
767
|
+
let _ = std::fs::remove_file(format!("{}.sock", receipt.display()));
|
|
411
768
|
let human = cmd_models_with(
|
|
412
769
|
&ModelsArgs {
|
|
413
770
|
provider: "pi".into(),
|
|
@@ -441,12 +798,16 @@ mod tests {
|
|
|
441
798
|
#[test]
|
|
442
799
|
fn runner_fails_closed_for_nonzero_oversize_timeout_and_unavailable() {
|
|
443
800
|
let fail = native_timeout_fixture();
|
|
801
|
+
let started = Instant::now();
|
|
444
802
|
let (failed_result, fail_observation) = with_native_mode("exit7", || {
|
|
445
803
|
run_catalog(&fail, Duration::from_secs(1), 1024)
|
|
446
804
|
});
|
|
805
|
+
let elapsed = started.elapsed();
|
|
447
806
|
assert_eq!(
|
|
448
807
|
failed_result.unwrap_err(),
|
|
449
|
-
"Pi model catalog command failed"
|
|
808
|
+
"Pi model catalog command failed",
|
|
809
|
+
"runner observation: {}",
|
|
810
|
+
safe_runner_observation(elapsed, &fail_observation)
|
|
450
811
|
);
|
|
451
812
|
assert_eq!(fail_observation.spawn_count, 1);
|
|
452
813
|
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));
|