@team-agent/installer 0.5.74 → 0.5.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +9 -0
  4. package/README.zh.md +8 -0
  5. package/crates/team-agent/src/cli/adapters.rs +23 -96
  6. package/crates/team-agent/src/cli/emit.rs +35 -7
  7. package/crates/team-agent/src/cli/mod.rs +63 -42
  8. package/crates/team-agent/src/cli/models.rs +138 -27
  9. package/crates/team-agent/src/cli/status_port/brief.rs +1137 -0
  10. package/crates/team-agent/src/cli/status_port.rs +8 -0
  11. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  12. package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
  13. package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
  14. package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
  15. package/crates/team-agent/src/layout/worker_env.rs +366 -0
  16. package/crates/team-agent/src/leader/lease.rs +274 -40
  17. package/crates/team-agent/src/leader/registry.rs +177 -19
  18. package/crates/team-agent/src/leader/types.rs +4 -0
  19. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +93 -5
  20. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1820 -60
  21. package/crates/team-agent/src/lifecycle/launch/spawn.rs +4 -0
  22. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +138 -13
  23. package/crates/team-agent/src/lifecycle/restart/common.rs +3 -0
  24. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +11 -4
  25. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +45 -14
  26. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +1 -0
  27. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
  28. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +24 -28
  29. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +22 -10
  30. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +110 -31
  31. package/crates/team-agent/src/lifecycle/types.rs +4 -0
  32. package/crates/team-agent/src/messaging/delivery.rs +200 -29
  33. package/crates/team-agent/src/messaging/leader_channel.rs +12 -3
  34. package/crates/team-agent/src/messaging/watchers.rs +48 -4
  35. package/crates/team-agent/src/state/paths.rs +17 -2
  36. package/crates/team-agent/src/state/persist.rs +166 -6
  37. package/crates/team-agent/src/state/projection.rs +18 -2
  38. package/crates/team-agent/src/state/repository.rs +36 -0
  39. package/crates/team-agent/src/state/selector.rs +23 -3
  40. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +122 -0
  41. package/crates/team-agent/src/tmux_backend/tests.rs +350 -7
  42. package/crates/team-agent/src/tmux_backend.rs +217 -26
  43. package/crates/team-agent/src/transport/test_support.rs +18 -3
  44. package/crates/team-agent/src/transport.rs +99 -0
  45. package/package.json +4 -4
