@team-agent/installer 0.5.102 → 0.5.103

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.
@@ -15,14 +15,13 @@
15
15
 
16
16
  use std::collections::BTreeSet;
17
17
  use std::fs::OpenOptions;
18
- use std::io::{Read, Write};
18
+ use std::io::Write;
19
19
  #[cfg(test)]
20
20
  use std::cell::RefCell;
21
21
  use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
22
22
  use std::path::{Path, PathBuf};
23
- use std::process::{Command, Stdio};
24
- use std::sync::mpsc;
25
- use std::time::{Duration, Instant};
23
+ use std::process::Command;
24
+ use std::time::Duration;
26
25
 
27
26
  use sha2::{Digest, Sha256};
28
27
 
@@ -132,62 +131,9 @@ pub(crate) fn new_pi_session_id() -> SessionId {
132
131
  /// errors: 非 UTF-8、header/row 异常、重复或空 catalog 时返回 ProviderError
133
132
  /// ---
134
133
  pub(crate) fn parse_pi_list_models_table(bytes: &[u8]) -> Result<Vec<String>, ProviderError> {
135
- let text = std::str::from_utf8(bytes).map_err(|error| {
136
- ProviderError::Command(format!("Pi model catalog is not UTF-8: {error}"))
137
- })?;
138
- let mut lines = text.lines();
139
- let header = lines
140
- .next()
141
- .ok_or_else(|| ProviderError::Command("Pi model catalog is empty".to_string()))?;
142
- let header_columns = header.split_whitespace().collect::<Vec<_>>();
143
- if header_columns.get(0) != Some(&"provider") || header_columns.get(1) != Some(&"model") {
144
- return Err(ProviderError::Command(
145
- "Pi model catalog has an unexpected header".to_string(),
146
- ));
147
- }
148
- let mut seen = BTreeSet::new();
149
- let mut models = Vec::new();
150
- for (index, line) in lines.enumerate() {
151
- if line.trim().is_empty() {
152
- continue;
153
- }
154
- let malformed = || {
155
- ProviderError::Command(format!(
156
- "Pi model catalog row {} is malformed",
157
- index + 2
158
- ))
159
- };
160
- let (provider, model) = line
161
- .trim()
162
- .split_once(char::is_whitespace)
163
- .ok_or_else(malformed)?;
164
- let mut model = model.trim();
165
- // Pi prints the raw model id, which may contain spaces. Only the
166
- // capability columns following it are single whitespace-free values.
167
- for _ in 2..header_columns.len() {
168
- model = model
169
- .rsplit_once(char::is_whitespace)
170
- .ok_or_else(malformed)?
171
- .0
172
- .trim_end();
173
- }
174
- if model.is_empty() {
175
- return Err(malformed());
176
- }
177
- let exact = format!("{provider}/{model}");
178
- if !seen.insert(exact.clone()) {
179
- return Err(ProviderError::Command(format!(
180
- "Pi model catalog contains duplicate exact id {exact:?}"
181
- )));
182
- }
183
- models.push(exact);
184
- }
185
- if models.is_empty() {
186
- return Err(ProviderError::Command(
187
- "Pi model catalog contains no models".to_string(),
188
- ));
189
- }
190
- Ok(models)
134
+ crate::provider::model_catalog::parse_pi_catalog(bytes)
135
+ .map(|records| records.into_iter().map(|record| record.id).collect())
136
+ .map_err(|error| ProviderError::Command(error.pi_legacy_message()))
191
137
  }
192
138
 
193
139
  /// ---
@@ -658,43 +604,6 @@ fn observe_pi_catalog_test(f: impl FnOnce(&mut PiCatalogTestContext)) {
658
604
  });
659
605
  }
660
606
 
661
- fn spawn_catalog_reader(
662
- stdout: std::process::ChildStdout,
663
- max_bytes: u64,
664
- ) -> mpsc::Receiver<(std::io::Result<usize>, Vec<u8>)> {
665
- let (sender, receiver) = mpsc::channel();
666
- std::thread::spawn(move || {
667
- let mut bytes = Vec::new();
668
- let result = stdout.take(max_bytes + 1).read_to_end(&mut bytes);
669
- let _ = sender.send((result, bytes));
670
- });
671
- receiver
672
- }
673
-
674
- fn finish_catalog_reader(
675
- receiver: mpsc::Receiver<(std::io::Result<usize>, Vec<u8>)>,
676
- status: std::process::ExitStatus,
677
- timeout: Duration,
678
- max_bytes: u64,
679
- ) -> Result<Vec<u8>, String> {
680
- let (result, bytes) = match receiver.recv_timeout(timeout) {
681
- Ok(value) => value,
682
- Err(_) => {
683
- #[cfg(test)]
684
- observe_pi_catalog_test(|context| context.observation.reader_timeout = true);
685
- return Err("Pi model catalog command timed out".to_string());
686
- }
687
- };
688
- if !status.success() {
689
- return Err("Pi model catalog command failed".into());
690
- }
691
- result.map_err(|_| "Pi model catalog could not be read".to_string())?;
692
- if bytes.len() as u64 > max_bytes {
693
- return Err("Pi model catalog exceeds the bounded output limit".into());
694
- }
695
- Ok(bytes)
696
- }
697
-
698
607
  #[cfg(test)]
