@team-agent/installer 0.3.2 → 0.3.3

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 (78) hide show
  1. package/Cargo.lock +34 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -1
  4. package/crates/team-agent/src/cli/adapters.rs +196 -19
  5. package/crates/team-agent/src/cli/diagnose.rs +144 -10
  6. package/crates/team-agent/src/cli/emit.rs +286 -52
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +799 -316
  9. package/crates/team-agent/src/cli/status_port.rs +25 -2
  10. package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
  11. package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
  12. package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
  13. package/crates/team-agent/src/cli/tests/run_delegation.rs +57 -3
  14. package/crates/team-agent/src/cli/types.rs +17 -0
  15. package/crates/team-agent/src/compiler.rs +15 -5
  16. package/crates/team-agent/src/coordinator/health.rs +89 -20
  17. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  18. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  19. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  20. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  21. package/crates/team-agent/src/coordinator/types.rs +15 -3
  22. package/crates/team-agent/src/db/schema.rs +37 -2
  23. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  24. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  25. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  26. package/crates/team-agent/src/fake_worker.rs +146 -3
  27. package/crates/team-agent/src/leader/start.rs +121 -23
  28. package/crates/team-agent/src/leader/types.rs +44 -1
  29. package/crates/team-agent/src/lib.rs +3 -0
  30. package/crates/team-agent/src/lifecycle/display.rs +645 -47
  31. package/crates/team-agent/src/lifecycle/launch.rs +818 -116
  32. package/crates/team-agent/src/lifecycle/mod.rs +2 -0
  33. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  34. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  35. package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
  36. package/crates/team-agent/src/lifecycle/restart/common.rs +177 -83
  37. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +443 -9
  38. package/crates/team-agent/src/lifecycle/restart/remove.rs +22 -6
  39. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  40. package/crates/team-agent/src/lifecycle/restart.rs +4 -1
  41. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
  43. package/crates/team-agent/src/lifecycle/types.rs +19 -0
  44. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  45. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  46. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  47. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  48. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  49. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  50. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  51. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  52. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  53. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  54. package/crates/team-agent/src/message_store.rs +21 -4
  55. package/crates/team-agent/src/messaging/delivery.rs +87 -37
  56. package/crates/team-agent/src/messaging/mod.rs +9 -6
  57. package/crates/team-agent/src/messaging/results.rs +153 -16
  58. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  59. package/crates/team-agent/src/messaging/send.rs +35 -3
  60. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  61. package/crates/team-agent/src/messaging/types.rs +11 -3
  62. package/crates/team-agent/src/os_probe.rs +119 -0
  63. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  64. package/crates/team-agent/src/packaging/tests.rs +23 -0
  65. package/crates/team-agent/src/provider/adapter.rs +483 -67
  66. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  67. package/crates/team-agent/src/provider/classify.rs +51 -4
  68. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  69. package/crates/team-agent/src/provider/types.rs +47 -0
  70. package/crates/team-agent/src/session_capture.rs +616 -0
  71. package/crates/team-agent/src/state/persist.rs +57 -0
  72. package/crates/team-agent/src/state/projection.rs +32 -23
  73. package/crates/team-agent/src/state/selector.rs +5 -2
  74. package/crates/team-agent/src/tmux_backend.rs +97 -60
  75. package/crates/team-agent/src/transport/test_support.rs +9 -0
  76. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  77. package/crates/team-agent/src/transport.rs +13 -2
  78. package/package.json +4 -4
@@ -5,8 +5,9 @@ use std::process::Command;
5
5
 
6
6
  use super::helpers::{find_session_id, parse_jsonl_records, patterns};
7
7
  use super::types::{
8
- AuthHintStatus, CaptureVia, CapturedSession, Confidence, McpConfig, ProviderCaps,
9
- ProviderError, RolloutPath, SessionId, StatusPatterns,
8
+ AuthHintStatus, CaptureVia, CapturedSession, CommandPlan, Confidence, McpConfig,
9
+ ProviderCaps, ProviderCommandContext, ProviderError, RolloutPath, SessionId,
10
+ StatusPatterns,
10
11
  };
11
12
  use super::{AuthMode, Provider};
12
13
 
@@ -57,6 +58,20 @@ pub trait ProviderAdapter {
57
58
  tools: &[&str],
58
59
  ) -> Result<Vec<String>, ProviderError>;
59
60
 