@@ -364,6 +364,78 @@ fn save_runtime_state_with_merge_options(
364
364
  topology_update_agent_ids: &[&str],
365
365
  receiver_update_team_key: Option<&str>,
366
366
  exact_owner_seed_to_clear: Option<&Value>,
367
+ ) -> Result<(), StateError> {
368
+ persist_runtime_state_with_merge_options_and_expected(
369
+ workspace,
370
+ state,
371
+ deleted_agent_ids,
372
+ skip_capture_backfill_team_key,
373
+ skip_capture_backfill_agent_ids,
374
+ topology_update_team_key,
375
+ topology_update_agent_ids,
376
+ receiver_update_team_key,
377
+ exact_owner_seed_to_clear,
378
+ None,
379
+ None,
380
+ )
381
+ }
382
+
383
+ pub(crate) fn save_runtime_state_with_receiver_authority_and_expected(
384
+ workspace: &Path,
385
+ state: &Value,
386
+ receiver_team_key: &str,
387
+ expected_owner: &Value,
388
+ expected_receiver: &Value,
389
+ ) -> Result<(), StateError> {
390
+ persist_runtime_state_with_merge_options_and_expected(
391
+ workspace,
392
+ state,
393
+ &[],
394
+ None,
395
+ &[],
396
+ None,
397
+ &[],
398
+ Some(receiver_team_key),
399
+ None,
400
+ Some((expected_owner, expected_receiver)),
401
+ None,
402
+ )
403
+ }
404
+
405
+ pub(crate) fn save_runtime_state_with_exact_owner_receiver_cleanup(
406
+ workspace: &Path,
407
+ state: &Value,
408
+ team_key: &str,
409
+ expected_owner: &Value,
410
+ expected_receiver: &Value,
411
+ ) -> Result<(), StateError> {
412
+ persist_runtime_state_with_merge_options_and_expected(
413
+ workspace,
414
+ state,
415
+ &[],
416
+ None,
417
+ &[],
418
+ None,
419
+ &[],
420
+ Some(team_key),
421
+ None,
422
+ None,
423
+ Some((team_key, expected_owner, expected_receiver)),
424
+ )
425
+ }
426
+
427
+ fn persist_runtime_state_with_merge_options_and_expected(
428
+ workspace: &Path,
429
+ state: &Value,
430
+ deleted_agent_ids: &[&str],
431
+ skip_capture_backfill_team_key: Option<&str>,
432
+ skip_capture_backfill_agent_ids: &[&str],
433
+ topology_update_team_key: Option<&str>,
434
+ topology_update_agent_ids: &[&str],
435
+ receiver_update_team_key: Option<&str>,
436
+ exact_owner_seed_to_clear: Option<&Value>,
437
+ expected_owner_receiver: Option<(&Value, &Value)>,
438
+ exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
367
439
  ) -> Result<(), StateError> {
368
440
  let path = runtime_state_path(workspace);
369
441
  // Python `state.py:497`:先对入参 state 跑 `_migrate_state_identity`(就地填缺失 leader uuid)。
@@ -385,7 +457,9 @@ fn save_runtime_state_with_merge_options(
385
457
  // Exact-seed cleanup must observe lock-held disk, not a pre-lock cache or
386
458
  // byte-equality hit. A matching in-memory document would otherwise no-op
387
459
  // and leave the seed, or skip copying a concurrent owner.
388
- let skip_pre_lock_fast_path = exact_owner_seed_to_clear.is_some();
460
+ let skip_pre_lock_fast_path = exact_owner_seed_to_clear.is_some()
461
+ || expected_owner_receiver.is_some()
462
+ || exact_owner_receiver_to_clear.is_some();
389
463
  if !skip_pre_lock_fast_path && cache_equals(&path, &migrated) {
390
464
  return Ok(());
391
465
  }
@@ -418,7 +492,21 @@ fn save_runtime_state_with_merge_options(
418
492
  if let Some(parent) = path.parent() {
419
493
  std::fs::create_dir_all(parent)?;
420
494
  }
421
- if let Some(latest) = read_latest_state_under_lock(workspace, &path) {
495
+ let latest = read_latest_state_under_lock(workspace, &path);
496
+ if let Some((expected_owner, expected_receiver)) = expected_owner_receiver {
497
+ let expected_matches = latest.as_ref().is_some_and(|latest| {
498
+ latest_team_owner_receiver(latest, receiver_update_team_key)
499
+ .is_some_and(|(owner, receiver)| {
500
+ owner == expected_owner && receiver == expected_receiver
501
+ })
502
+ });
503
+ if !expected_matches {
504
+ return Err(StateError::SaveConflict(
505
+ "fresh caller owner/receiver changed before commit".to_string(),
506
+ ));
507
+ }
508
+ }
509
+ if let Some(latest) = latest {
422
510
  let deleted = deleted_agent_ids
423
511
  .iter()
424
512
  .copied()
@@ -455,6 +543,7 @@ fn save_runtime_state_with_merge_options(
455
543
  &topology_updates,
456
544
  &receiver_updates,
457
545
  receiver_update_team_key.zip(exact_owner_seed_to_clear),
546
+ exact_owner_receiver_to_clear,
458
547
  )?;
459
548
  }
460
549
  // Stage 3 save-output strip second pass (defence-in-depth): after the
@@ -523,6 +612,7 @@ fn apply_persist_merge_contract(
523
612
  topology_updates: &BTreeSet<(String, String)>,
524
613
  receiver_updates: &BTreeSet<String>,
525
614
  exact_owner_seed_to_clear: Option<(&str, &Value)>,
615
+ exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
526
616
  ) -> Result<(), StateError> {
527
617
  // A0/R1: the projection gate only guards the TOP-LEVEL passes (top-level agents and
528
618
  // the top-level<->active-team cross projections depend on which team is active); the
@@ -645,6 +735,16 @@ fn apply_persist_merge_contract(
645
735
  apply_lock_held_exact_owner_cleanup(incoming_entry, latest_entry, seed);
646
736
  }
647
737
  }
738
+ if let Some((clear_team, seed, expected_receiver)) = exact_owner_receiver_to_clear {
739
+ if team == clear_team {
740
+ apply_lock_held_exact_owner_receiver_cleanup(
741
+ incoming_entry,
742
+ latest_entry,
743
+ seed,
744
+ Some(expected_receiver),
745
+ );
746
+ }
747
+ }
648
748
  preserve_latest_endpoint_convergence_fields(incoming_entry, latest_entry);
649
749
  }
650
750
  }
@@ -776,14 +876,26 @@ fn preserve_latest_ownership_fields(
776
876
  }
777
877
 
778
878
  fn apply_lock_held_exact_owner_cleanup(incoming: &mut Value, latest: &Value, seed: &Value) {
779
- // Compare the lock-held disk owner to the attempt-local seed. Equal-epoch
780
- // concurrent owners must survive; only the exact seed is cleared, and the
781
- // epoch is not bumped so a same-epoch takeover cannot be overwritten.
879
+ apply_lock_held_exact_owner_receiver_cleanup(incoming, latest, seed, None);
880
+ }
881
+
882
+ fn apply_lock_held_exact_owner_receiver_cleanup(
883
+ incoming: &mut Value,
884
+ latest: &Value,
885
+ seed: &Value,
886
+ expected_receiver: Option<&Value>,
887
+ ) {
888
+ // Compare the lock-held disk owner and, when supplied, receiver to the
889
+ // attempt-local grant. A same-owner receiver update is a concurrent
890
+ // winner and must survive cleanup just like a different owner/epoch.
782
891
  let latest_owner = latest.get("team_owner");
892
+ let receiver_matches = expected_receiver.is_none_or(|expected| {
893
+ latest.get("leader_receiver") == Some(expected)
894
+ });
783
895
  let Some(incoming_obj) = incoming.as_object_mut() else {
784
896
  return;
785
897
  };
786
- if latest_owner == Some(seed) {
898
+ if latest_owner == Some(seed) && receiver_matches {
787
899
  incoming_obj.insert("team_owner".to_string(), Value::Null);
788
900
  incoming_obj.insert("leader_receiver".to_string(), Value::Null);
789
901
  if let Some(epoch) = latest
@@ -873,6 +985,21 @@ fn latest_has_preferable_ownership(
873
985
  && (ownership_attached(latest) || !ownership_attached(incoming))
874
986
  }
875
987
 
988
+ fn latest_team_owner_receiver<'a>(
989
+ latest: &'a Value,
990
+ team_key: Option<&str>,
991
+ ) -> Option<(&'a Value, &'a Value)> {
992
+ let entry = if let Some(team_key) = team_key {
993
+ latest
994
+ .get("teams")
995
+ .and_then(Value::as_object)
996
+ .and_then(|teams| teams.get(team_key))?
997
+ } else {
998
+ latest
999
+ };
1000
+ Some((entry.get("team_owner")?, entry.get("leader_receiver")?))
1001
+ }
1002
+
876
1003
  fn ownership_epoch(state: &Value) -> u64 {
877
1004
  state
878
1005
  .get("owner_epoch")
@@ -2985,6 +3112,7 @@ mod tests {
2985
3112
  &BTreeSet::new(),
2986
3113
  &BTreeSet::new(),
2987
3114
  None,
3115
+ None,
2988
3116
  )
2989
3117
  .unwrap();
2990
3118
 
@@ -3020,6 +3148,7 @@ mod tests {
3020
3148
  &BTreeSet::new(),
3021
3149
  &receiver_updates,
3022
3150
  None,
3151
+ None,
3023
3152
  )
3024
3153
  .unwrap();
3025
3154
 
@@ -3219,6 +3348,37 @@ mod tests {
3219
3348
  );
3220
3349
  }
3221
3350
 
3351
+ #[test]
3352
+ fn lock_held_exact_seed_receiver_cleanup_preserves_same_owner_receiver_update() {
3353
+ let ws = temp_ws();
3354
+ save_runtime_state(
3355
+ &ws,
3356
+ &team_state_with_binding(&ws, seed_owner(1), seed_receiver(1), 1),
3357
+ )
3358
+ .unwrap();
3359
+ let seed = persisted_fresh_owner(&ws);
3360
+ let seeded_receiver = load_runtime_state(&ws).unwrap()["teams"]["fresh"]
3361
+ ["leader_receiver"]
3362
+ .clone();
3363
+ let mut winner = team_state_with_binding(&ws, seed_owner(1), takeover_receiver(1), 1);
3364
+ winner["teams"]["fresh"]["team_owner"] = seed.clone();
3365
+ save_runtime_state_with_receiver_authority(&ws, &winner, "fresh", None).unwrap();
3366
+ save_runtime_state_with_exact_owner_receiver_cleanup(
3367
+ &ws,
3368
+ &tombstone_incoming(&ws, 1),
3369
+ "fresh",
3370
+ &seed,
3371
+ &seeded_receiver,
3372
+ )
3373
+ .unwrap();
3374
+ assert_fresh_binding(
3375
+ &load_runtime_state(&ws).unwrap(),
3376
+ &seed,
3377
+ &takeover_receiver(1),
3378
+ 1,
3379
+ );
3380
+ }
3381
+
3222
3382
  #[test]
3223
3383
  fn lock_held_exact_seed_cleanup_preserves_epoch2_takeover_binding() {
3224
3384
  let ws = temp_ws();
@@ -16,7 +16,8 @@ use serde_json::{json, Map, Value};
16
16
 
17
17
  use super::StateError;
18
18
  use crate::state::persist::{
19
- load_runtime_state, save_runtime_state_with_deleted_agents,
19
+ load_runtime_state, load_runtime_state_without_migrations,
20
+ save_runtime_state_with_deleted_agents,
20
21
  save_runtime_state_with_lifecycle_topology_authority,
21
22
  save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip,
22
23
  save_runtime_state_with_team_tombstone_lifecycle_topology_authority,
@@ -504,7 +505,22 @@ pub fn resolve_runtime_team_scope(
504
505
  workspace: &Path,
505
506
  team: Option<&str>,
506
507
  ) -> Result<TeamScopeResolution, StateError> {
507
- let state = load_runtime_state(workspace)?;
508
+ resolve_runtime_team_scope_loaded(load_runtime_state(workspace)?, team)
509
+ }
510
+
511
+ /// Same team selection as [`resolve_runtime_team_scope`], but never migrates or
512
+ /// writes runtime state. Status and other read-only CLI surfaces must use this.
513
+ pub fn resolve_runtime_team_scope_readonly(
514
+ workspace: &Path,
515
+ team: Option<&str>,
516
+ ) -> Result<TeamScopeResolution, StateError> {
517
+ resolve_runtime_team_scope_loaded(load_runtime_state_without_migrations(workspace)?, team)
518
+ }
519
+
520
+ fn resolve_runtime_team_scope_loaded(
521
+ state: Value,
522
+ team: Option<&str>,
523
+ ) -> Result<TeamScopeResolution, StateError> {
508
524
  let alive = team_state_candidates(&state);
509
525
  // Python `if team:` —— 空串 falsy,等同无 team(对抗 P1:此前 Some("") 误入 team 分支,空
510
526
  // team_dir 匹配致歧义/未找到错误串漂移)。
@@ -48,6 +48,10 @@ use super::persist::{
48
48
  save_runtime_state_with_lifecycle_topology_authority as helper_write_root_with_lifecycle_topology_authority,
49
49
  save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip as helper_write_root_with_lifecycle_topology_authority_and_capture_backfill_skip,
50
50
  save_runtime_state_with_receiver_authority as helper_write_root_with_receiver_authority,
51
+ save_runtime_state_with_receiver_authority_and_expected
52
+ as helper_write_root_with_receiver_authority_and_expected,
53
+ save_runtime_state_with_exact_owner_receiver_cleanup
54
+ as helper_write_root_with_exact_owner_receiver_cleanup,
51
55
  save_runtime_state_with_team_tombstone_lifecycle_topology_authority as helper_write_root_with_team_tombstone_lifecycle_topology_authority,
52
56
  save_runtime_state_with_team_tombstoned_agents as helper_write_root_with_team_tombstoned_agents,
53
57
  save_runtime_state_without_migrations as helper_write_root_without_migrations,
@@ -206,10 +210,20 @@ pub enum StateWriteIntent<'a> {
206
210
  ClaimLeader {
207
211
  team_key: &'a str,
208
212
  },
213
+ ClaimLeaderFreshCaller {
214
+ team_key: &'a str,
215
+ expected_owner: &'a Value,
216
+ expected_receiver: &'a Value,
217
+ },
209
218
  ClearExactTeamOwner {
210
219
  team_key: &'a str,
211
220
  seed: &'a Value,
212
221
  },
222
+ ClearExactTeamOwnerAndReceiver {
223
+ team_key: &'a str,
224
+ seed: &'a Value,
225
+ expected_receiver: &'a Value,
226
+ },
213
227
  LeaderBindingRestoreNonTargetTeams {
214
228
  target_team_key: &'a str,
215
229
  },
@@ -389,9 +403,31 @@ fn route_direct(
389
403
  StateWriteIntent::ClaimLeader { team_key } => {
390
404
  helper_write_root_with_receiver_authority(workspace, state, team_key, None)
391
405
  }
406
+ StateWriteIntent::ClaimLeaderFreshCaller {
407
+ team_key,
408
+ expected_owner,
409
+ expected_receiver,
410
+ } => helper_write_root_with_receiver_authority_and_expected(
411
+ workspace,
412
+ state,
413
+ team_key,
414
+ expected_owner,
415
+ expected_receiver,
416
+ ),
392
417
  StateWriteIntent::ClearExactTeamOwner { team_key, seed } => {
393
418
  helper_write_root_with_receiver_authority(workspace, state, team_key, Some(seed))
394
419
  }
420
+ StateWriteIntent::ClearExactTeamOwnerAndReceiver {
421
+ team_key,
422
+ seed,
423
+ expected_receiver,
424
+ } => helper_write_root_with_exact_owner_receiver_cleanup(
425
+ workspace,
426
+ state,
427
+ team_key,
428
+ seed,
429
+ expected_receiver,
430
+ ),
395
431
  StateWriteIntent::LeaderBindingRestoreNonTargetTeams { .. } => {
396
432
  helper_write_root_without_migrations(workspace, state)
397
433
  }
@@ -7,7 +7,9 @@ use serde_json::Value;
7
7
 
8
8
  use crate::model::paths::{canonical_run_workspace, runtime_spec_path, team_workspace};
9
9
  use crate::state::persist::runtime_state_path;
10
- use crate::state::projection::resolve_runtime_team_scope;
10
+ use crate::state::projection::{
11
+ resolve_runtime_team_scope, resolve_runtime_team_scope_readonly, TeamScopeResolution,
12
+ };
11
13
  use crate::state::StateError;
12
14
 
13
15
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -35,6 +37,24 @@ pub fn resolve_active_team(
35
37
  input: &Path,
36
38
  team: Option<&str>,
37
39
  mode: SelectorMode,
40
+ ) -> Result<SelectedTeam, StateError> {
41
+ resolve_active_team_with(input, team, mode, resolve_runtime_team_scope)
42
+ }
43
+
44
+ /// Status-path selector: in-memory projection only, no runtime-state writeback.
45
+ pub fn resolve_active_team_readonly(
46
+ input: &Path,
47
+ team: Option<&str>,
48
+ mode: SelectorMode,
49
+ ) -> Result<SelectedTeam, StateError> {
50
+ resolve_active_team_with(input, team, mode, resolve_runtime_team_scope_readonly)
51
+ }
52
+
53
+ fn resolve_active_team_with(
54
+ input: &Path,
55
+ team: Option<&str>,
56
+ mode: SelectorMode,
57
+ resolve_scope: fn(&Path, Option<&str>) -> Result<TeamScopeResolution, StateError>,
38
58
  ) -> Result<SelectedTeam, StateError> {
39
59
  let explicit_spec = input.join("team.spec.yaml");
40
60
  let (run_workspace, resolved) = if explicit_spec.exists() {
@@ -44,7 +64,7 @@ pub fn resolve_active_team(
44
64
  } else {
45
65
  team_run
46
66
  };
47
- let resolved = resolve_runtime_team_scope(&run, team)?;
67
+ let resolved = resolve_scope(&run, team)?;
48
68
  (run, resolved)
49
69
  } else {
50
70
  let run =
@@ -59,7 +79,7 @@ pub fn resolve_active_team(
59
79
  input.display()
60
80
  )));
61
81
  }
62
- let resolved = resolve_runtime_team_scope(&run, team)?;
82
+ let resolved = resolve_scope(&run, team)?;
63
83
  (run, resolved)
64
84
  };
65
85
  let state = resolved.state;
@@ -0,0 +1,122 @@
1
+ use std::fs::OpenOptions;
2
+ use std::io::{Read, Write};
3
+ use std::os::unix::net::{UnixListener, UnixStream};
4
+ use std::path::{Path, PathBuf};
5
+ use std::process::{Command, Stdio};
6
+ use std::time::Duration;
7
+
8
+ const CHILD_FLAG: &str = "--team-agent-models-timeout-child";
9
+ const LIST_MODELS_FLAG: &str = "--list-models";
10
+ const RECEIPT_ENV: &str = "TEAM_AGENT_MODELS_TIMEOUT_RECEIPT";
11
+ const MODE_ENV: &str = "TEAM_AGENT_MODELS_TIMEOUT_MODE";
12
+ const READY_SOCKET_ENV: &str = "TEAM_AGENT_MODELS_TIMEOUT_READY_SOCKET";
13
+ const STDOUT_MARKER: &[u8] = b"__team_agent_models_timeout_descendant_stdout_v1__\n";
14
+
15
+ fn receipt_path() -> PathBuf {
16
+ std::env::var_os(RECEIPT_ENV)
17
+ .map(PathBuf::from)
18
+ .unwrap_or_else(|| std::process::exit(4))
19
+ }
20
+
21
+ fn append_receipt(path: &Path, key: &str, value: impl std::fmt::Display) -> Result<(), ()> {
22
+ let mut receipt = OpenOptions::new()
23
+ .create(true)
24
+ .append(true)
25
+ .open(path)
26
+ .map_err(|_| ())?;
27
+ writeln!(receipt, "{key}={value}").map_err(|_| ())
28
+ }
29
+
30
+ fn child_main(args: &[String]) {
31
+ if args.len() != 1 || args[0] != CHILD_FLAG {
32
+ std::process::exit(5);
33
+ }
34
+ let receipt = receipt_path();
35
+ let socket = std::env::var_os(READY_SOCKET_ENV)
36
+ .map(PathBuf::from)
37
+ .unwrap_or_else(|| std::process::exit(5));
38
+
39
+ let mut stdout = std::io::stdout();
40
+ if stdout.write_all(STDOUT_MARKER).is_err() || stdout.flush().is_err() {
41
+ std::process::exit(5);
42
+ }
43
+ if append_receipt(&receipt, "child_argv_exact", 1).is_err()
44
+ || append_receipt(&receipt, "descendant_pid", std::process::id()).is_err()
45
+ || append_receipt(&receipt, "descendant_started", 1).is_err()
46
+ || append_receipt(&receipt, "descendant_stdout_inherited", 1).is_err()
47
+ {
48
+ std::process::exit(5);
49
+ }
50
+
51
+ let mut ready = UnixStream::connect(socket).unwrap_or_else(|_| std::process::exit(5));
52
+ if ready.write_all(b"1").is_err() {
53
+ std::process::exit(5);
54
+ }
55
+ std::thread::sleep(Duration::from_secs(2));
56
+ }
57
+
58
+ fn main() {
59
+ let args: Vec<String> = std::env::args().skip(1).collect();
60
+ if args.first().is_some_and(|arg| arg == CHILD_FLAG) {
61
+ child_main(&args);
62
+ return;
63
+ }
64
+ if args.len() != 1 || args[0] != LIST_MODELS_FLAG {
65
+ std::process::exit(2);
66
+ }
67
+
68
+ match std::env::var(MODE_ENV) {
69
+ Ok(mode) if mode == "exit7" => {
70
+ eprintln!("sensitive-token");
71
+ std::process::exit(7);
72
+ }
73
+ Ok(mode) if mode == "oversize" => {
74
+ let mut stdout = std::io::stdout();
75
+ let bytes = [b'x'; 64];
76
+ let _ = stdout.write_all(&bytes);
77
+ let _ = stdout.flush();
78
+ return;
79
+ }
80
+ Ok(mode) if mode == "sleep" => {
81
+ std::thread::sleep(Duration::from_secs(2));
82
+ return;
83
+ }
84
+ Ok(mode) if mode == "descendant" => {}
85
+ Err(std::env::VarError::NotPresent) => {}
86
+ Ok(_) | Err(std::env::VarError::NotUnicode(_)) => std::process::exit(2),
87
+ }
88
+
89
+ let receipt = receipt_path();
90
+ let socket = PathBuf::from(format!("{}.sock", receipt.display()));
91
+ let _ = std::fs::remove_file(&socket);
92
+ let listener = UnixListener::bind(&socket).unwrap_or_else(|_| std::process::exit(4));
93
+ if append_receipt(&receipt, "parent_started_once", 1).is_err()
94
+ || append_receipt(&receipt, "parent_argv_exact", 1).is_err()
95
+ || append_receipt(&receipt, "parent_pid", std::process::id()).is_err()
96
+ || append_receipt(&receipt, "child_spawn_attempts", 1).is_err()
97
+ {
98
+ std::process::exit(4);
99
+ }
100
+
101
+ let executable = std::env::current_exe().unwrap_or_else(|_| std::process::exit(4));
102
+ let child = Command::new(executable)
103
+ .arg(CHILD_FLAG)
104
+ .env(RECEIPT_ENV, &receipt)
105
+ .env(READY_SOCKET_ENV, &socket)
106
+ .stdin(Stdio::null())
107
+ .stdout(Stdio::inherit())
108
+ .stderr(Stdio::null())
109
+ .spawn()
110
+ .unwrap_or_else(|_| std::process::exit(4));
111
+ let (mut ready, _) = listener.accept().unwrap_or_else(|_| std::process::exit(4));
112
+ let mut byte = [0_u8; 1];
113
+ if ready.read_exact(&mut byte).is_err() || byte != [b'1'] {
114
+ std::process::exit(4);
115
+ }
116
+ if append_receipt(&receipt, "child_pid", child.id()).is_err() {
117
+ std::process::exit(4);
118
+ }
119
+ let _ = std::fs::remove_file(&socket);
120
+ // The parent exits successfully without waiting; the child retains the
121
+ // inherited stdout pipe and self-exits after the real reader deadline.
122
+ }