@team-agent/installer 0.5.3 → 0.5.4

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.
@@ -101,11 +101,8 @@ fn layer2_backing_missing_refusal_carries_checked_paths_and_recovery_hint() {
101
101
  use std::sync::atomic::{AtomicU32, Ordering};
102
102
  static N: AtomicU32 = AtomicU32::new(0);
103
103
  let n = N.fetch_add(1, Ordering::Relaxed);
104
- let ws = std::env::temp_dir().join(format!(
105
- "ta_rs_l2_backingmiss_{}_{}",
106
- std::process::id(),
107
- n
108
- ));
104
+ let ws =
105
+ std::env::temp_dir().join(format!("ta_rs_l2_backingmiss_{}_{}", std::process::id(), n));
109
106
  std::fs::create_dir_all(&ws).unwrap();
110
107
 
111
108
  let missing_rollout = ws.join(".missing-rollout.jsonl");
@@ -168,6 +165,148 @@ fn layer2_backing_missing_refusal_carries_checked_paths_and_recovery_hint() {
168
165
  }
169
166
  }
170
167
 
168
+ fn write_codex_identity_rollout(
169
+ workspace: &std::path::Path,
170
+ file_name: &str,
171
+ session_id: &str,
172
+ embedded_agent_id: &str,
173
+ ) -> std::path::PathBuf {
174
+ let path = workspace.join(file_name);
175
+ let text = format!(
176
+ "{{\"session_meta\":{{\"payload\":{{\"id\":\"{session_id}\",\"cwd\":\"{}\"}}}}}}\n\
177
+ {{\"type\":\"turn_context\",\"payload\":{{}}}}\n\
178
+ {{\"type\":\"response_item\",\"payload\":{{\"content\":[{{\"type\":\"input_text\",\"text\":\"You are Team Agent worker `{embedded_agent_id}` with role `fixture`.\"}}]}}}}\n",
179
+ workspace.to_string_lossy()
180
+ );
181
+ std::fs::write(&path, text).unwrap();
182
+ path
183
+ }
184
+
185
+ #[test]
186
+ fn restart_refuses_codex_session_identity_mismatch_without_allow_fresh() {
187
+ let ws = temp_ws();
188
+ let rollout = write_codex_identity_rollout(
189
+ &ws,
190
+ "rollout-frontend-poison.jsonl",
191
+ "019f3327-c35a-7023-b3cd-1bea93a7a157",
192
+ "ios-dev",
193
+ );
194
+ let state = json!({
195
+ "agents": {
196
+ "frontend": {
197
+ "provider": "codex",
198
+ "status": "running",
199
+ "session_id": "019f3327-c35a-7023-b3cd-1bea93a7a157",
200
+ "rollout_path": rollout.to_string_lossy(),
201
+ "captured_at": "2026-07-05T17:04:04Z",
202
+ "captured_via": "fs_watch",
203
+ "spawn_cwd": ws.to_string_lossy(),
204
+ "first_send_at": "2026-07-05T17:10:00Z"
205
+ }
206
+ }
207
+ });
208
+
209
+ let plan = crate::lifecycle::restart::classify_restart_plan_with_resume_validation(
210
+ Some(&ws),
211
+ &state,
212
+ false,
213
+ )
214
+ .unwrap();
215
+
216
+ assert_eq!(plan.decisions.len(), 1);
217
+ assert_eq!(plan.decisions[0].decision, ResumeDecision::Refuse);
218
+ assert_eq!(plan.unresumable.len(), 1);
219
+ let entry = &plan.unresumable[0];
220
+ assert_eq!(entry.agent_id.as_str(), "frontend");
221
+ assert_eq!(entry.reason, "session_identity_mismatch");
222
+ assert_eq!(
223
+ entry
224
+ .refusal_reason
225
+ .as_ref()
226
+ .map(crate::provider::session::ResumeRefusalReason::wire),
227
+ Some("session_identity_mismatch")
228
+ );
229
+ }
230
+
231
+ #[test]
232
+ fn restart_identity_probe_reports_real_frontend_ios_dev_mismatch_shape() {
233
+ let ws = temp_ws();
234
+ let rollout = write_codex_identity_rollout(
235
+ &ws,
236
+ "rollout-frontend-poison.jsonl",
237
+ "019f3327-c35a-7023-b3cd-1bea93a7a157",
238
+ "ios-dev",
239
+ );
240
+ let rollout_path = crate::provider::RolloutPath::new(rollout.clone());
241
+ let probe = crate::lifecycle::restart::session_identity_probe_for_agent(
242
+ &aid("frontend"),
243
+ crate::provider::Provider::Codex,
244
+ Some(&rollout_path),
245
+ );
246
+ assert_eq!(probe.identity_ok, Some(false));
247
+ assert_eq!(probe.embedded_agent_id.as_deref(), Some("ios-dev"));
248
+ assert_eq!(probe.rollout_path.as_deref(), Some(rollout.as_path()));
249
+ }
250
+
251
+ #[test]
252
+ fn restart_allow_fresh_only_fresh_starts_mismatched_codex_worker() {
253
+ let ws = temp_ws();
254
+ let frontend_rollout = write_codex_identity_rollout(
255
+ &ws,
256
+ "rollout-frontend-poison.jsonl",
257
+ "019f3327-c35a-7023-b3cd-1bea93a7a157",
258
+ "ios-dev",
259
+ );
260
+ let backend_rollout = write_codex_identity_rollout(
261
+ &ws,
262
+ "rollout-backend-valid.jsonl",
263
+ "019f3327-backend-valid",
264
+ "backend",
265
+ );
266
+ let state = json!({
267
+ "agents": {
268
+ "frontend": {
269
+ "provider": "codex",
270
+ "status": "running",
271
+ "session_id": "019f3327-c35a-7023-b3cd-1bea93a7a157",
272
+ "rollout_path": frontend_rollout.to_string_lossy(),
273
+ "captured_at": "2026-07-05T17:04:04Z",
274
+ "captured_via": "fs_watch",
275
+ "spawn_cwd": ws.to_string_lossy()
276
+ },
277
+ "backend": {
278
+ "provider": "codex",
279
+ "status": "running",
280
+ "session_id": "019f3327-backend-valid",
281
+ "rollout_path": backend_rollout.to_string_lossy(),
282
+ "captured_at": "2026-07-05T17:04:05Z",
283
+ "captured_via": "fs_watch",
284
+ "spawn_cwd": ws.to_string_lossy()
285
+ }
286
+ }
287
+ });
288
+
289
+ let plan = crate::lifecycle::restart::classify_restart_plan_with_resume_validation(
290
+ Some(&ws),
291
+ &state,
292
+ true,
293
+ )
294
+ .unwrap();
295
+
296
+ assert!(
297
+ plan.unresumable.is_empty(),
298
+ "allow_fresh should convert the poisoned worker to fresh without refusing; got {:?}",
299
+ plan.unresumable
300
+ );
301
+ let decisions = plan
302
+ .decisions
303
+ .iter()
304
+ .map(|decision| (decision.agent_id.as_str(), decision.decision))
305
+ .collect::<std::collections::BTreeMap<_, _>>();
306
+ assert_eq!(decisions.get("frontend"), Some(&ResumeDecision::FreshStart));
307
+ assert_eq!(decisions.get("backend"), Some(&ResumeDecision::Resume));
308
+ }
309
+
171
310
  #[test]
172
311
  fn unit0_restart_corrupt_first_send_at_blocks_before_resume_classification() {
173
312
  // The corrupt-first_send_at branch is the hard-refuse gate that fires
@@ -440,6 +440,7 @@ pub struct StartedAgent {
440
440
  pub agent_id: AgentId,
441
441
  pub start_mode: StartMode,
442
442
  pub target: String,
443
+ pub spawned_at: String,
443
444
  pub session_id: Option<SessionId>,
444
445
  pub rollout_path: Option<RolloutPath>,
445
446
  pub pending_session_id: Option<SessionId>,
@@ -5,12 +5,12 @@ use std::process::Command;
5
5
 
6
6
  use super::helpers::patterns;
7
7
  use super::types::{
8
- AuthHintStatus, CapturedSession, CommandPlan, McpConfig, ProviderCaps,
9
- ProviderCommandContext, ProviderError, SessionId, StatusPatterns,
8
+ AuthHintStatus, CapturedSession, CommandPlan, McpConfig, ProviderCaps, ProviderCommandContext,
9
+ ProviderError, SessionId, StatusPatterns,
10
10
  };
11
11
  use super::{AuthMode, Provider};
12
12
 
13
- pub use crate::provider::session_scan::{CapturedSessionCandidate, CaptureSessionContext};
13
+ pub use crate::provider::session_scan::{CaptureSessionContext, CapturedSessionCandidate};
14
14
 
15
15
  // ===========================================================================
16
16
  // TRAIT: ProviderAdapter (method SIGNATURES only — 无 body)
@@ -70,7 +70,7 @@ pub trait ProviderAdapter {
70
70
  ctx.model,
71
71
  ctx.tools,
72
72
  )
73
- .map(CommandPlan::argv_only)
73
+ .map(CommandPlan::argv_only)
74
74
  }
75
75
 
76
76
  /// 启动后从 provider session 日志捕获 session_id + rollout_path
@@ -95,6 +95,7 @@ pub trait ProviderAdapter {
95
95
  .into_iter()
96
96
  .map(|captured| CapturedSessionCandidate {
97
97
  captured,
98
+ embedded_agent_id: None,
98
99
  positive_agent_id_match: false,
99
100
  agent_path_match: false,
100
101
  })
@@ -183,7 +184,7 @@ pub trait ProviderAdapter {
183
184
  ctx.model,
184
185
  ctx.tools,
185
186
  )
186
- .map(CommandPlan::argv_only)
187
+ .map(CommandPlan::argv_only)
187
188
  }
188
189
 
189
190
  /// 计算本 provider 该用的 MCP server 配置(`adapter.py` mcp_config;claude
@@ -226,19 +227,17 @@ pub trait ProviderAdapter {
226
227
  sleep_s: f64,
227
228
  ) -> super::startup_prompt::StartupPromptOutcome {
228
229
  std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| match self.provider() {
229
- Provider::Codex => {
230
- super::startup_prompt::codex_handle_startup_prompts(transport, target, checks, sleep_s)
231
- }
230
+ Provider::Codex => super::startup_prompt::codex_handle_startup_prompts(
231
+ transport, target, checks, sleep_s,
232
+ ),
232
233
  Provider::Claude | Provider::ClaudeCode => {
233
234
  super::startup_prompt::claude_handle_startup_prompts(
234
235
  transport, target, checks, sleep_s,
235
236
  )
236
237
  }
237
- Provider::Copilot => {
238
- super::startup_prompt::copilot_handle_startup_prompts(
239
- transport, target, checks, sleep_s,
240
- )
241
- }
238
+ Provider::Copilot => super::startup_prompt::copilot_handle_startup_prompts(
239
+ transport, target, checks, sleep_s,
240
+ ),
242
241
  _ => super::startup_prompt::StartupPromptOutcome::default(),
243
242
  }))
244
243
  .unwrap_or_default()
@@ -342,7 +341,9 @@ impl ProviderAdapter for BasicProviderAdapter {
342
341
  let output = Command::new(command_name(self.provider))
343
342
  .arg("--version")
344
343
  .output()
345
- .map_err(|e| ProviderError::Io(format!("{} --version: {e}", command_name(self.provider))))?;
344
+ .map_err(|e| {
345
+ ProviderError::Io(format!("{} --version: {e}", command_name(self.provider)))
346
+ })?;
346
347
  if !output.status.success() {
347
348
  return Ok("unknown".to_string());
348
349
  }
@@ -390,10 +391,24 @@ impl ProviderAdapter for BasicProviderAdapter {
390
391
  tools: &[&str],
391
392
  ) -> Result<Vec<String>, ProviderError> {
392
393
  match self.provider {
393
- Provider::Claude | Provider::ClaudeCode => {
394
- Ok(claude_launch_command(self, auth_mode, mcp_config, system_prompt, model, tools)?)
395
- }
396
- Provider::Codex => Ok(codex_base_command(None, auth_mode, mcp_config, system_prompt, model, tools, None, None)),
394
+ Provider::Claude | Provider::ClaudeCode => Ok(claude_launch_command(
395
+ self,
396
+ auth_mode,
397
+ mcp_config,
398
+ system_prompt,
399
+ model,
400
+ tools,
401
+ )?),
402
+ Provider::Codex => Ok(codex_base_command(
403
+ None,
404
+ auth_mode,
405
+ mcp_config,
406
+ system_prompt,
407
+ model,
408
+ tools,
409
+ None,
410
+ None,
411
+ )),
397
412
  // §C1 worker argv 形态 + C-1/C-5/C-6 cr verdict:
398
413
  // copilot --no-color --no-auto-update [<dangerous|granular>] [--model]
399
414
  // --additional-mcp-config <inline json> --session-id <expected_uuid> -C <cwd>
@@ -401,7 +416,11 @@ impl ProviderAdapter for BasicProviderAdapter {
401
416
  // AGENTS.md(launch 路径写文件,见 lifecycle/launch.rs)注入,**不入 argv**
402
417
  // (B2 灵魂件降级,C-1-2 禁 silent 写全局)。
403
418
  Provider::Copilot => Ok(copilot_base_command(
404
- auth_mode, mcp_config, system_prompt, model, tools,
419
+ auth_mode,
420
+ mcp_config,
421
+ system_prompt,
422
+ model,
423
+ tools,
405
424
  )),
406
425
  Provider::GeminiCli => {
407
426
  let mut argv = vec!["gemini".to_string()];
@@ -439,7 +458,9 @@ impl ProviderAdapter for BasicProviderAdapter {
439
458
  // `--resume <existing_id>` on a session id that already has
440
459
  // a real transcript.
441
460
  let expected = next_session_token();
442
- let managed = ctx.profile_launch.is_some_and(|profile| profile.managed_mcp_config);
461
+ let managed = ctx
462
+ .profile_launch
463
+ .is_some_and(|profile| profile.managed_mcp_config);
443
464
  let projects_root = ctx
444
465
  .profile_launch
445
466
  .and_then(|profile| profile.claude_projects_root.clone());
@@ -614,7 +635,9 @@ impl ProviderAdapter for BasicProviderAdapter {
614
635
  )));
615
636
  }
616
637
  let Some(session_id) = session_id else {
617
- return Err(ProviderError::ResumeUnavailable("resume requires session_id".to_string()));
638
+ return Err(ProviderError::ResumeUnavailable(
639
+ "resume requires session_id".to_string(),
640
+ ));
618
641
  };
619
642
  match self.provider {
620
643
  Provider::Codex => {
@@ -632,8 +655,16 @@ impl ProviderAdapter for BasicProviderAdapter {
632
655
  Ok(argv)
633
656
  }
634
657
  Provider::Claude | Provider::ClaudeCode => {
635
- let mut argv =
636
- claude_base_command(self, auth_mode, mcp_config, system_prompt, model, tools, false, None)?;
658
+ let mut argv = claude_base_command(
659
+ self,
660
+ auth_mode,
661
+ mcp_config,
662
+ system_prompt,
663
+ model,
664
+ tools,
665
+ false,
666
+ None,
667
+ )?;
637
668
  argv.push("--resume".to_string());
638
669
  argv.push(session_id.as_str().to_string());
639
670
  Ok(argv)
@@ -641,9 +672,8 @@ impl ProviderAdapter for BasicProviderAdapter {
641
672
  // §C1 cr verdict:resume 同 base + `--resume <sid>`(去 --session-id,
642
673
  // copilot --resume 接受 id|name)。
643
674
  Provider::Copilot => {
644
- let mut argv = copilot_base_command_resume(
645
- auth_mode, mcp_config, system_prompt, model, tools,
646
- );
675
+ let mut argv =
676
+ copilot_base_command_resume(auth_mode, mcp_config, system_prompt, model, tools);
647
677
  argv.push("--resume".to_string());
648
678
  argv.push(session_id.as_str().to_string());
649
679
  Ok(argv)
@@ -663,9 +693,13 @@ impl ProviderAdapter for BasicProviderAdapter {
663
693
  match self.provider {
664
694
  Provider::Claude | Provider::ClaudeCode => {
665
695
  let Some(session_id) = session_id else {
666
- return Err(ProviderError::ResumeUnavailable("resume requires session_id".to_string()));
696
+ return Err(ProviderError::ResumeUnavailable(
697
+ "resume requires session_id".to_string(),
698
+ ));
667
699
  };
668
- let managed = ctx.profile_launch.is_some_and(|profile| profile.managed_mcp_config);
700
+ let managed = ctx
701
+ .profile_launch
702
+ .is_some_and(|profile| profile.managed_mcp_config);
669
703
  let model = claude_context_model(ctx);
670
704
  let mut argv = claude_base_command(
671
705
  self,
@@ -758,7 +792,9 @@ impl ProviderAdapter for BasicProviderAdapter {
758
792
  )));
759
793
  }
760
794
  let Some(session_id) = session_id else {
761
- return Err(ProviderError::ResumeUnavailable("fork requires session_id".to_string()));
795
+ return Err(ProviderError::ResumeUnavailable(
796
+ "fork requires session_id".to_string(),
797
+ ));
762
798
  };
763
799
  match self.provider {
764
800
  Provider::Codex => {
@@ -776,8 +812,16 @@ impl ProviderAdapter for BasicProviderAdapter {
776
812
  Ok(argv)
777
813
  }
778
814
  Provider::Claude | Provider::ClaudeCode => {
779
- let mut argv =
780
- claude_base_command(self, auth_mode, mcp_config, system_prompt, model, tools, false, None)?;
815
+ let mut argv = claude_base_command(
816
+ self,
817
+ auth_mode,
818
+ mcp_config,
819
+ system_prompt,
820
+ model,
821
+ tools,
822
+ false,
823
+ None,
824
+ )?;
781
825
  argv.push("--session-id".to_string());
782
826
  argv.push(next_session_token());
783
827
  argv.push("--resume".to_string());
@@ -792,10 +836,12 @@ impl ProviderAdapter for BasicProviderAdapter {
792
836
  Provider::Copilot => Err(ProviderError::CapabilityUnsupported(
793
837
  "copilot CLI 无 fork 旗标,session-store 不支持 branched continuation".to_string(),
794
838
  )),
795
- Provider::GeminiCli | Provider::Fake => Err(ProviderError::CapabilityUnsupported(format!(
796
- "{} does not support native session fork",
797
- provider_wire(self.provider)
798
- ))),
839
+ Provider::GeminiCli | Provider::Fake => {
840
+ Err(ProviderError::CapabilityUnsupported(format!(
841
+ "{} does not support native session fork",
842
+ provider_wire(self.provider)
843
+ )))
844
+ }
799
845
  }
800
846
  }
801
847
 
@@ -813,10 +859,14 @@ impl ProviderAdapter for BasicProviderAdapter {
813
859
  )));
814
860
  }
815
861
  let Some(session_id) = session_id else {
816
- return Err(ProviderError::ResumeUnavailable("fork requires session_id".to_string()));
862
+ return Err(ProviderError::ResumeUnavailable(
863
+ "fork requires session_id".to_string(),
864
+ ));
817
865
  };
818
866
  let expected = next_session_token();
819
- let managed = ctx.profile_launch.is_some_and(|profile| profile.managed_mcp_config);
867
+ let managed = ctx
868
+ .profile_launch
869
+ .is_some_and(|profile| profile.managed_mcp_config);
820
870
  let projects_root = ctx
821
871
  .profile_launch
822
872
  .and_then(|profile| profile.claude_projects_root.clone());
@@ -910,10 +960,22 @@ impl ProviderAdapter for BasicProviderAdapter {
910
960
 
911
961
  fn status_patterns(&self) -> Result<StatusPatterns, ProviderError> {
912
962
  match self.provider {
913
- Provider::Claude | Provider::ClaudeCode => patterns(r"[>❯]\s", r"[✶✢✽✻✳·].*…", r"Error|Traceback"),
914
- Provider::Codex => patterns(r"(›|❯|codex>)", r"•.*esc to interrupt", r"Error|Traceback|panic"),
915
- Provider::Copilot => patterns(r"(?m)^\s*❯\s*$| / commands · \? help", r"working|processing", r"Error|panic"),
916
- Provider::GeminiCli | Provider::Fake => patterns(r">", r"working|processing", r"Error|Traceback"),
963
+ Provider::Claude | Provider::ClaudeCode => {
964
+ patterns(r"[>❯]\s", r"[✶✢✽✻✳·].*…", r"Error|Traceback")
965
+ }
966
+ Provider::Codex => patterns(
967
+ r"(›|❯|codex>)",
968
+ r"•.*esc to interrupt",
969
+ r"Error|Traceback|panic",
970
+ ),
971
+ Provider::Copilot => patterns(
972
+ r"(?m)^\s*❯\s*$| / commands · \? help",
973
+ r"working|processing",
974
+ r"Error|panic",
975
+ ),
976
+ Provider::GeminiCli | Provider::Fake => {
977
+ patterns(r">", r"working|processing", r"Error|Traceback")
978
+ }
917
979
  }
918
980
  }
919
981