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