699
608
  pub(crate) fn run_pi_catalog_after_parent_exit_for_test(
700
609
  stdout: std::process::ChildStdout,
@@ -708,86 +617,53 @@ pub(crate) fn run_pi_catalog_after_parent_exit_for_test(
708
617
  context.observation.parent_exit_success = Some(status.success());
709
618
  context.observation.parent_exit_code = status.code();
710
619
  });
711
- finish_catalog_reader(
712
- spawn_catalog_reader(stdout, max_bytes),
713
- status,
714
- timeout,
715
- max_bytes,
620
+ crate::provider::model_catalog::finish_stdout_after_exit(
621
+ stdout, status, timeout, max_bytes, &mut PiCatalogObserver,
716
622
  )
623
+ .map_err(|error| error.pi_legacy_message())
717
624
  }
718
625
 
719
- /// Discover candidates with one PATH-first catalog observation. This deliberately
720
- /// does not resolve adapters or scan wrapper chains; callers use it only for
721
- /// rejecting an unqualified role model before lifecycle mutation.
626
+ /// Shared bounded native-command runner; launch preflight keeps its exact model gate.
722
627
  pub(crate) fn run_pi_catalog(
723
628
  executable: &Path,
724
629
  timeout: Duration,
725
630
  max_bytes: u64,
726
631
  ) -> Result<Vec<u8>, String> {
727
- let mut command = Command::new(executable);
728
- command.arg("--list-models");
632
+ let mut extra_env = Vec::new();
729
633
  #[cfg(test)]
730
634
  observe_pi_catalog_test(|context| {
731
635
  context.observation.spawn_count += 1;
732
636
  context.observation.argv.push("--list-models".to_string());
733
- command.env("TEAM_AGENT_MODELS_TIMEOUT_RECEIPT", &context.receipt);
637
+ extra_env.push(("TEAM_AGENT_MODELS_TIMEOUT_RECEIPT".into(), context.receipt.as_os_str().into()));
734
638
  if let Some(mode) = context.helper_mode.as_deref() {
735
- command.env("TEAM_AGENT_MODELS_TIMEOUT_MODE", mode);
639
+ extra_env.push(("TEAM_AGENT_MODELS_TIMEOUT_MODE".into(), mode.into()));
736
640
  }
737
641
  });
738
- let mut child = command
739
- .stdin(Stdio::null())
740
- .stdout(Stdio::piped())
741
- .stderr(Stdio::null())
742
- .spawn()
743
- .map_err(|error| {
744
- #[cfg(test)]
745
- {
746
- eprintln!(
747
- "run_pi_catalog spawn failed kind={:?} raw_os_error={:?}",
748
- error.kind(),
749
- error.raw_os_error()
750
- );
751
- }
752
- let _ = error;
753
- "Pi executable is unavailable on PATH".to_string()
754
- })?;
642
+ crate::provider::model_catalog::run_command(
643
+ executable, &["--list-models"], None, None, &[], &extra_env, timeout, max_bytes,
644
+ &mut PiCatalogObserver,
645
+ )
646
+ .map_err(|error| error.pi_legacy_message())
647
+ }
648
+
649
+ struct PiCatalogObserver;
650
+ impl crate::provider::model_catalog::CatalogObserver for PiCatalogObserver {
755
651
  #[cfg(test)]
756
- observe_pi_catalog_test(|context| context.observation.parent_pid = Some(child.id()));
757
- let stdout = child.stdout.take().ok_or_else(|| {
758
- let _ = child.kill();
759
- let _ = child.wait();
760
- "Pi model catalog output unavailable".to_string()
761
- })?;
762
- let receiver = spawn_catalog_reader(stdout, max_bytes);
763
- let start = Instant::now();
764
- loop {
765
- match child.try_wait() {
766
- Ok(Some(status)) => {
767
- #[cfg(test)]
768
- observe_pi_catalog_test(|context| {
769
- context.observation.parent_exit_success = Some(status.success());
770
- context.observation.parent_exit_code = status.code();
771
- });
772
- return finish_catalog_reader(
773
- receiver,
774
- status,
775
- timeout.saturating_sub(start.elapsed()),
776
- max_bytes,
777
- );
778
- }
779
- Ok(None) if start.elapsed() < timeout => std::thread::sleep(Duration::from_millis(20)),
780
- Ok(None) => {
781
- let _ = child.kill();
782
- let _ = child.wait();
783
- return Err("Pi model catalog command timed out".into());
784
- }
785
- Err(_) => {
786
- let _ = child.kill();
787
- let _ = child.wait();
788
- return Err("Pi model catalog command could not be observed".into());
789
- }
790
- }
652
+ fn spawned(&mut self, pid: u32) {
653
+ observe_pi_catalog_test(|context| context.observation.parent_pid = Some(pid));
654
+ }
655
+
656
+ #[cfg(test)]
657
+ fn exited(&mut self, status: &std::process::ExitStatus) {
658
+ observe_pi_catalog_test(|context| {
659
+ context.observation.parent_exit_success = Some(status.success());
660
+ context.observation.parent_exit_code = status.code();
661
+ });
662
+ }
663
+
664
+ #[cfg(test)]
665
+ fn output_timeout(&mut self) {
666
+ observe_pi_catalog_test(|context| context.observation.reader_timeout = true);
791
667
  }
792
668
  }
793
669
 
@@ -1046,25 +1046,6 @@ fn leader_result_presentation(task_id: &str) -> super::presentation::Presentatio
1046
1046
  )
