@team-agent/installer 0.5.59 → 0.5.61

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 (68) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +44 -1
  4. package/crates/team-agent/src/cli/diagnose.rs +183 -0
  5. package/crates/team-agent/src/cli/emit.rs +63 -14
  6. package/crates/team-agent/src/cli/leader.rs +8 -1
  7. package/crates/team-agent/src/cli/mod.rs +263 -5
  8. package/crates/team-agent/src/cli/named_address.rs +18 -1
  9. package/crates/team-agent/src/cli/send/presentation.rs +2 -0
  10. package/crates/team-agent/src/cli/spec.rs +4 -1
  11. package/crates/team-agent/src/cli/status_port/store.rs +7 -3
  12. package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
  13. package/crates/team-agent/src/cli/types.rs +33 -0
  14. package/crates/team-agent/src/communication_mode/mod.rs +53 -0
  15. package/crates/team-agent/src/compiler/tests.rs +5 -5
  16. package/crates/team-agent/src/compiler.rs +53 -1
  17. package/crates/team-agent/src/coordinator/tick.rs +21 -8
  18. package/crates/team-agent/src/db/message_store.rs +45 -1
  19. package/crates/team-agent/src/fake_worker.rs +21 -1
  20. package/crates/team-agent/src/leader/start.rs +732 -94
  21. package/crates/team-agent/src/leader/tests/identity.rs +64 -57
  22. package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
  23. package/crates/team-agent/src/lib.rs +4 -0
  24. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +58 -0
  25. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +57 -51
  26. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +148 -8
  27. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +5 -0
  28. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  29. package/crates/team-agent/src/lifecycle/launch.rs +1 -1
  30. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
  31. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  32. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +35 -1
  33. package/crates/team-agent/src/lifecycle/types.rs +1 -0
  34. package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
  35. package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
  36. package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
  37. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  38. package/crates/team-agent/src/mcp_server/tools.rs +100 -66
  39. package/crates/team-agent/src/mcp_server/types.rs +5 -0
  40. package/crates/team-agent/src/mcp_server/wire.rs +27 -21
  41. package/crates/team-agent/src/messaging/delivery.rs +105 -38
  42. package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
  43. package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
  44. package/crates/team-agent/src/messaging/presentation.rs +109 -0
  45. package/crates/team-agent/src/messaging/results.rs +165 -17
  46. package/crates/team-agent/src/messaging/send.rs +94 -81
  47. package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
  48. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
  49. package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
  50. package/crates/team-agent/src/messaging/types.rs +5 -0
  51. package/crates/team-agent/src/messaging/watchers.rs +5 -0
  52. package/crates/team-agent/src/model/mod.rs +1 -0
  53. package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
  54. package/crates/team-agent/src/model/spec.rs +16 -0
  55. package/crates/team-agent/src/provider/adapter.rs +57 -0
  56. package/crates/team-agent/src/provider/adapters/claude_fork.rs +10 -2
  57. package/crates/team-agent/src/provider/session/capture.rs +69 -34
  58. package/crates/team-agent/src/provider/session/context_fork/codex.rs +386 -9
  59. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +3 -14
  60. package/crates/team-agent/src/provider/session/context_fork.rs +7 -2
  61. package/crates/team-agent/src/provider/session/mod.rs +3 -2
  62. package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
  63. package/crates/team-agent/src/provider/session_scan/codex.rs +28 -0
  64. package/crates/team-agent/src/provider/session_scan/common.rs +34 -8
  65. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  66. package/crates/team-agent/src/topology.rs +3 -0
  67. package/package.json +4 -4
  68. package/skills/team-agent/command-coverage.json +379 -0
@@ -5,10 +5,16 @@ use std::io::{IsTerminal, Read, Write};
5
5
  use std::path::{Path, PathBuf};
6
6
  use std::process::{Command, Stdio};
7
7
 
8
+ #[cfg(target_os = "linux")]
9
+ use std::os::fd::AsRawFd;
10
+ #[cfg(unix)]
11
+ use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt};
12
+
13
+ use crate::model::pane_authority_refusal as pane_refusal;
8
14
  use crate::provider::{get_adapter, Provider};
9
15
  use crate::tmux_backend::TmuxBackend;
10
16
  use crate::transport::{
11
- PaneId, PaneLiveness, SessionName, SpawnResult, Target, Transport, WindowName,
17
+ PaneId, PaneInfo, PaneLiveness, SessionName, SpawnResult, Target, Transport, WindowName,
12
18
  };
13
19
 
