@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +44 -1
- package/crates/team-agent/src/cli/diagnose.rs +183 -0
- package/crates/team-agent/src/cli/emit.rs +63 -14
- package/crates/team-agent/src/cli/leader.rs +8 -1
- package/crates/team-agent/src/cli/mod.rs +263 -5
- package/crates/team-agent/src/cli/named_address.rs +18 -1
- package/crates/team-agent/src/cli/send/presentation.rs +2 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/status_port/store.rs +7 -3
- package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
- package/crates/team-agent/src/cli/types.rs +33 -0
- package/crates/team-agent/src/communication_mode/mod.rs +53 -0
- package/crates/team-agent/src/compiler/tests.rs +5 -5
- package/crates/team-agent/src/compiler.rs +53 -1
- package/crates/team-agent/src/coordinator/tick.rs +21 -8
- package/crates/team-agent/src/db/message_store.rs +45 -1
- package/crates/team-agent/src/fake_worker.rs +21 -1
- package/crates/team-agent/src/leader/start.rs +732 -94
- package/crates/team-agent/src/leader/tests/identity.rs +64 -57
- package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +58 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +57 -51
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +148 -8
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +5 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +35 -1
- package/crates/team-agent/src/lifecycle/types.rs +1 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
- package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
- package/crates/team-agent/src/mcp_server/tools.rs +100 -66
- package/crates/team-agent/src/mcp_server/types.rs +5 -0
- package/crates/team-agent/src/mcp_server/wire.rs +27 -21
- package/crates/team-agent/src/messaging/delivery.rs +105 -38
- package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
- package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
- package/crates/team-agent/src/messaging/presentation.rs +109 -0
- package/crates/team-agent/src/messaging/results.rs +165 -17
- package/crates/team-agent/src/messaging/send.rs +94 -81
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
- package/crates/team-agent/src/messaging/types.rs +5 -0
- package/crates/team-agent/src/messaging/watchers.rs +5 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
- package/crates/team-agent/src/model/spec.rs +16 -0
- package/crates/team-agent/src/provider/adapter.rs +57 -0
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +10 -2
- package/crates/team-agent/src/provider/session/capture.rs +69 -34
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +386 -9
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +3 -14
- package/crates/team-agent/src/provider/session/context_fork.rs +7 -2
- package/crates/team-agent/src/provider/session/mod.rs +3 -2
- package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
- package/crates/team-agent/src/provider/session_scan/codex.rs +28 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +34 -8
- package/crates/team-agent/src/provider/session_scan.rs +8 -0
- package/crates/team-agent/src/topology.rs +3 -0
- package/package.json +4 -4
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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":
|
|
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 =
|
|
830
|
-
|
|
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 =
|
|
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(
|
|
863
|
-
|
|
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
|
-
"
|
|
866
|
-
serde_json::json!(
|
|
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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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":
|
|
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
|
|
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
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
.
|
|
1003
|
-
.
|
|
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
|
-
|
|
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 =
|
|
1035
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
1084
|
-
serde_json::json!(
|
|
1692
|
+
"window_name".to_string(),
|
|
1693
|
+
serde_json::json!(window_name.as_str()),
|
|
1085
1694
|
);
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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":
|
|
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
|
-
|
|
1142
|
-
|
|
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
|
-
.
|
|
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,
|
|
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 =
|
|
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
|
|
1918
|
-
|
|
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();
|