1047
1047
  }
1048
1048
 
1049
- const LEADER_NOTIFICATION_LIMIT_BYTES: usize = 160;
1050
-
1051
- fn truncate_utf8(text: &str, max_bytes: usize) -> String {
1052
- if text.len() <= max_bytes {
1053
- return text.to_string();
1054
- }
1055
- let suffix = "...";
1056
- let budget = max_bytes.saturating_sub(suffix.len());
1057
- let mut end = 0;
1058
- for (index, ch) in text.char_indices() {
1059
- let next = index + ch.len_utf8();
1060
- if next > budget {
1061
- break;
1062
- }
1063
- end = next;
1064
- }
1065
- format!("{}{}", &text[..end], suffix)
1066
- }
1067
-
1068
1049
  fn format_report_result_leader_summary(
1069
1050
  result_id: &str,
1070
1051
  task_id: &str,
@@ -1075,19 +1056,8 @@ fn format_report_result_leader_summary(
1075
1056
  let summary = envelope
1076
1057
  .get("summary")
1077
1058
  .and_then(serde_json::Value::as_str)
1078
- .unwrap_or("")
1079
- .lines()
1080
- .next()
1081
- .unwrap_or("")
1082
- .chars()
1083
- .filter(|ch| !ch.is_control())
1084
- .collect::<String>();
1085
- truncate_utf8(
1086
- &format!(
1087
- "Task {task_id} reported {status} from {agent_id}: {summary}; Result id: {result_id}"
1088
- ),
1089
- LEADER_NOTIFICATION_LIMIT_BYTES,
1090
- )
1059
+ .unwrap_or("");
1060
+ format!("Task {task_id} reported {status} from {agent_id}: {summary}; Result id: {result_id}")
1091
1061
  }
1092
1062
 
1093
1063
  fn auto_finalize_result(
@@ -1441,18 +1411,17 @@ mod tests {
1441
1411
  }
1442
1412
 
1443
1413
  #[test]
1444
- fn leader_result_summary_is_single_line_and_byte_bounded() {
1445
- let envelope = serde_json::json!({"summary": "中".repeat(4096)});
1414
+ fn leader_result_summary_preserves_long_multiline_text_verbatim() {
1415
+ let source = format!("首行\nbranch: fix/keyboard-receive-and-compose\ncommit: {}", "a1b2c3d4e5f67890".repeat(24));
1416
+ let envelope = serde_json::json!({"summary": source});
1446
1417
  let summary = super::format_report_result_leader_summary(
1447
- "res-1",
1448
- "task-1",
1449
- "worker",
1450
- "success",
1451
- &envelope,
1418
+ "res-1", "task-1", "worker", "success", &envelope,
1452
1419
  );
1453
- assert!(summary.len() <= 160);
1454
- assert!(!summary.contains('\n'));
1455
- assert!(summary.ends_with("..."));
1420
+ let source = envelope["summary"].as_str().unwrap();
1421
+ assert_eq!(summary, format!("Task task-1 reported success from worker: {source}; Result id: res-1"));
1422
+ assert!(summary.len() > 160);
1423
+ assert!(summary.contains("\nbranch: fix/keyboard-receive-and-compose\ncommit: "));
1424
+ assert!(summary.contains("a1b2c3d4e5f67890".repeat(24).as_str()));
1456
1425
  }
1457
1426
 
1458
1427
  #[test]
@@ -51,6 +51,7 @@ pub(crate) mod adapters;
51
51
  pub mod approvals;
52
52
  pub mod classify;
53
53
  pub mod faults;
54
+ pub mod model_catalog;
54
55
  pub mod session;
55
56
  pub mod session_scan;
56
57
  pub mod startup_prompt;