61
+ fn build_command_plan(
62
+ &self,
63
+ ctx: ProviderCommandContext<'_>,
64
+ ) -> Result<CommandPlan, ProviderError> {
65
+ self.build_command_with_tools(
66
+ ctx.auth_mode,
67
+ ctx.mcp_config,
68
+ ctx.system_prompt,
69
+ ctx.model,
70
+ ctx.tools,
71
+ )
72
+ .map(CommandPlan::argv_only)
73
+ }
74
+
60
75
  /// 启动后从 provider session 日志捕获 session_id + rollout_path
61
76
  /// (`claude.py:73`/`codex.py:62`)。fs watch / mtime fallback / repair。
62
77
  fn capture_session_id(
@@ -66,6 +81,25 @@ pub trait ProviderAdapter {
66
81
  timeout_s: u64,
67
82
  ) -> Result<Option<CapturedSession>, ProviderError>;
68
83
 
84
+ /// Internal capture surface for same-team multi-agent attribution: enumerate every
85
+ /// cwd-matching provider transcript candidate, then let the runtime allocate them
86
+ /// once per tick/restart pass using per-agent context.
87
+ fn capture_session_candidates(
88
+ &self,
89
+ context: &CaptureSessionContext,
90
+ timeout_s: u64,
91
+ ) -> Result<Vec<CapturedSessionCandidate>, ProviderError> {
92
+ Ok(self
93
+ .capture_session_id(&context.agent_id, &context.spawn_cwd, timeout_s)?
94
+ .into_iter()
95
+ .map(|captured| CapturedSessionCandidate {
96
+ captured,
97
+ positive_agent_id_match: false,
98
+ agent_path_match: false,
99
+ })
100
+ .collect())
101
+ }
102
+
69
103
  /// restart/reset 路径:从已存 transcript/rollout 回收 session_id
70
104
  /// (`claude.py:115`)。`None` 合法(找不到)。
71
105
  fn recover_session_id(
@@ -100,6 +134,22 @@ pub trait ProviderAdapter {
100
134
  tools: &[&str],
101
135
  ) -> Result<Vec<String>, ProviderError>;
102
136
 
137
+ fn build_resume_command_plan(
138
+ &self,
139
+ session_id: Option<&SessionId>,
140
+ ctx: ProviderCommandContext<'_>,
141
+ ) -> Result<CommandPlan, ProviderError> {
142
+ self.build_resume_command_with_context(
143
+ session_id,
144
+ ctx.auth_mode,
145
+ ctx.mcp_config,
146
+ ctx.system_prompt,
147
+ ctx.model,
148
+ ctx.tools,
149
+ )
150
+ .map(CommandPlan::argv_only)
151
+ }
152
+
103
153
  /// 构造 fork 命令(`providers.py:99`)。fork 需 caps.fork ∧ auth_mode!=compatible_api;
104
154
  /// 不支持 → `Err`。
105
155
  fn fork(
@@ -119,6 +169,22 @@ pub trait ProviderAdapter {
119
169
  tools: &[&str],
120
170
  ) -> Result<Vec<String>, ProviderError>;
121
171
 
172
+ fn fork_plan(
173
+ &self,
174
+ session_id: Option<&SessionId>,
175
+ ctx: ProviderCommandContext<'_>,
176
+ ) -> Result<CommandPlan, ProviderError> {
177
+ self.fork_with_context(
178
+ session_id,
179
+ ctx.auth_mode,
180
+ ctx.mcp_config,
181
+ ctx.system_prompt,
182
+ ctx.model,
183
+ ctx.tools,
184
+ )
185
+ .map(CommandPlan::argv_only)
186
+ }
187
+
122
188
  /// 计算本 provider 该用的 MCP server 配置(`adapter.py` mcp_config;claude
123
189
  /// compatible_api 走 `ensure_compatible_claude_mcp_config`)。
124
190
  fn mcp_config(&self, auth_mode: AuthMode) -> Result<McpConfig, ProviderError>;
@@ -134,8 +200,9 @@ pub trait ProviderAdapter {
134
200
  /// 校验 model 名对本 provider 合法(`codex debug models` 等;doctor)。
135
201
  fn validate_model(&self, model: &str) -> Result<bool, ProviderError>;
136
202
 
137
- /// Provider-specific startup prompt handling. Non-Codex adapters return an empty list; Codex
138
- /// delegates to the provider-layer recognizer.
203
+ /// Provider-specific startup prompt handling. Codex and Claude delegate to
204
+ /// provider-layer recognizers; providers without startup prompts return an
205
+ /// empty list.
139
206
  fn handle_startup_prompts(
140
207
  &self,
141
208
  transport: &dyn crate::transport::Transport,
@@ -147,12 +214,35 @@ pub trait ProviderAdapter {
147
214
  Provider::Codex => {
148
215
  super::startup_prompt::codex_handle_startup_prompts(transport, target, checks, sleep_s)
149
216
  }
217
+ Provider::Claude | Provider::ClaudeCode => {
218
+ super::startup_prompt::claude_handle_startup_prompts(
219
+ transport, target, checks, sleep_s,
220
+ )
221
+ }
150
222
  _ => Vec::new(),
151
223
  }))
152
224
  .unwrap_or_default()
153
225
  }
154
226
  }
155
227
 
228
+ #[derive(Debug, Clone, PartialEq, Eq)]
229
+ pub struct CaptureSessionContext {
230
+ pub agent_id: String,
231
+ pub spawn_cwd: PathBuf,
232
+ pub pane_id: Option<String>,
233
+ pub pane_pid: Option<u32>,
234
+ pub spawned_at: Option<String>,
235
+ pub expected_session_id: Option<SessionId>,
236
+ pub provider_projects_root: Option<PathBuf>,
237
+ }
238
+
239
+ #[derive(Debug, Clone, PartialEq, Eq)]
240
+ pub struct CapturedSessionCandidate {
241
+ pub captured: CapturedSession,
242
+ pub positive_agent_id_match: bool,
243
+ pub agent_path_match: bool,
244
+ }
245
+
156
246
  // ===========================================================================
157
247
  // FACADE 自由函数 (doc §71 providers.get_adapter — body unimplemented)
158
248
  // ===========================================================================
@@ -230,9 +320,12 @@ impl ProviderAdapter for BasicProviderAdapter {
230
320
  }
231
321
 
232
322
  fn auth_hint(&self, auth_mode: AuthMode) -> AuthHintStatus {
233
- match auth_mode {
234
- AuthMode::Subscription => AuthHintStatus::Present,
235
- AuthMode::OfficialApi | AuthMode::CompatibleApi => AuthHintStatus::MissingOrUnknown,
323
+ match self.provider {
324
+ Provider::Claude | Provider::ClaudeCode => claude_auth_hint(auth_mode),
325
+ _ => match auth_mode {
326
+ AuthMode::Subscription => AuthHintStatus::Present,
327
+ AuthMode::OfficialApi | AuthMode::CompatibleApi => AuthHintStatus::MissingOrUnknown,
328
+ },
236
329
  }
237
330
  }
238
331
 
@@ -256,7 +349,7 @@ impl ProviderAdapter for BasicProviderAdapter {
256
349
  ) -> Result<Vec<String>, ProviderError> {
257
350
  match self.provider {
258
351
  Provider::Claude | Provider::ClaudeCode => {
259
- Ok(claude_launch_command(self, auth_mode, mcp_config, system_prompt, model)?)
352
+ Ok(claude_launch_command(self, auth_mode, mcp_config, system_prompt, model, tools)?)
260
353
  }
261
354
  Provider::Codex => Ok(codex_base_command(None, auth_mode, mcp_config, system_prompt, model, tools)),
262
355
  Provider::GeminiCli => {
@@ -271,51 +364,83 @@ impl ProviderAdapter for BasicProviderAdapter {
271
364
  }
272
365
  }
273
366
 
367
+ fn build_command_plan(
368
+ &self,
369
+ ctx: ProviderCommandContext<'_>,
370
+ ) -> Result<CommandPlan, ProviderError> {
371
+ match self.provider {
372
+ Provider::Claude | Provider::ClaudeCode => {
373
+ let expected = next_session_token();
374
+ let managed = ctx.profile_launch.is_some_and(|profile| profile.managed_mcp_config);
375
+ let projects_root = ctx
376
+ .profile_launch
377
+ .and_then(|profile| profile.claude_projects_root.clone());
378
+ let model = claude_context_model(ctx);
379
+ let mut argv = claude_base_command(
380
+ self,
381
+ ctx.auth_mode,
382
+ ctx.mcp_config,
383
+ ctx.system_prompt,
384
+ model,
385
+ ctx.tools,
386
+ managed,
387
+ )?;
388
+ argv.push("--session-id".to_string());
389
+ argv.push(expected.clone());
390
+ Ok(CommandPlan {
391
+ argv,
392
+ expected_session_id: Some(SessionId::new(expected)),
393
+ provider_projects_root: projects_root,
394
+ managed_mcp_config: managed,
395
+ })
396
+ }
397
+ _ => self
398
+ .build_command_with_tools(
399
+ ctx.auth_mode,
400
+ ctx.mcp_config,
401
+ ctx.system_prompt,
402
+ ctx.model,
403
+ ctx.tools,
404
+ )
405
+ .map(CommandPlan::argv_only),
406
+ }
407
+ }
408
+
274
409
  fn capture_session_id(
275
410
  &self,
276
411
  agent_id: &str,
277
412
  spawn_cwd: &Path,
278
- _timeout_s: u64,
413
+ timeout_s: u64,
279
414
  ) -> Result<Option<CapturedSession>, ProviderError> {
280
- let candidates = candidate_session_files(self.provider, spawn_cwd, agent_id)?;
281
- for candidate in candidates {
282
- let path = candidate.path;
283
- let Ok(text) = std::fs::read_to_string(&path) else {
284
- continue;
285
- };
286
- let records = parse_session_records(&text);
287
- if records.is_empty() {
288
- continue;
289
- }
290
- if candidate.requires_cwd_match && !provider_home_records_match_spawn_cwd(&records, spawn_cwd) {
291
- continue;
292
- }
293
- let session_id = records.iter().find_map(find_session_id);
294
- if matches!(self.provider, Provider::Claude | Provider::ClaudeCode)
295
- && session_id.is_some()
296
- && !records.iter().any(has_cwd_field)
297
- {
298
- continue;
415
+ let context = CaptureSessionContext {
416
+ agent_id: agent_id.to_string(),
417
+ spawn_cwd: spawn_cwd.to_path_buf(),
418
+ pane_id: None,
419
+ pane_pid: None,
420
+ spawned_at: None,
421
+ expected_session_id: None,
422
+ provider_projects_root: None,
423
+ };
424
+ Ok(self
425
+ .capture_session_candidates(&context, timeout_s)?
426
+ .into_iter()
427
+ .next()
428
+ .map(|candidate| candidate.captured))
429
+ }
430
+
431
+ fn capture_session_candidates(
432
+ &self,
433
+ context: &CaptureSessionContext,
434
+ timeout_s: u64,
435
+ ) -> Result<Vec<CapturedSessionCandidate>, ProviderError> {
436
+ let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_s);
437
+ loop {
438
+ let out = scan_session_candidates_once(self.provider, context)?;
439
+ if !out.is_empty() || timeout_s == 0 || std::time::Instant::now() >= deadline {
440
+ return Ok(out);
299
441
  }
300
- let captured_via = if session_id.is_some() {
301
- CaptureVia::FsWatch
302
- } else {
303
- CaptureVia::FsMtimeFallback
304
- };
305
- let attribution_confidence = if session_id.is_some() {
306
- Confidence::High
307
- } else {
308
- Confidence::Low
309
- };
310
- return Ok(Some(CapturedSession {
311
- session_id: session_id.map(SessionId::new),
312
- rollout_path: Some(RolloutPath::new(path)),
313
- captured_via,
314
- attribution_confidence,
315
- spawn_cwd: spawn_cwd.to_path_buf(),
316
- }));
442
+ std::thread::sleep(std::time::Duration::from_millis(100));
317
443
  }
318
- Ok(None)
319
444
  }
320
445
 
321
446
  fn recover_session_id(
@@ -380,7 +505,8 @@ impl ProviderAdapter for BasicProviderAdapter {
380
505
  Ok(argv)
381
506
  }
382
507
  Provider::Claude | Provider::ClaudeCode => {
383
- let mut argv = claude_base_command(self, auth_mode, mcp_config, system_prompt, model)?;
508
+ let mut argv =
509
+ claude_base_command(self, auth_mode, mcp_config, system_prompt, model, tools, false)?;
384
510
  argv.push("--resume".to_string());
385
511
  argv.push(session_id.as_str().to_string());
386
512
  Ok(argv)
@@ -392,6 +518,49 @@ impl ProviderAdapter for BasicProviderAdapter {
392
518
  }
393
519
  }
394
520
 
521
+ fn build_resume_command_plan(
522
+ &self,
523
+ session_id: Option<&SessionId>,
524
+ ctx: ProviderCommandContext<'_>,
525
+ ) -> Result<CommandPlan, ProviderError> {
526
+ match self.provider {
527
+ Provider::Claude | Provider::ClaudeCode => {
528
+ let Some(session_id) = session_id else {
529
+ return Err(ProviderError::ResumeUnavailable("resume requires session_id".to_string()));
530
+ };
531
+ let managed = ctx.profile_launch.is_some_and(|profile| profile.managed_mcp_config);
532
+ let model = claude_context_model(ctx);
533
+ let mut argv = claude_base_command(
534
+ self,
535
+ ctx.auth_mode,
536
+ ctx.mcp_config,
537
+ ctx.system_prompt,
538
+ model,
539
+ ctx.tools,
540
+ managed,
541
+ )?;
542
+ argv.push("--resume".to_string());
543
+ argv.push(session_id.as_str().to_string());
544
+ let mut plan = CommandPlan::argv_only(argv);
545
+ plan.provider_projects_root = ctx
546
+ .profile_launch
547
+ .and_then(|profile| profile.claude_projects_root.clone());
548
+ plan.managed_mcp_config = managed;
549
+ Ok(plan)
550
+ }
551
+ _ => self
552
+ .build_resume_command_with_context(
553
+ session_id,
554
+ ctx.auth_mode,
555
+ ctx.mcp_config,
556
+ ctx.system_prompt,
557
+ ctx.model,
558
+ ctx.tools,
559
+ )
560
+ .map(CommandPlan::argv_only),
561
+ }
562
+ }
563
+
395
564
  fn fork(
396
565
  &self,
397
566
  session_id: Option<&SessionId>,
@@ -433,7 +602,8 @@ impl ProviderAdapter for BasicProviderAdapter {
433
602
  Ok(argv)
434
603
  }
435
604
  Provider::Claude | Provider::ClaudeCode => {
436
- let mut argv = claude_base_command(self, auth_mode, mcp_config, system_prompt, model)?;
605
+ let mut argv =
606
+ claude_base_command(self, auth_mode, mcp_config, system_prompt, model, tools, false)?;
437
607
  argv.push("--session-id".to_string());
438
608
  argv.push(next_session_token());
439
609
  argv.push("--resume".to_string());
@@ -448,6 +618,62 @@ impl ProviderAdapter for BasicProviderAdapter {
448
618
  }
449
619
  }
450
620
 
621
+ fn fork_plan(
622
+ &self,
623
+ session_id: Option<&SessionId>,
624
+ ctx: ProviderCommandContext<'_>,
625
+ ) -> Result<CommandPlan, ProviderError> {
626
+ match self.provider {
627
+ Provider::Claude | Provider::ClaudeCode => {
628
+ if !self.caps().fork || ctx.auth_mode == AuthMode::CompatibleApi {
629
+ return Err(ProviderError::CapabilityUnsupported(format!(
630
+ "{} does not support native session fork",
631
+ provider_wire(self.provider)
632
+ )));
633
+ }
634
+ let Some(session_id) = session_id else {
635
+ return Err(ProviderError::ResumeUnavailable("fork requires session_id".to_string()));
636
+ };
637
+ let expected = next_session_token();
638
+ let managed = ctx.profile_launch.is_some_and(|profile| profile.managed_mcp_config);
639
+ let projects_root = ctx
640
+ .profile_launch
641
+ .and_then(|profile| profile.claude_projects_root.clone());
642
+ let model = claude_context_model(ctx);
643
+ let mut argv = claude_base_command(
644
+ self,
645
+ ctx.auth_mode,
646
+ ctx.mcp_config,
647
+ ctx.system_prompt,
648
+ model,
649
+ ctx.tools,
650
+ managed,
651
+ )?;
652
+ argv.push("--session-id".to_string());
653
+ argv.push(expected.clone());
654
+ argv.push("--resume".to_string());
655
+ argv.push(session_id.as_str().to_string());
656
+ argv.push("--fork-session".to_string());
657
+ Ok(CommandPlan {
658
+ argv,
659
+ expected_session_id: Some(SessionId::new(expected)),
660
+ provider_projects_root: projects_root,
661
+ managed_mcp_config: managed,
662
+ })
663
+ }
664
+ _ => self
665
+ .fork_with_context(
666
+ session_id,
667
+ ctx.auth_mode,
668
+ ctx.mcp_config,
669
+ ctx.system_prompt,
670
+ ctx.model,
671
+ ctx.tools,
672
+ )
673
+ .map(CommandPlan::argv_only),
674
+ }
675
+ }
676
+
451
677
  fn mcp_config(&self, auth_mode: AuthMode) -> Result<McpConfig, ProviderError> {
452
678
  let server = mcp_server_config(auth_mode);
453
679
  Ok(McpConfig {
@@ -515,6 +741,101 @@ fn auth_mode_wire(auth_mode: AuthMode) -> &'static str {
515
741
  }
516
742
  }
517
743
 
744
+ fn claude_auth_hint(auth_mode: AuthMode) -> AuthHintStatus {
745
+ if auth_mode != AuthMode::Subscription {
746
+ return AuthHintStatus::MissingOrUnknown;
747
+ }
748
+ if !command_on_path("claude") {
749
+ return AuthHintStatus::Missing;
750
+ }
751
+ let output = match Command::new("claude").args(["auth", "status"]).output() {
752
+ Ok(output) => output,
753
+ Err(_) => return AuthHintStatus::MissingOrUnknown,
754
+ };
755
+ let text = if output.stdout.is_empty() {
756
+ String::from_utf8_lossy(&output.stderr).to_string()
757
+ } else {
758
+ String::from_utf8_lossy(&output.stdout).to_string()
759
+ };
760
+ let status = serde_json::from_str::<serde_json::Value>(text.trim()).unwrap_or_default();
761
+ if status.get("loggedIn").and_then(serde_json::Value::as_bool) == Some(true)
762
+ || output.status.success()
763
+ {
764
+ AuthHintStatus::Present
765
+ } else {
766
+ AuthHintStatus::Missing
767
+ }
768
+ }
769
+
770
+ fn claude_context_model(ctx: ProviderCommandContext<'_>) -> Option<&str> {
771
+ ctx.profile_launch
772
+ .and_then(|profile| profile.command_overrides.model.as_deref())
773
+ .or(ctx.model)
774
+ }
775
+
776
+ fn scan_session_candidates_once(
777
+ provider: Provider,
778
+ context: &CaptureSessionContext,
779
+ ) -> Result<Vec<CapturedSessionCandidate>, ProviderError> {
780
+ let candidates = candidate_session_files(provider, context)?;
781
+ let mut out = Vec::new();
782
+ for candidate in candidates {
783
+ let path = candidate.path;
784
+ let Ok(text) = std::fs::read_to_string(&path) else {
785
+ continue;
786
+ };
787
+ let records = parse_session_records(&text);
788
+ if records.is_empty() {
789
+ continue;
790
+ }
791
+ if candidate.requires_cwd_match
792
+ && !provider_home_records_match_spawn_cwd(&records, &context.spawn_cwd)
793
+ {
794
+ continue;
795
+ }
796
+ let session_id = records.iter().find_map(find_session_id);
797
+ if matches!(provider, Provider::Claude | Provider::ClaudeCode)
798
+ && session_id.is_some()
799
+ && !records.iter().any(has_cwd_field)
800
+ {
801
+ continue;
802
+ }
803
+ let captured_via = if session_id.is_some() {
804
+ CaptureVia::FsWatch
805
+ } else {
806
+ CaptureVia::FsMtimeFallback
807
+ };
808
+ let attribution_confidence = if session_id.is_some() {
809
+ Confidence::High
810
+ } else {
811
+ Confidence::Low
812
+ };
813
+ let positive_agent_id_match = candidate_text_has_team_agent_id(&text, context);
814
+ let agent_path_match = candidate_path_matches_agent_id(&path, context);
815
+ out.push(CapturedSessionCandidate {
816
+ captured: CapturedSession {
817
+ session_id: session_id.map(SessionId::new),
818
+ rollout_path: Some(RolloutPath::new(path)),
819
+ captured_via,
820
+ attribution_confidence,
821
+ spawn_cwd: context.spawn_cwd.clone(),
822
+ },
823
+ positive_agent_id_match,
824
+ agent_path_match,
825
+ });
826
+ }
827
+ if let Some(expected) = context.expected_session_id.as_ref() {
828
+ out.sort_by_key(|candidate| {
829
+ candidate
830
+ .captured
831
+ .session_id
832
+ .as_ref()
833
+ .is_none_or(|session| session.as_str() != expected.as_str())
834
+ });
835
+ }
836
+ Ok(out)
837
+ }
838
+
518
839
  fn command_on_path(name: &str) -> bool {
519
840
  let Some(path) = std::env::var_os("PATH") else {
520
841
  return false;
@@ -529,19 +850,21 @@ struct SessionCandidate {
529
850
 
530
851
  fn candidate_session_files(
531
852
  provider: Provider,
532
- spawn_cwd: &Path,
533
- agent_id: &str,
853
+ context: &CaptureSessionContext,
534
854
  ) -> Result<Vec<SessionCandidate>, ProviderError> {
535
855
  let mut out = Vec::new();
536
- collect_candidate_files(spawn_cwd, agent_id, 0, false, &mut out)?;
856
+ if let Some(root) = context.provider_projects_root.as_ref() {
857
+ collect_optional_candidate_files(root, &context.agent_id, &mut out)?;
858
+ }
859
+ collect_candidate_files(&context.spawn_cwd, &context.agent_id, 0, false, &mut out)?;
537
860
  if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) {
538
861
  match provider {
539
862
  Provider::Codex => {
540
- collect_optional_candidate_files(&home.join(".codex").join("sessions"), agent_id, &mut out)?;
863
+ collect_optional_candidate_files(&home.join(".codex").join("sessions"), &context.agent_id, &mut out)?;
541
864
  }
542
865
  Provider::Claude | Provider::ClaudeCode => {
543
- collect_optional_candidate_files(&home.join(".claude").join("sessions"), agent_id, &mut out)?;
544
- collect_optional_candidate_files(&home.join(".claude").join("projects"), agent_id, &mut out)?;
866
+ collect_optional_candidate_files(&home.join(".claude").join("sessions"), &context.agent_id, &mut out)?;
867
+ collect_optional_candidate_files(&home.join(".claude").join("projects"), &context.agent_id, &mut out)?;
545
868
  }
546
869
  Provider::GeminiCli | Provider::Fake => {}
547
870
  }
@@ -633,6 +956,37 @@ fn provider_home_records_match_spawn_cwd(records: &[serde_json::Value], spawn_cw
633
956
  .any(|cwd| paths_equivalent(Path::new(cwd), spawn_cwd))
634
957
  }
635
958
 
959
+ fn candidate_text_has_team_agent_id(text: &str, context: &CaptureSessionContext) -> bool {
960
+ let id = context.agent_id.as_str();
961
+ if id.is_empty() {
962
+ return false;
963
+ }
964
+ [
965
+ format!("\"TEAM_AGENT_ID\":\"{id}\""),
966
+ format!("\"TEAM_AGENT_ID\": \"{id}\""),
967
+ format!("TEAM_AGENT_ID={id}"),
968
+ format!("env.TEAM_AGENT_ID=\"{id}\""),
969
+ format!("env.TEAM_AGENT_ID=\\\"{id}\\\""),
970
+ format!("\"TEAM_AGENT_AGENT_ID\":\"{id}\""),
971
+ format!("\"TEAM_AGENT_AGENT_ID\": \"{id}\""),
972
+ format!("TEAM_AGENT_AGENT_ID={id}"),
973
+ ]
974
+ .iter()
975
+ .any(|needle| text.contains(needle))
976
+ }
977
+
978
+ fn candidate_path_matches_agent_id(path: &Path, context: &CaptureSessionContext) -> bool {
979
+ let id = context.agent_id.as_str();
980
+ if id.is_empty() {
981
+ return false;
982
+ }
983
+ let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
984
+ return false;
985
+ };
986
+ let dashed = id.replace('_', "-");
987
+ name.contains(id) || name.contains(&dashed)
988
+ }
989
+
636
990
  fn record_cwd(record: &serde_json::Value) -> Option<String> {
637
991
  record
638
992
  .get("cwd")
@@ -654,7 +1008,7 @@ fn paths_equivalent(left: &Path, right: &Path) -> bool {
654
1008
  }
655
1009
  let left = std::fs::canonicalize(left).unwrap_or_else(|_| left.to_path_buf());
656
1010
  let right = std::fs::canonicalize(right).unwrap_or_else(|_| right.to_path_buf());
657
- left == right || left.starts_with(&right)
1011
+ left == right || left.parent().is_some_and(|parent| parent == right)
658
1012
  }
659
1013
 
660
1014
  /// `true` iff any path component is `.team` (the Team Agent runtime/logs root) — used
@@ -686,8 +1040,9 @@ fn claude_launch_command(
686
1040
  mcp_config: Option<&McpConfig>,
687
1041
  system_prompt: Option<&str>,
688
1042
  model: Option<&str>,
1043
+ tools: &[&str],
689
1044
  ) -> Result<Vec<String>, ProviderError> {
690
- let mut argv = claude_base_command(adapter, auth_mode, mcp_config, system_prompt, model)?;
1045
+ let mut argv = claude_base_command(adapter, auth_mode, mcp_config, system_prompt, model, tools, false)?;
691
1046
  argv.push("--session-id".to_string());
692
1047
  argv.push(next_session_token());
693
1048
  Ok(argv)
@@ -699,12 +1054,16 @@ fn claude_base_command(
699
1054
  mcp_config: Option<&McpConfig>,
700
1055
  system_prompt: Option<&str>,
701
1056
  model: Option<&str>,
1057
+ tools: &[&str],
1058
+ managed_mcp_config: bool,
702
1059
  ) -> Result<Vec<String>, ProviderError> {
703
- let mut argv = vec![
704
- "claude".to_string(),
705
- "--permission-mode".to_string(),
706
- "default".to_string(),
707
- ];
1060
+ let mut argv = vec!["claude".to_string()];
1061
+ if claude_dangerous_auto_approve(tools) {
1062
+ argv.push("--dangerously-skip-permissions".to_string());
1063
+ } else {
1064
+ argv.push("--permission-mode".to_string());
1065
+ argv.push("default".to_string());
1066
+ }
708
1067
  if let Some(model) = model {
709
1068
  argv.push("--model".to_string());
710
1069
  argv.push(model.to_string());
@@ -713,7 +1072,11 @@ fn claude_base_command(
713
1072
  argv.push("--append-system-prompt".to_string());
714
1073
  argv.push(prompt.to_string());
715
1074
  }
716
- if mcp_config.is_some() || auth_mode == AuthMode::CompatibleApi || system_prompt.is_some_and(prompt_needs_native_mcp) {
1075
+ if !managed_mcp_config
1076
+ && (mcp_config.is_some()
1077
+ || auth_mode == AuthMode::CompatibleApi
1078
+ || system_prompt.is_some_and(prompt_needs_native_mcp))
1079
+ {
717
1080
  let raw = if let Some(config) = mcp_config {
718
1081
  serde_json::json!({"mcpServers": config.raw.clone()})
719
1082
  } else {
@@ -722,10 +1085,10 @@ fn claude_base_command(
722
1085
  argv.push("--mcp-config".to_string());
723
1086
  argv.push(raw.to_string());
724
1087
  argv.push("--strict-mcp-config".to_string());
725
- for tool in ["Bash", "Grep"] {
726
- argv.push("--disallowedTools".to_string());
727
- argv.push(tool.to_string());
728
- }
1088
+ }
1089
+ for tool in claude_disallowed_tools(tools) {
1090
+ argv.push("--disallowedTools".to_string());
1091
+ argv.push(tool.to_string());
729
1092
  }
730
1093
  Ok(argv)
731
1094
  }
@@ -825,6 +1188,27 @@ fn codex_dangerous_auto_approve(tools: &[&str]) -> bool {
825
1188
  tools.contains(&"dangerous_auto_approve")
826
1189
  }
827
1190
 
1191
+ fn claude_dangerous_auto_approve(tools: &[&str]) -> bool {
1192
+ tools.contains(&"dangerous_auto_approve")
1193
+ }
1194
+
1195
+ fn claude_disallowed_tools(tools: &[&str]) -> Vec<&'static str> {
1196
+ let mut disallowed = Vec::new();
1197
+ if !tools.contains(&"execute_bash") {
1198
+ disallowed.push("Bash");
1199
+ }
1200
+ if !tools.contains(&"fs_read") {
1201
+ disallowed.push("Read");
1202
+ }
1203
+ if !tools.contains(&"fs_write") {
1204
+ disallowed.extend(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
1205
+ }
1206
+ if !tools.contains(&"fs_list") {
1207
+ disallowed.extend(["Glob", "Grep"]);
1208
+ }
1209
+ disallowed
1210
+ }
1211
+
828
1212
  fn codex_sandbox_mode(tools: &[&str]) -> &'static str {
829
1213
  if tools.iter().any(|tool| matches!(*tool, "fs_write" | "execute_bash")) {
830
1214
  "workspace-write"
@@ -879,8 +1263,40 @@ fn has_cwd_field(record: &serde_json::Value) -> bool {
879
1263
  }
880
1264
 
881
1265
  fn next_session_token() -> String {
1266
+ use sha2::Digest;
1267
+
1268
+ static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1269
+
882
1270
  let nanos = std::time::SystemTime::now()
883
1271
  .duration_since(std::time::UNIX_EPOCH)
884
1272
  .map_or(0, |d| d.as_nanos());
885
- format!("session-{nanos:x}")
1273
+ let counter = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1274
+ let mut hasher = sha2::Sha256::new();
1275
+ hasher.update(nanos.to_le_bytes());
1276
+ hasher.update(std::process::id().to_le_bytes());
1277
+ hasher.update(counter.to_le_bytes());
1278
+ let digest = hasher.finalize();
1279
+ let mut bytes = [0u8; 16];
1280
+ bytes.copy_from_slice(&digest[..16]);
1281
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
1282
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
1283
+ format!(
1284
+ "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
1285
+ bytes[0],
1286
+ bytes[1],
1287
+ bytes[2],
1288
+ bytes[3],
1289
+ bytes[4],
1290
+ bytes[5],
1291
+ bytes[6],
1292
+ bytes[7],
1293
+ bytes[8],
1294
+ bytes[9],
1295
+ bytes[10],
1296
+ bytes[11],
1297
+ bytes[12],
1298
+ bytes[13],
1299
+ bytes[14],
1300
+ bytes[15],
1301
+ )
886
1302
  }