14
20
  use super::helpers::{
@@ -22,6 +28,50 @@ use super::{
22
28
 
23
29
  // ── leader::start — leader_start_plan / start_leader / session 名 ──
24
30
 
31
+ pub(crate) struct PreparedLeaderStart {
32
+ plan: LeaderStartPlan,
33
+ ambient_authority: Option<VerifiedAmbientPaneAuthority>,
34
+ }
35
+
36
+ #[derive(Clone, Copy)]
37
+ enum ManagedClientAttachMode {
38
+ AttachSession,
39
+ SwitchClient,
40
+ }
41
+
42
+ const DIFFERENT_TMUX_SERVER_PREFIX: &str =
43
+ "managed launcher refuses a different ambient tmux server";
44
+
45
+ impl PreparedLeaderStart {
46
+ pub(crate) fn plan(&self) -> &LeaderStartPlan {
47
+ &self.plan
48
+ }
49
+ }
50
+
51
+ pub(crate) enum PrepareLeaderStartError {
52
+ PaneAuthorityRefused(pane_refusal::PaneAuthorityRefusal),
53
+ Leader(LeaderError),
54
+ }
55
+
56
+ impl PrepareLeaderStartError {
57
+ fn into_leader_error(self) -> LeaderError {
58
+ match self {
59
+ Self::PaneAuthorityRefused(refusal) => LeaderError::Validation(format!(
60
+ "{}; open a new terminal outside the current tmux/pane or run \
61
+ `team-agent attach-leader` from the intended workspace pane",
62
+ refusal.reason().as_str()
63
+ )),
64
+ Self::Leader(error) => error,
65
+ }
66
+ }
67
+ }
68
+
69
+ impl From<LeaderError> for PrepareLeaderStartError {
70
+ fn from(error: LeaderError) -> Self {
71
+ Self::Leader(error)
72
+ }
73
+ }
74
+
25
75
  /// `leader_start_plan`(card §46;`__init__.py:82`)。计算 leader 启动计划
26
76
  /// (exec in-TMUX / new tmux session / attach existing)。provider 未安装 → `Err(Start)`。
27
77
  pub fn leader_start_plan(
@@ -32,6 +82,122 @@ pub fn leader_start_plan(
32
82
  confirm_attach: bool,
33
83
  attach_session: Option<&SessionName>,
34
84
  external_leader: bool,
85
+ ) -> Result<LeaderStartPlan, LeaderError> {
86
+ prepare_leader_start(
87
+ provider,
88
+ provider_args,
89
+ workspace,
90
+ attach_existing,
91
+ confirm_attach,
92
+ attach_session,
93
+ external_leader,
94
+ )
95
+ .map(|prepared| prepared.plan)
96
+ .map_err(PrepareLeaderStartError::into_leader_error)
97
+ }
98
+
99
+ pub(crate) fn prepare_leader_start(
100
+ provider: Provider,
101
+ provider_args: &[String],
102
+ workspace: &Path,
103
+ attach_existing: bool,
104
+ confirm_attach: bool,
105
+ attach_session: Option<&SessionName>,
106
+ external_leader: bool,
107
+ ) -> Result<PreparedLeaderStart, PrepareLeaderStartError> {
108
+ prepare_leader_start_with_nested_attach(
109
+ provider,
110
+ provider_args,
111
+ workspace,
112
+ attach_existing,
113
+ confirm_attach,
114
+ attach_session,
115
+ external_leader,
116
+ false,
117
+ )
118
+ }
119
+
120
+ pub(crate) fn prepare_leader_start_with_nested_attach(
121
+ provider: Provider,
122
+ provider_args: &[String],
123
+ workspace: &Path,
124
+ attach_existing: bool,
125
+ confirm_attach: bool,
126
+ attach_session: Option<&SessionName>,
127
+ external_leader: bool,
128
+ allow_nested_attach: bool,
129
+ ) -> Result<PreparedLeaderStart, PrepareLeaderStartError> {
130
+ let explicit_external_path = external_leader || attach_existing || attach_session.is_some();
131
+ let state_external_path = workspace_state_uses_external_leader(workspace);
132
+ let (ambient_authority, managed_client_attach_mode, managed_provider_reentry) =
133
+ if explicit_external_path || state_external_path {
134
+ (ambient_pane_authority_preflight(workspace)?, None, false)
135
+ } else if workspace_state_is_managed_provider_reentry(workspace) {
136
+ (ambient_pane_authority_preflight(workspace)?, None, true)
137
+ } else {
138
+ let (authority, attach_mode) =
139
+ managed_launcher_ambient_route(workspace, allow_nested_attach)?;
140
+ (authority, attach_mode, false)
141
+ };
142
+ let plan = leader_start_plan_with_ambient_authority(
143
+ provider,
144
+ provider_args,
145
+ workspace,
146
+ attach_existing,
147
+ confirm_attach,
148
+ attach_session,
149
+ external_leader,
150
+ std::env::var_os("TMUX").is_some(),
151
+ managed_client_attach_mode,
152
+ managed_provider_reentry,
153
+ )?;
154
+ Ok(PreparedLeaderStart {
155
+ plan,
156
+ ambient_authority,
157
+ })
158
+ }
159
+
160
+ pub(crate) fn leader_start_plan_after_ambient_authority_check(
161
+ provider: Provider,
162
+ provider_args: &[String],
163
+ workspace: &Path,
164
+ attach_existing: bool,
165
+ confirm_attach: bool,
166
+ attach_session: Option<&SessionName>,
167
+ external_leader: bool,
168
+ ) -> Result<LeaderStartPlan, LeaderError> {
169
+ let in_tmux = std::env::var_os("TMUX").is_some();
170
+ let explicit_external_path = external_leader || attach_existing || attach_session.is_some();
171
+ let managed_client_attach_mode = (!explicit_external_path).then_some(if in_tmux {
172
+ ManagedClientAttachMode::SwitchClient
173
+ } else {
174
+ ManagedClientAttachMode::AttachSession
175
+ });
176
+ leader_start_plan_with_ambient_authority(
177
+ provider,
178
+ provider_args,
179
+ workspace,
180
+ attach_existing,
181
+ confirm_attach,
182
+ attach_session,
183
+ external_leader,
184
+ in_tmux,
185
+ managed_client_attach_mode,
186
+ false,
187
+ )
188
+ }
189
+
190
+ fn leader_start_plan_with_ambient_authority(
191
+ provider: Provider,
192
+ provider_args: &[String],
193
+ workspace: &Path,
194
+ attach_existing: bool,
195
+ confirm_attach: bool,
196
+ attach_session: Option<&SessionName>,
197
+ external_leader: bool,
198
+ in_tmux: bool,
199
+ managed_client_attach_mode: Option<ManagedClientAttachMode>,
200
+ managed_provider_reentry: bool,
35
201
  ) -> Result<LeaderStartPlan, LeaderError> {
36
202
  if attach_session.is_some() && !confirm_attach {
37
203
  return Err(LeaderError::Start(
@@ -54,7 +220,12 @@ pub fn leader_start_plan(
54
220
  }
55
221
  let state = crate::state::persist::load_runtime_state(workspace).ok();
56
222
  let identity = leader_identity_context(workspace, None, state.as_ref())?;
57
- let external_path = external_leader || attach_existing || attach_session.is_some();
223
+ let external_path = external_leader
224
+ || attach_existing
225
+ || attach_session.is_some()
226
+ || state
227
+ .as_ref()
228
+ .is_some_and(crate::state::projection::state_is_external_leader);
58
229
  // 0.3.28 Step 2: managed mode now uses the SAME dedicated leader session
59
230
  // as the external path (`team-agent-leader-<provider>-<folder>-<sha1[:8]>`)
60
231
  // — Python parity. Pre-0.3.28 the managed branch used
@@ -76,7 +247,6 @@ pub fn leader_start_plan(
76
247
  } else {
77
248
  Some(managed_leader_session_name(provider, workspace))
78
249
  };
79
- let in_tmux = std::env::var_os("TMUX").is_some();
80
250
  if !in_tmux {
81
251
  ensure_tmux_installed()?;
82
252
  }
@@ -89,7 +259,7 @@ pub fn leader_start_plan(
89
259
  } else {
90
260
  false
91
261
  };
92
- let mode = if !external_path && in_tmux {
262
+ let mode = if managed_provider_reentry {
93
263
  LeaderStartMode::ExecProvider
94
264
  } else if !external_path {
95
265
  LeaderStartMode::ManagedTmuxClient
@@ -108,6 +278,7 @@ pub fn leader_start_plan(
108
278
  workspace,
109
279
  session_name.as_ref(),
110
280
  &leader_env,
281
+ managed_client_attach_mode,
111
282
  )?;
112
283
  let plan_session_name = if mode == LeaderStartMode::ExecProvider && !external_path {
113
284
  None
@@ -188,7 +359,7 @@ pub fn start_leader(
188
359
  attach_session: Option<&SessionName>,
189
360
  external_leader: bool,
190
361
  ) -> Result<(), LeaderError> {
191
- let plan = leader_start_plan(
362
+ let prepared = prepare_leader_start(
192
363
  provider,
193
364
  provider_args,
194
365
  workspace,
@@ -196,7 +367,9 @@ pub fn start_leader(
196
367
  confirm_attach,
197
368
  attach_session,
198
369
  external_leader,
199
- )?;
370
+ )
371
+ .map_err(PrepareLeaderStartError::into_leader_error)?;
372
+ let plan = prepared.plan();
200
373
  crate::event_log::EventLog::new(workspace).write(
201
374
  super::LeaderEvent::LeaderStart.name(),
202
375
  serde_json::json!({
@@ -205,7 +378,7 @@ pub fn start_leader(
205
378
  "session_name": plan.session_name.as_ref().map(|s| s.as_str().to_string()),
206
379
  }),
207
380
  )?;
208
- execute_leader_plan(&plan, workspace).map(|_| ())
381
+ execute_prepared_leader_start(&prepared, workspace).map(|_| ())
209
382
  }
210
383
 
211
384
  /// Execute a precomputed leader launch plan.
@@ -215,6 +388,41 @@ pub fn start_leader(
215
388
  pub fn execute_leader_plan(
216
389
  plan: &LeaderStartPlan,
217
390
  workspace: &Path,
391
+ ) -> Result<LeaderLaunchOutcome, LeaderError> {
392
+ let ambient_authority = if plan.mode == LeaderStartMode::ExecProvider {
393
+ match ambient_pane_authority_preflight(workspace) {
394
+ Ok(Some(authority)) => Some(authority),
395
+ Ok(None) => {
396
+ return Err(LeaderError::Validation(
397
+ "exec provider ambient pane authority missing".to_string(),
398
+ ));
399
+ }
400
+ Err(PrepareLeaderStartError::PaneAuthorityRefused(refusal)) => {
401
+ return Ok(LeaderLaunchOutcome::not_started(refusal.reason().as_str()));
402
+ }
403
+ Err(PrepareLeaderStartError::Leader(error)) => return Err(error),
404
+ }
405
+ } else {
406
+ None
407
+ };
408
+ execute_leader_plan_after_ambient_authority(plan, workspace, ambient_authority.as_ref())
409
+ }
410
+
411
+ pub(crate) fn execute_prepared_leader_start(
412
+ prepared: &PreparedLeaderStart,
413
+ workspace: &Path,
414
+ ) -> Result<LeaderLaunchOutcome, LeaderError> {
415
+ execute_leader_plan_after_ambient_authority(
416
+ &prepared.plan,
417
+ workspace,
418
+ prepared.ambient_authority.as_ref(),
419
+ )
420
+ }
421
+
422
+ fn execute_leader_plan_after_ambient_authority(
423
+ plan: &LeaderStartPlan,
424
+ workspace: &Path,
425
+ ambient_authority: Option<&VerifiedAmbientPaneAuthority>,
218
426
  ) -> Result<LeaderLaunchOutcome, LeaderError> {
219
427
  if plan.mode == LeaderStartMode::ManagedTmuxClient {
220
428
  return execute_managed_leader_plan(plan, workspace);
@@ -223,21 +431,31 @@ pub fn execute_leader_plan(
223
431
  let detached = plan.mode == LeaderStartMode::NewTmuxSession
224
432
  && !std::io::stdin().is_terminal()
225
433
  && insert_detach_flag(&mut argv);
226
- if plan.mode == LeaderStartMode::ExecProvider && !plan.is_external_leader {
434
+ let bind_verified_ambient = plan.mode == LeaderStartMode::ExecProvider
435
+ && (!plan.is_external_leader || workspace_state_uses_external_leader(workspace));
436
+ if bind_verified_ambient {
437
+ let authority = ambient_authority.ok_or_else(|| {
438
+ LeaderError::Validation("exec provider ambient pane authority missing".to_string())
439
+ })?;
227
440
  // 0.5.35 (`.team/artifacts/managed-leader-provider-reentry-locate.md`
228
441
  // §5/§6): three-way classify BEFORE persist. Same physical pane =
229
442
  // provider process replacement (refresh only). Different pane with
230
443
  // an existing canonical binding = no canonical rewrite (claim /
231
444
  // takeover is the recovery path, §7). Unbound = first-time write.
232
- match classify_exec_provider_binding(workspace, plan)? {
445
+ match classify_exec_provider_binding(workspace, plan, authority)? {
233
446
  ExecProviderBinding::ManagedReentry => {
234
- refresh_managed_leader_provider_binding(plan, workspace)?;
447
+ refresh_managed_leader_provider_binding(plan, workspace, authority)?;
235
448
  }
236
449
  ExecProviderBinding::DifferentPaneAlreadyBound => {
237
450
  // No canonical state write. Provider still runs below.
238
451
  }
239
452
  ExecProviderBinding::Unbound => {
240
- persist_exec_provider_leader_binding(plan, workspace)?;
453
+ persist_exec_provider_leader_binding(
454
+ plan,
455
+ workspace,
456
+ authority,
457
+ plan.is_external_leader,
458
+ )?;
241
459
  }
242
460
  }
243
461
  } else if plan.is_external_leader {
@@ -266,6 +484,402 @@ pub fn execute_leader_plan(
266
484
  }
267
485
  }
268
486
 
487
+ struct VerifiedAmbientPaneAuthority {
488
+ pane_id: PaneId,
489
+ observed: PaneInfo,
490
+ endpoint: String,
491
+ tmux: String,
492
+ }
493
+
494
+ fn workspace_state_uses_external_leader(workspace: &Path) -> bool {
495
+ crate::state::persist::load_runtime_state_without_migrations(workspace)
496
+ .ok()
497
+ .as_ref()
498
+ .is_some_and(crate::state::projection::state_is_external_leader)
499
+ }
500
+
501
+ fn workspace_state_is_managed_provider_reentry(workspace: &Path) -> bool {
502
+ let Ok(state) = crate::state::persist::load_runtime_state_without_migrations(workspace) else {
503
+ return false;
504
+ };
505
+ if crate::state::projection::state_is_external_leader(&state) {
506
+ return false;
507
+ }
508
+ let Some(team_key) = state
509
+ .get("active_team_key")
510
+ .and_then(serde_json::Value::as_str)
511
+ else {
512
+ return false;
513
+ };
514
+ let Some(receiver) = state
515
+ .get("teams")
516
+ .and_then(serde_json::Value::as_object)
517
+ .and_then(|teams| teams.get(team_key))
518
+ .and_then(|team| team.get("leader_receiver"))
519
+ else {
520
+ return false;
521
+ };
522
+ let Some(pane) = std::env::var_os("TMUX_PANE") else {
523
+ return false;
524
+ };
525
+ let Some(tmux) = std::env::var_os("TMUX") else {
526
+ return false;
527
+ };
528
+ let tmux = tmux.to_string_lossy();
529
+ let endpoint = tmux.split(',').next().unwrap_or("").trim();
530
+ receiver.get("status").and_then(serde_json::Value::as_str) == Some("attached")
531
+ && receiver.get("pane_id").and_then(serde_json::Value::as_str) == pane.to_str()
532
+ && receiver
533
+ .get("tmux_socket")
534
+ .and_then(serde_json::Value::as_str)
535
+ == Some(endpoint)
536
+ && receiver
537
+ .get("session_name")
538
+ .and_then(serde_json::Value::as_str)
539
+ .is_some_and(|session| session.starts_with(LEADER_SESSION_PREFIX))
540
+ }
541
+
542
+ fn ambient_pane_authority_preflight(
543
+ workspace: &Path,
544
+ ) -> Result<Option<VerifiedAmbientPaneAuthority>, PrepareLeaderStartError> {
545
+ let Some(tmux) = std::env::var_os("TMUX") else {
546
+ return Ok(None);
547
+ };
548
+ let tmux = tmux.into_string().map_err(|_| {
549
+ PrepareLeaderStartError::PaneAuthorityRefused(ambient_tmux_endpoint_refusal(
550
+ workspace,
551
+ pane_refusal::AmbientTmuxEndpointUnavailableCause::TmuxValueNotUnicode,
552
+ ))
553
+ })?;
554
+ verified_ambient_pane_authority(workspace, &tmux)
555
+ .map(Some)
556
+ .map_err(PrepareLeaderStartError::PaneAuthorityRefused)
557
+ }
558
+
559
+ fn managed_launcher_ambient_route(
560
+ workspace: &Path,
561
+ allow_nested_attach: bool,
562
+ ) -> Result<
563
+ (
564
+ Option<VerifiedAmbientPaneAuthority>,
565
+ Option<ManagedClientAttachMode>,
566
+ ),
567
+ PrepareLeaderStartError,
568
+ > {
569
+ let Some(tmux) = std::env::var_os("TMUX") else {
570
+ return Ok((None, Some(ManagedClientAttachMode::AttachSession)));
571
+ };
572
+ let tmux = tmux.into_string().map_err(|_| {
573
+ PrepareLeaderStartError::PaneAuthorityRefused(ambient_tmux_endpoint_refusal(
574
+ workspace,
575
+ pane_refusal::AmbientTmuxEndpointUnavailableCause::TmuxValueNotUnicode,
576
+ ))
577
+ })?;
578
+ let observed_endpoint = validated_ambient_tmux_endpoint(workspace, &tmux)
579
+ .map_err(PrepareLeaderStartError::PaneAuthorityRefused)?;
580
+ let target_socket_name = TmuxBackend::for_workspace(workspace)
581
+ .tmux_endpoint()
582
+ .ok_or_else(|| LeaderError::Start("workspace tmux endpoint missing".to_string()))?;
583
+ let target_endpoint = crate::tmux_backend::socket_path_for_workspace(workspace);
584
+ let same_server = target_endpoint.as_ref().is_some_and(|target| {
585
+ let observed = Path::new(&observed_endpoint)
586
+ .canonicalize()
587
+ .unwrap_or_else(|_| PathBuf::from(&observed_endpoint));
588
+ let target = target.canonicalize().unwrap_or_else(|_| target.clone());
589
+ observed == target
590
+ });
591
+ if same_server {
592
+ let authority = verified_ambient_pane_authority(workspace, &tmux)
593
+ .map_err(PrepareLeaderStartError::PaneAuthorityRefused)?;
594
+ return Ok((Some(authority), Some(ManagedClientAttachMode::SwitchClient)));
595
+ }
596
+ if !allow_nested_attach {
597
+ return Err(LeaderError::Validation(format!(
598
+ "{DIFFERENT_TMUX_SERVER_PREFIX}: observed_endpoint={observed_endpoint}; \
599
+ requested_workspace_socket={target_socket_name}"
600
+ ))
601
+ .into());
602
+ }
603
+ let authority = verified_ambient_pane_authority(workspace, &tmux)
604
+ .map_err(PrepareLeaderStartError::PaneAuthorityRefused)?;
605
+ Ok((
606
+ Some(authority),
607
+ Some(ManagedClientAttachMode::AttachSession),
608
+ ))
609
+ }
610
+
611
+ fn validated_ambient_tmux_endpoint(
612
+ workspace: &Path,
613
+ tmux: &str,
614
+ ) -> Result<String, pane_refusal::PaneAuthorityRefusal> {
615
+ let tuple = tmux.split(',').map(str::trim).collect::<Vec<_>>();
616
+ let [endpoint, server_pid, session_index] = tuple.as_slice() else {
617
+ return Err(ambient_tmux_endpoint_refusal(
618
+ workspace,
619
+ pane_refusal::AmbientTmuxEndpointUnavailableCause::TmuxTupleFieldCountInvalid,
620
+ ));
621
+ };
622
+ if endpoint.is_empty() {
623
+ return Err(ambient_tmux_endpoint_refusal(
624
+ workspace,
625
+ pane_refusal::AmbientTmuxEndpointUnavailableCause::EndpointMissing,
626
+ ));
627
+ }
628
+ if !Path::new(endpoint).is_absolute() {
629
+ return Err(ambient_tmux_endpoint_refusal(
630
+ workspace,
631
+ pane_refusal::AmbientTmuxEndpointUnavailableCause::EndpointNotAbsolute,
632
+ ));
633
+ }
634
+ if server_pid.parse::<u32>().is_err() {
635
+ return Err(ambient_tmux_endpoint_refusal(
636
+ workspace,
637
+ pane_refusal::AmbientTmuxEndpointUnavailableCause::ServerPidInvalid,
638
+ ));
639
+ }
640
+ if session_index.parse::<u32>().is_err() {
641
+ return Err(ambient_tmux_endpoint_refusal(
642
+ workspace,
643
+ pane_refusal::AmbientTmuxEndpointUnavailableCause::SessionIndexInvalid,
644
+ ));
645
+ }
646
+ Ok((*endpoint).to_string())
647
+ }
648
+
649
+ fn verified_ambient_pane_authority(
650
+ workspace: &Path,
651
+ tmux: &str,
652
+ ) -> Result<VerifiedAmbientPaneAuthority, pane_refusal::PaneAuthorityRefusal> {
653
+ let requested_workspace = resolve_workspace_for_hash(workspace);
654
+ let endpoint = validated_ambient_tmux_endpoint(workspace, tmux)?;
655
+ let pane_id = match std::env::var_os("TMUX_PANE") {
656
+ None => {
657
+ return Err(pane_refusal::PaneAuthorityRefusal::new(
658
+ pane_refusal::PaneAuthorityRefusalFacts::AmbientPaneIdUnavailable(
659
+ pane_refusal::AmbientPaneIdUnavailableFacts {
660
+ requested_workspace,
661
+ observed_pane_id: pane_refusal::UnavailablePaneAuthorityFact::new(
662
+ pane_refusal::AmbientPaneIdUnavailableCause::EnvironmentValueMissing,
663
+ ),
664
+ endpoint,
665
+ },
666
+ ),
667
+ ));
668
+ }
669
+ Some(value) => match value.into_string() {
670
+ Ok(value) if value.trim().is_empty() => {
671
+ return Err(pane_refusal::PaneAuthorityRefusal::new(
672
+ pane_refusal::PaneAuthorityRefusalFacts::AmbientPaneIdUnavailable(
673
+ pane_refusal::AmbientPaneIdUnavailableFacts {
674
+ requested_workspace,
675
+ observed_pane_id: pane_refusal::UnavailablePaneAuthorityFact::new(
676
+ pane_refusal::AmbientPaneIdUnavailableCause::EnvironmentValueEmpty,
677
+ ),
678
+ endpoint,
679
+ },
680
+ ),
681
+ ));
682
+ }
683
+ Ok(value) => PaneId::new(value.trim().to_string()),
684
+ Err(_) => {
685
+ return Err(pane_refusal::PaneAuthorityRefusal::new(
686
+ pane_refusal::PaneAuthorityRefusalFacts::AmbientPaneIdUnavailable(
687
+ pane_refusal::AmbientPaneIdUnavailableFacts {
688
+ requested_workspace,
689
+ observed_pane_id: pane_refusal::UnavailablePaneAuthorityFact::new(
690
+ pane_refusal::AmbientPaneIdUnavailableCause::EnvironmentValueNotUnicode,
691
+ ),
692
+ endpoint,
693
+ },
694
+ ),
695
+ ));
696
+ }
697
+ },
698
+ };
699
+ let observed = current_tmux_pane_info(&pane_id, &endpoint).map_err(|error| {
700
+ let cause = match error {
701
+ CurrentTmuxPaneInfoError::QueryFailed => {
702
+ pane_refusal::AmbientPaneWorkspaceUnavailableCause::PaneQueryFailed
703
+ }
704
+ CurrentTmuxPaneInfoError::PaneNotFound => {
705
+ pane_refusal::AmbientPaneWorkspaceUnavailableCause::PaneNotFound
706
+ }
707
+ };
708
+ pane_refusal::PaneAuthorityRefusal::new(
709
+ pane_refusal::PaneAuthorityRefusalFacts::AmbientPaneWorkspaceUnavailable(
710
+ pane_refusal::AmbientPaneWorkspaceUnavailableFacts {
711
+ requested_workspace: requested_workspace.clone(),
712
+ observed_pane_id: pane_id.as_str().to_string(),
713
+ observed_pane_workspace: pane_refusal::UnavailablePaneAuthorityFact::new(cause),
714
+ endpoint: endpoint.clone(),
715
+ },
716
+ ),
717
+ )
718
+ })?;
719
+ let pane_workspace = observed.current_path.clone().ok_or_else(|| {
720
+ pane_refusal::PaneAuthorityRefusal::new(
721
+ pane_refusal::PaneAuthorityRefusalFacts::AmbientPaneWorkspaceUnavailable(
722
+ pane_refusal::AmbientPaneWorkspaceUnavailableFacts {
723
+ requested_workspace: requested_workspace.clone(),
724
+ observed_pane_id: pane_id.as_str().to_string(),
725
+ observed_pane_workspace: pane_refusal::UnavailablePaneAuthorityFact::new(
726
+ pane_refusal::AmbientPaneWorkspaceUnavailableCause::CurrentPathMissing,
727
+ ),
728
+ endpoint: endpoint.clone(),
729
+ },
730
+ ),
731
+ )
732
+ })?;
733
+ let caller_tty = caller_controlling_tty().map_err(|cause| {
734
+ pane_refusal::PaneAuthorityRefusal::new(
735
+ pane_refusal::PaneAuthorityRefusalFacts::CallerControllingTtyUnavailable(
736
+ pane_refusal::CallerControllingTtyUnavailableFacts {
737
+ requested_workspace: requested_workspace.clone(),
738
+ observed_pane_id: pane_id.as_str().to_string(),
739
+ endpoint: endpoint.clone(),
740
+ caller_controlling_tty: pane_refusal::UnavailablePaneAuthorityFact::new(cause),
741
+ },
742
+ ),
743
+ )
744
+ })?;
745
+ let pane_tty_path = observed.tty.as_deref().ok_or_else(|| {
746
+ pane_refusal::PaneAuthorityRefusal::new(
747
+ pane_refusal::PaneAuthorityRefusalFacts::ObservedPaneTtyUnavailable(
748
+ pane_refusal::ObservedPaneTtyUnavailableFacts {
749
+ requested_workspace: requested_workspace.clone(),
750
+ observed_pane_id: pane_id.as_str().to_string(),
751
+ endpoint: endpoint.clone(),
752
+ observed_pane_tty: pane_refusal::UnavailablePaneAuthorityFact::new(
753
+ pane_refusal::ObservedPaneTtyUnavailableCause::PaneTtyMissing,
754
+ ),
755
+ },
756
+ ),
757
+ )
758
+ })?;
759
+ let pane_tty = pane_tty_device(pane_tty_path).map_err(|cause| {
760
+ pane_refusal::PaneAuthorityRefusal::new(
761
+ pane_refusal::PaneAuthorityRefusalFacts::ObservedPaneTtyUnavailable(
762
+ pane_refusal::ObservedPaneTtyUnavailableFacts {
763
+ requested_workspace: requested_workspace.clone(),
764
+ observed_pane_id: pane_id.as_str().to_string(),
765
+ endpoint: endpoint.clone(),
766
+ observed_pane_tty: pane_refusal::UnavailablePaneAuthorityFact::new(cause),
767
+ },
768
+ ),
769
+ )
770
+ })?;
771
+ if caller_tty != pane_tty {
772
+ return Err(pane_refusal::PaneAuthorityRefusal::new(
773
+ pane_refusal::PaneAuthorityRefusalFacts::PaneTtyMismatch(
774
+ pane_refusal::PaneTtyMismatchFacts {
775
+ requested_workspace,
776
+ observed_pane_id: pane_id.as_str().to_string(),
777
+ endpoint,
778
+ caller_controlling_tty: caller_tty,
779
+ observed_pane_tty: pane_tty,
780
+ },
781
+ ),
782
+ ));
783
+ }
784
+ if !crate::messaging::leader_channel::path_is_in_workspace(&pane_workspace, workspace) {
785
+ return Err(pane_refusal::PaneAuthorityRefusal::new(
786
+ pane_refusal::PaneAuthorityRefusalFacts::PaneWorkspaceMismatch(
787
+ pane_refusal::PaneWorkspaceMismatchFacts {
788
+ requested_workspace,
789
+ observed_pane_id: pane_id.as_str().to_string(),
790
+ observed_pane_workspace: pane_workspace,
791
+ endpoint,
792
+ },
793
+ ),
794
+ ));
795
+ }
796
+ Ok(VerifiedAmbientPaneAuthority {
797
+ pane_id,
798
+ observed,
799
+ endpoint,
800
+ tmux: tmux.to_string(),
801
+ })
802
+ }
803
+
804
+ fn ambient_tmux_endpoint_refusal(
805
+ workspace: &Path,
806
+ cause: pane_refusal::AmbientTmuxEndpointUnavailableCause,
807
+ ) -> pane_refusal::PaneAuthorityRefusal {
808
+ pane_refusal::PaneAuthorityRefusal::new(
809
+ pane_refusal::PaneAuthorityRefusalFacts::AmbientTmuxEndpointUnavailable(
810
+ pane_refusal::AmbientTmuxEndpointUnavailableFacts {
811
+ requested_workspace: resolve_workspace_for_hash(workspace),
812
+ endpoint: pane_refusal::UnavailablePaneAuthorityFact::new(cause),
813
+ },
814
+ ),
815
+ )
816
+ }
817
+
818
+ #[cfg(target_os = "macos")]
819
+ fn caller_controlling_tty() -> Result<u64, pane_refusal::CallerControllingTtyUnavailableCause> {
820
+ // `/dev/tty` proves that this process has a controlling terminal, but its
821
+ // own path and rdev remain the generic alias. Query the process table for
822
+ // the actual controlling device identity.
823
+ let _controlling_tty = std::fs::File::open("/dev/tty")
824
+ .map_err(|_| pane_refusal::CallerControllingTtyUnavailableCause::NoControllingTty)?;
825
+ let mut info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::zeroed();
826
+ let info_size = std::mem::size_of::<libc::proc_bsdinfo>();
827
+ let read = unsafe {
828
+ libc::proc_pidinfo(
829
+ libc::getpid(),
830
+ libc::PROC_PIDTBSDINFO,
831
+ 0,
832
+ info.as_mut_ptr().cast(),
833
+ info_size as libc::c_int,
834
+ )
835
+ };
836
+ if read != info_size as libc::c_int {
837
+ return Err(pane_refusal::CallerControllingTtyUnavailableCause::DeviceIdentityUnresolvable);
838
+ }
839
+ Ok(u64::from(unsafe { info.assume_init() }.e_tdev))
840
+ }
841
+
842
+ #[cfg(target_os = "linux")]
843
+ fn caller_controlling_tty() -> Result<u64, pane_refusal::CallerControllingTtyUnavailableCause> {
844
+ let controlling_tty = std::fs::OpenOptions::new()
845
+ .read(true)
846
+ .custom_flags(libc::O_NOCTTY | libc::O_CLOEXEC)
847
+ .open("/dev/tty")
848
+ .map_err(|_| pane_refusal::CallerControllingTtyUnavailableCause::NoControllingTty)?;
849
+ let mut device = 0 as libc::c_uint;
850
+ let result = unsafe { libc::ioctl(controlling_tty.as_raw_fd(), libc::TIOCGDEV, &mut device) };
851
+ if result != 0 {
852
+ return Err(pane_refusal::CallerControllingTtyUnavailableCause::DeviceIdentityUnresolvable);
853
+ }
854
+ Ok(u64::from(device))
855
+ }
856
+
857
+ #[cfg(not(any(target_os = "macos", target_os = "linux")))]
858
+ fn caller_controlling_tty() -> Result<u64, pane_refusal::CallerControllingTtyUnavailableCause> {
859
+ Err(pane_refusal::CallerControllingTtyUnavailableCause::PlatformUnsupported)
860
+ }
861
+
862
+ #[cfg(unix)]
863
+ fn pane_tty_device(path: &str) -> Result<u64, pane_refusal::ObservedPaneTtyUnavailableCause> {
864
+ let pane_tty = std::fs::OpenOptions::new()
865
+ .read(true)
866
+ .custom_flags(libc::O_NOCTTY | libc::O_CLOEXEC)
867
+ .open(path)
868
+ .map_err(|_| pane_refusal::ObservedPaneTtyUnavailableCause::PaneTtyUnresolvable)?;
869
+ let metadata = pane_tty
870
+ .metadata()
871
+ .map_err(|_| pane_refusal::ObservedPaneTtyUnavailableCause::PaneTtyUnresolvable)?;
872
+ if !metadata.file_type().is_char_device() {
873
+ return Err(pane_refusal::ObservedPaneTtyUnavailableCause::PaneTtyNotCharacterDevice);
874
+ }
875
+ Ok(metadata.rdev())
876
+ }
877
+
878
+ #[cfg(not(unix))]
879
+ fn pane_tty_device(_path: &str) -> Result<u64, pane_refusal::ObservedPaneTtyUnavailableCause> {
880
+ Err(pane_refusal::ObservedPaneTtyUnavailableCause::PaneTtyUnresolvable)
881
+ }
882
+
269
883
  /// B5: the deterministic leader-session naming prefix IS the ownership truth source —
270
884
  /// shutdown's socket teardown spares sessions carrying it (no separate registry).
271
885
  pub const LEADER_SESSION_PREFIX: &str = "team-agent-leader-";
@@ -329,6 +943,7 @@ fn start_argv(
329
943
  workspace: &Path,
330
944
  session_name: Option<&SessionName>,
331
945
  leader_env: &BTreeMap<String, String>,
946
+ managed_client_attach_mode: Option<ManagedClientAttachMode>,
332
947
  ) -> Result<Vec<String>, LeaderError> {
333
948
  let provider_cmd = provider_command_name(provider).to_string();
334
949
  match mode {
@@ -343,7 +958,10 @@ fn start_argv(
343
958
  "managed leader session missing".to_string(),
344
959
  ));
345
960
  };
346
- managed_client_argv(workspace, session, provider)
961
+ let attach_mode = managed_client_attach_mode.ok_or_else(|| {
962
+ LeaderError::Start("managed client attach mode missing".to_string())
963
+ })?;
964
+ managed_client_argv(workspace, session, provider, attach_mode)
347
965
  }
348
966
  LeaderStartMode::AttachExisting => {
349
967
  let Some(session) = session_name else {
@@ -417,25 +1035,25 @@ fn managed_client_argv(
417
1035
  workspace: &Path,
418
1036
  session: &SessionName,
419
1037
  provider: Provider,
1038
+ attach_mode: ManagedClientAttachMode,
420
1039
  ) -> Result<Vec<String>, LeaderError> {
421
1040
  // 0.3.28 Step 2: leader window inside the dedicated leader session is
422
1041
  // named after `provider_wire(provider)` (e.g. `claude`, `codex`, `fake`),
423
1042
  // never the literal `leader`. Pre-0.3.28 this hardcoded `:leader`.
424
1043
  let target = format!("{}:{}", session.as_str(), provider_wire(provider));
425
- let argv = if std::env::var_os("TMUX").is_some() {
426
- vec![
1044
+ let argv = match attach_mode {
1045
+ ManagedClientAttachMode::SwitchClient => vec![
427
1046
  "tmux".to_string(),
428
1047
  "switch-client".to_string(),
429
1048
  "-t".to_string(),
430
1049
  target,
431
- ]
432
- } else {
433
- vec![
1050
+ ],
1051
+ ManagedClientAttachMode::AttachSession => vec![
434
1052
  "tmux".to_string(),
435
1053
  "attach-session".to_string(),
436
1054
  "-t".to_string(),
437
1055
  target,
438
- ]
1056
+ ],
439
1057
  };
440
1058
  Ok(TmuxBackend::argv_for_workspace(workspace, &argv))
441
1059
  }
@@ -721,6 +1339,11 @@ fn persist_managed_leader_binding(
721
1339
  let session = spawned.session.as_str().to_string();
722
1340
  let window = spawned.window.as_str().to_string();
723
1341
  let pane = spawned.pane_id.as_str().to_string();
1342
+ let attach_mode = if plan.argv.iter().any(|arg| arg == "switch-client") {
1343
+ "switch-client"
1344
+ } else {
1345
+ "attach-session"
1346
+ };
724
1347
  let receiver = serde_json::json!({
725
1348
  "mode": "direct_tmux",
726
1349
  "status": "attached",
@@ -783,7 +1406,7 @@ fn persist_managed_leader_binding(
783
1406
  "leader_client".to_string(),
784
1407
  serde_json::json!({
785
1408
  "diagnostic_only": true,
786
- "attach_mode": if std::env::var_os("TMUX").is_some() { "switch-client" } else { "attach-session" },
1409
+ "attach_mode": attach_mode,
787
1410
  "tmux": std::env::var("TMUX").ok(),
788
1411
  }),
789
1412
  );
@@ -821,17 +1444,15 @@ fn persist_managed_leader_binding(
821
1444
  fn persist_exec_provider_leader_binding(
822
1445
  plan: &LeaderStartPlan,
823
1446
  workspace: &Path,
1447
+ authority: &VerifiedAmbientPaneAuthority,
1448
+ is_external_leader: bool,
824
1449
  ) -> Result<(), LeaderError> {
825
1450
  let identity = plan
826
1451
  .identity
827
1452
  .as_ref()
828
1453
  .ok_or_else(|| LeaderError::Start("exec provider leader identity missing".to_string()))?;
829
- let pane = std::env::var("TMUX_PANE")
830
- .ok()
831
- .filter(|value| !value.trim().is_empty())
832
- .ok_or_else(|| LeaderError::Start("exec provider leader pane missing".to_string()))?;
833
- let pane_id = PaneId::new(pane.clone());
834
- let target = current_tmux_pane_info(&pane_id);
1454
+ let pane = authority.pane_id.as_str();
1455
+ let target = &authority.observed;
835
1456
  let mut state = crate::state::persist::load_runtime_state(workspace)
836
1457
  .unwrap_or_else(|_| serde_json::json!({}));
837
1458
  let owner_epoch = state
@@ -846,7 +1467,7 @@ fn persist_exec_provider_leader_binding(
846
1467
  .unwrap_or(0)
847
1468
  .saturating_add(1);
848
1469
  let now = chrono::Utc::now().to_rfc3339();
849
- let socket = crate::tmux_backend::socket_name_from_tmux_env();
1470
+ let socket = authority.endpoint.as_str();
850
1471
  let provider = serde_json::to_value(plan.provider)?;
851
1472
  let mut receiver = serde_json::json!({
852
1473
  "mode": "direct_tmux",
@@ -859,24 +1480,18 @@ fn persist_exec_provider_leader_binding(
859
1480
  "attached_at": now,
860
1481
  "discovery": "current_pane",
861
1482
  });
862
- if let Some(target) = target.as_ref() {
863
- if let Some(obj) = receiver.as_object_mut() {
1483
+ if let Some(obj) = receiver.as_object_mut() {
1484
+ obj.insert(
1485
+ "session_name".to_string(),
1486
+ serde_json::json!(target.session.as_str()),
1487
+ );
1488
+ if let Some(window_name) = target.window_name.as_ref() {
864
1489
  obj.insert(
865
- "session_name".to_string(),
866
- serde_json::json!(target.session.as_str()),
1490
+ "window_name".to_string(),
1491
+ serde_json::json!(window_name.as_str()),
867
1492
  );
868
- if let Some(window_name) = target.window_name.as_ref() {
869
- obj.insert(
870
- "window_name".to_string(),
871
- serde_json::json!(window_name.as_str()),
872
- );
873
- }
874
- }
875
- }
876
- if let Some(socket) = socket.as_ref() {
877
- if let Some(obj) = receiver.as_object_mut() {
878
- obj.insert("tmux_socket".to_string(), serde_json::json!(socket));
879
1493
  }
1494
+ obj.insert("tmux_socket".to_string(), serde_json::json!(socket));
880
1495
  }
881
1496
  let owner = serde_json::json!({
882
1497
  "pane_id": pane,
@@ -893,23 +1508,22 @@ fn persist_exec_provider_leader_binding(
893
1508
  "active_team_key".to_string(),
894
1509
  serde_json::json!(identity.team_id.as_str()),
895
1510
  );
896
- if let Some(target) = target.as_ref() {
897
- obj.insert(
898
- "session_name".to_string(),
899
- serde_json::json!(target.session.as_str()),
900
- );
901
- }
902
- if let Some(socket) = socket.as_ref() {
903
- obj.insert("tmux_endpoint".to_string(), serde_json::json!(socket));
904
- obj.insert("tmux_socket".to_string(), serde_json::json!(socket));
905
- }
906
- obj.insert("is_external_leader".to_string(), serde_json::json!(false));
1511
+ obj.insert(
1512
+ "session_name".to_string(),
1513
+ serde_json::json!(target.session.as_str()),
1514
+ );
1515
+ obj.insert("tmux_endpoint".to_string(), serde_json::json!(socket));
1516
+ obj.insert("tmux_socket".to_string(), serde_json::json!(socket));
1517
+ obj.insert(
1518
+ "is_external_leader".to_string(),
1519
+ serde_json::json!(is_external_leader),
1520
+ );
907
1521
  obj.insert(
908
1522
  "leader_client".to_string(),
909
1523
  serde_json::json!({
910
1524
  "diagnostic_only": true,
911
1525
  "attach_mode": "exec-provider",
912
- "tmux": std::env::var("TMUX").ok(),
1526
+ "tmux": authority.tmux.as_str(),
913
1527
  }),
914
1528
  );
915
1529
  }
@@ -954,13 +1568,9 @@ enum ExecProviderBinding {
954
1568
  fn classify_exec_provider_binding(
955
1569
  workspace: &Path,
956
1570
  plan: &LeaderStartPlan,
1571
+ authority: &VerifiedAmbientPaneAuthority,
957
1572
  ) -> Result<ExecProviderBinding, LeaderError> {
958
- let Some(pane) = std::env::var("TMUX_PANE")
959
- .ok()
960
- .filter(|value| !value.trim().is_empty())
961
- else {
962
- return Ok(ExecProviderBinding::Unbound);
963
- };
1573
+ let pane = authority.pane_id.as_str();
964
1574
  let state = match crate::state::persist::load_runtime_state(workspace) {
965
1575
  Ok(state) => state,
966
1576
  Err(_) => return Ok(ExecProviderBinding::Unbound),
@@ -996,12 +1606,11 @@ fn classify_exec_provider_binding(
996
1606
  if receiver_pane != pane {
997
1607
  return Ok(ExecProviderBinding::DifferentPaneAlreadyBound);
998
1608
  }
999
- let pane_id = PaneId::new(pane.clone());
1000
- let target = current_tmux_pane_info(&pane_id);
1001
- let leader_prefixed_session = target
1002
- .as_ref()
1003
- .map(|t| t.session.as_str().starts_with(LEADER_SESSION_PREFIX))
1004
- .unwrap_or(false);
1609
+ let leader_prefixed_session = authority
1610
+ .observed
1611
+ .session
1612
+ .as_str()
1613
+ .starts_with(LEADER_SESSION_PREFIX);
1005
1614
  if !leader_prefixed_session {
1006
1615
  return Ok(ExecProviderBinding::DifferentPaneAlreadyBound);
1007
1616
  }
@@ -1010,8 +1619,7 @@ fn classify_exec_provider_binding(
1010
1619
  .and_then(serde_json::Value::as_str)
1011
1620
  .filter(|s| !s.is_empty());
1012
1621
  if let Some(recorded_socket) = recorded_socket {
1013
- let caller_socket = crate::tmux_backend::socket_name_from_tmux_env();
1014
- if caller_socket.as_deref() != Some(recorded_socket) {
1622
+ if authority.endpoint != recorded_socket {
1015
1623
  return Ok(ExecProviderBinding::DifferentPaneAlreadyBound);
1016
1624
  }
1017
1625
  }
@@ -1027,16 +1635,13 @@ fn classify_exec_provider_binding(
1027
1635
  fn refresh_managed_leader_provider_binding(
1028
1636
  plan: &LeaderStartPlan,
1029
1637
  workspace: &Path,
1638
+ authority: &VerifiedAmbientPaneAuthority,
1030
1639
  ) -> Result<(), LeaderError> {
1031
1640
  let identity = plan.identity.as_ref().ok_or_else(|| {
1032
1641
  LeaderError::Start("managed leader re-entry identity missing".to_string())
1033
1642
  })?;
1034
- let pane = std::env::var("TMUX_PANE")
1035
- .ok()
1036
- .filter(|value| !value.trim().is_empty())
1037
- .ok_or_else(|| LeaderError::Start("managed leader re-entry pane missing".to_string()))?;
1038
- let pane_id = PaneId::new(pane.clone());
1039
- let target = current_tmux_pane_info(&pane_id);
1643
+ let pane = authority.pane_id.as_str();
1644
+ let target = &authority.observed;
1040
1645
  let mut state = crate::state::persist::load_runtime_state(workspace)
1041
1646
  .unwrap_or_else(|_| serde_json::json!({}));
1042
1647
  let team_key = identity.team_id.as_str();
@@ -1078,20 +1683,18 @@ fn refresh_managed_leader_provider_binding(
1078
1683
  obj.insert("owner_epoch".to_string(), serde_json::json!(existing_epoch));
1079
1684
  obj.entry("leader_session_uuid".to_string())
1080
1685
  .or_insert_with(|| serde_json::json!(identity.leader_session_uuid.as_str()));
1081
- if let Some(target) = target.as_ref() {
1686
+ obj.insert(
1687
+ "session_name".to_string(),
1688
+ serde_json::json!(target.session.as_str()),
1689
+ );
1690
+ if let Some(window_name) = target.window_name.as_ref() {
1082
1691
  obj.insert(
1083
- "session_name".to_string(),
1084
- serde_json::json!(target.session.as_str()),
1692
+ "window_name".to_string(),
1693
+ serde_json::json!(window_name.as_str()),
1085
1694
  );
1086
- if let Some(window_name) = target.window_name.as_ref() {
1087
- obj.insert(
1088
- "window_name".to_string(),
1089
- serde_json::json!(window_name.as_str()),
1090
- );
1091
- }
1092
- if let Some(pane_pid) = target.pane_pid {
1093
- obj.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
1094
- }
1695
+ }
1696
+ if let Some(pane_pid) = target.pane_pid {
1697
+ obj.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
1095
1698
  }
1096
1699
  }
1097
1700
  let mut owner = match existing_owner.as_object() {
@@ -1118,7 +1721,7 @@ fn refresh_managed_leader_provider_binding(
1118
1721
  serde_json::json!({
1119
1722
  "diagnostic_only": true,
1120
1723
  "attach_mode": "exec-provider",
1121
- "tmux": std::env::var("TMUX").ok(),
1724
+ "tmux": authority.tmux.as_str(),
1122
1725
  "reentry": true,
1123
1726
  }),
1124
1727
  );
@@ -1138,12 +1741,21 @@ fn refresh_managed_leader_provider_binding(
1138
1741
  Ok(())
1139
1742
  }
1140
1743
 
1141
- fn current_tmux_pane_info(pane_id: &PaneId) -> Option<crate::transport::PaneInfo> {
1142
- tmux_transport_for_current_pane()
1744
+ enum CurrentTmuxPaneInfoError {
1745
+ QueryFailed,
1746
+ PaneNotFound,
1747
+ }
1748
+
1749
+ fn current_tmux_pane_info(
1750
+ pane_id: &PaneId,
1751
+ endpoint: &str,
1752
+ ) -> Result<crate::transport::PaneInfo, CurrentTmuxPaneInfoError> {
1753
+ crate::transport_factory::tmux_endpoint_transport(endpoint)
1143
1754
  .list_targets()
1144
- .ok()?
1755
+ .map_err(|_| CurrentTmuxPaneInfoError::QueryFailed)?
1145
1756
  .into_iter()
1146
1757
  .find(|target| target.pane_id == *pane_id)
1758
+ .ok_or(CurrentTmuxPaneInfoError::PaneNotFound)
1147
1759
  }
1148
1760
 
1149
1761
  fn persist_external_leader_topology_marker(
@@ -1244,6 +1856,12 @@ fn run_leader_argv(
1244
1856
  for key in &env_unset {
1245
1857
  command.env_remove(key);
1246
1858
  }
1859
+ if plan.mode == LeaderStartMode::ManagedTmuxClient
1860
+ && argv.iter().any(|arg| arg == "attach-session")
1861
+ {
1862
+ command.env_remove("TMUX");
1863
+ command.env_remove("TMUX_PANE");
1864
+ }
1247
1865
  let mut child = command.spawn()?;
1248
1866
  let stderr_reader = child.stderr.take().map(|mut child_stderr| {
1249
1867
  std::thread::spawn(move || {
@@ -1543,9 +2161,10 @@ mod tests {
1543
2161
  };
1544
2162
 
1545
2163
  use super::{
1546
- ensure_managed_provider_live_after_attach, execute_leader_plan, format_launcher_failure,
2164
+ ensure_managed_provider_live_after_attach, execute_leader_plan,
2165
+ execute_leader_plan_after_ambient_authority, format_launcher_failure,
1547
2166
  handle_exec_provider_startup_prompts, push_bounded_stderr, shlex_quote,
1548
- LEADER_STDERR_LIMIT,
2167
+ VerifiedAmbientPaneAuthority, LEADER_STDERR_LIMIT,
1549
2168
  };
1550
2169
 
1551
2170
  struct ScriptedTransport {
@@ -1857,7 +2476,7 @@ mod tests {
1857
2476
  detached: false,
1858
2477
  };
1859
2478
 
1860
- let outcome = execute_leader_plan(&plan, &workspace)
2479
+ let outcome = execute_leader_plan_after_ambient_authority(&plan, &workspace, None)
1861
2480
  .expect("external marker must be present before provider argv runs");
1862
2481
 
1863
2482
  assert_eq!(outcome.status, crate::leader::LeaderLaunchStatus::Exited);
@@ -1914,8 +2533,27 @@ mod tests {
1914
2533
  detached: false,
1915
2534
  };
1916
2535
 
1917
- let outcome = execute_leader_plan(&plan, &workspace)
1918
- .expect("current pane binding must be present before provider argv runs");
2536
+ let authority = VerifiedAmbientPaneAuthority {
2537
+ pane_id: PaneId::new("%77"),
2538
+ observed: PaneInfo {
2539
+ pane_id: PaneId::new("%77"),
2540
+ session: SessionName::new("team-agent-leader-current"),
2541
+ window_index: Some(0),
2542
+ window_name: Some(WindowName::new("codex")),
2543
+ pane_index: Some(0),
2544
+ tty: Some("/dev/ttys077".to_string()),
2545
+ current_command: Some("codex".to_string()),
2546
+ current_path: Some(workspace.clone()),
2547
+ active: true,
2548
+ pane_pid: None,
2549
+ leader_env: BTreeMap::new(),
2550
+ },
2551
+ endpoint: "/private/tmp/tmux-501/default".to_string(),
2552
+ tmux: "/private/tmp/tmux-501/default,123,0".to_string(),
2553
+ };
2554
+ let outcome =
2555
+ execute_leader_plan_after_ambient_authority(&plan, &workspace, Some(&authority))
2556
+ .expect("current pane binding must be present before provider argv runs");
1919
2557
 
1920
2558
  assert_eq!(outcome.status, crate::leader::LeaderLaunchStatus::Exited);
1921
2559
  let state = crate::state::persist::load_runtime_state(&workspace).unwrap();