@team-agent/installer 0.5.52 → 0.5.53

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 (62) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +18 -2
  4. package/crates/team-agent/src/cli/emit.rs +17 -0
  5. package/crates/team-agent/src/cli/mod.rs +155 -4
  6. package/crates/team-agent/src/cli/spec.rs +3 -0
  7. package/crates/team-agent/src/cli/types.rs +11 -0
  8. package/crates/team-agent/src/diagnose/orphans.rs +24 -3
  9. package/crates/team-agent/src/kill_audit.rs +67 -0
  10. package/crates/team-agent/src/leader/lease.rs +324 -57
  11. package/crates/team-agent/src/leader/rediscover/tests.rs +3 -0
  12. package/crates/team-agent/src/leader/rediscover.rs +6 -0
  13. package/crates/team-agent/src/leader/start.rs +144 -23
  14. package/crates/team-agent/src/leader/tests/idle.rs +3 -0
  15. package/crates/team-agent/src/leader/types.rs +6 -0
  16. package/crates/team-agent/src/lib.rs +1 -0
  17. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
  18. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +5 -0
  19. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +4 -0
  20. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +106 -0
  21. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +216 -208
  22. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +36 -0
  23. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +238 -0
  24. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +101 -5
  25. package/crates/team-agent/src/lifecycle/launch/role_source.rs +170 -0
  26. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
  27. package/crates/team-agent/src/lifecycle/launch.rs +14 -2
  28. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
  29. package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
  30. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -3
  31. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +110 -12
  32. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +8 -2
  33. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +1 -0
  34. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +3 -1
  35. package/crates/team-agent/src/lifecycle/types.rs +17 -0
  36. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +40 -5
  37. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -1
  38. package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
  39. package/crates/team-agent/src/mcp_server/tools.rs +18 -4
  40. package/crates/team-agent/src/mcp_server/types.rs +3 -0
  41. package/crates/team-agent/src/mcp_server/wire.rs +30 -7
  42. package/crates/team-agent/src/messaging/leader_channel.rs +32 -5
  43. package/crates/team-agent/src/messaging/leader_receiver.rs +68 -21
  44. package/crates/team-agent/src/messaging/tests/runtime.rs +34 -23
  45. package/crates/team-agent/src/provider/adapter.rs +54 -13
  46. package/crates/team-agent/src/provider/adapters/claude_fork.rs +122 -0
  47. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +306 -0
  48. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  49. package/crates/team-agent/src/provider/session/capture.rs +321 -52
  50. package/crates/team-agent/src/provider/session/context_fork.rs +499 -0
  51. package/crates/team-agent/src/provider/session/mod.rs +6 -0
  52. package/crates/team-agent/src/provider/session_scan/claude.rs +1 -1
  53. package/crates/team-agent/src/provider/session_scan/codex.rs +144 -27
  54. package/crates/team-agent/src/provider/session_scan/common.rs +39 -6
  55. package/crates/team-agent/src/provider/session_scan/copilot.rs +47 -3
  56. package/crates/team-agent/src/provider/session_scan.rs +3 -3
  57. package/crates/team-agent/src/provider/tests/copilot_fork.rs +191 -0
  58. package/crates/team-agent/src/provider/tests.rs +1 -0
  59. package/crates/team-agent/src/tmux_backend/tests.rs +51 -16
  60. package/crates/team-agent/src/tmux_backend.rs +29 -2
  61. package/package.json +4 -4
  62. package/skills/team-agent/SKILL.md +8 -3
@@ -1,7 +1,7 @@
1
1
  //! leader::start — leader_start_plan / start_leader / leader_session_name(派生 tmux session 名)。
2
2
 
3
3
  use std::collections::BTreeMap;
4
- use std::io::IsTerminal;
4
+ use std::io::{IsTerminal, Read, Write};
5
5
  use std::path::{Path, PathBuf};
6
6
  use std::process::{Command, Stdio};
7
7
 
@@ -243,14 +243,10 @@ pub fn execute_leader_plan(
243
243
  } else if plan.is_external_leader {
244
244
  persist_external_leader_topology_marker(plan, workspace)?;
245
245
  }
246
- let status = run_leader_argv(&argv, &plan.leader_env, plan, workspace)?;
247
- let code = status.code();
248
- if !status.success() {
249
- return Err(LeaderError::Start(format!(
250
- "leader launcher exited with status {}",
251
- code.map(|c| c.to_string())
252
- .unwrap_or_else(|| "signal".to_string())
253
- )));
246
+ let process = run_leader_argv(&argv, &plan.leader_env, plan, workspace)?;
247
+ let code = process.status.code();
248
+ if !process.status.success() {
249
+ return Err(LeaderError::Start(leader_launcher_failure(&process)));
254
250
  }
255
251
  if detached {
256
252
  Ok(LeaderLaunchOutcome {
@@ -471,14 +467,10 @@ fn execute_managed_leader_plan(
471
467
  workspace.to_path_buf(),
472
468
  spawned.pane_id.as_str().to_string(),
473
469
  );
474
- let status = run_leader_argv(&plan.argv, &BTreeMap::new(), plan, workspace)?;
475
- let code = status.code();
476
- if !status.success() {
477
- return Err(LeaderError::Start(format!(
478
- "leader launcher exited with status {}",
479
- code.map(|c| c.to_string())
480
- .unwrap_or_else(|| "signal".to_string())
481
- )));
470
+ let process = run_leader_argv(&plan.argv, &BTreeMap::new(), plan, workspace)?;
471
+ let code = process.status.code();
472
+ if !process.status.success() {
473
+ return Err(LeaderError::Start(leader_launcher_failure(&process)));
482
474
  }
483
475
  ensure_managed_provider_live_after_attach(&transport, &spawned)?;
484
476
  Ok(LeaderLaunchOutcome {
@@ -1213,7 +1205,7 @@ fn run_leader_argv(
1213
1205
  env: &BTreeMap<String, String>,
1214
1206
  plan: &LeaderStartPlan,
1215
1207
  workspace: &Path,
1216
- ) -> Result<std::process::ExitStatus, LeaderError> {
1208
+ ) -> Result<LeaderProcessExit, LeaderError> {
1217
1209
  let Some(program) = argv.first() else {
1218
1210
  return Err(LeaderError::Start(
1219
1211
  "leader launch argv is empty".to_string(),
@@ -1234,8 +1226,13 @@ fn run_leader_argv(
1234
1226
  command
1235
1227
  .args(argv.iter().skip(1))
1236
1228
  .stdin(Stdio::inherit())
1237
- .stdout(Stdio::inherit())
1238
- .stderr(Stdio::inherit());
1229
+ .stdout(Stdio::inherit());
1230
+ let capture_stderr = plan.mode != LeaderStartMode::ExecProvider;
1231
+ command.stderr(if capture_stderr {
1232
+ Stdio::piped()
1233
+ } else {
1234
+ Stdio::inherit()
1235
+ });
1239
1236
  // 0.4.x order fix: env_remove MUST run AFTER envs(). The `env` map comes
1240
1237
  // from `merged_exec_env` which seeds with `std::env::vars().collect()` —
1241
1238
  // calling `.envs(env)` re-adds every inherited CLAUDE_CODE_* the launching
@@ -1248,10 +1245,109 @@ fn run_leader_argv(
1248
1245
  command.env_remove(key);
1249
1246
  }
1250
1247
  let mut child = command.spawn()?;
1248
+ let stderr_reader = child.stderr.take().map(|mut child_stderr| {
1249
+ std::thread::spawn(move || {
1250
+ let mut captured = Vec::new();
1251
+ let mut buf = [0_u8; 4096];
1252
+ loop {
1253
+ let Ok(read) = child_stderr.read(&mut buf) else {
1254
+ break;
1255
+ };
1256
+ if read == 0 {
1257
+ break;
1258
+ }
1259
+ let _ = std::io::stderr().write_all(&buf[..read]);
1260
+ push_bounded_stderr(&mut captured, &buf[..read]);
1261
+ }
1262
+ captured
1263
+ })
1264
+ });
1251
1265
  if plan.mode == LeaderStartMode::ExecProvider {
1252
1266
  spawn_exec_provider_startup_prompt_handler(plan.provider, workspace.to_path_buf());
1253
1267
  }
1254
- child.wait().map_err(LeaderError::Io)
1268
+ let status = child.wait().map_err(LeaderError::Io)?;
1269
+ let stderr = match stderr_reader {
1270
+ Some(reader) => match reader.join() {
1271
+ Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
1272
+ Err(_) => String::new(),
1273
+ },
1274
+ None => String::new(),
1275
+ };
1276
+ Ok(LeaderProcessExit { status, stderr })
1277
+ }
1278
+
1279
+ const LEADER_STDERR_LIMIT: usize = 8192;
1280
+
1281
+ struct LeaderProcessExit {
1282
+ status: std::process::ExitStatus,
1283
+ stderr: String,
1284
+ }
1285
+
1286
+ fn push_bounded_stderr(captured: &mut Vec<u8>, chunk: &[u8]) {
1287
+ if chunk.len() >= LEADER_STDERR_LIMIT {
1288
+ captured.clear();
1289
+ captured.extend_from_slice(&chunk[chunk.len() - LEADER_STDERR_LIMIT..]);
1290
+ return;
1291
+ }
1292
+ let excess = captured
1293
+ .len()
1294
+ .saturating_add(chunk.len())
1295
+ .saturating_sub(LEADER_STDERR_LIMIT);
1296
+ if excess > 0 {
1297
+ captured.drain(..excess);
1298
+ }
1299
+ captured.extend_from_slice(chunk);
1300
+ }
1301
+
1302
+ fn leader_launcher_failure(process: &LeaderProcessExit) -> String {
1303
+ let status = process
1304
+ .status
1305
+ .code()
1306
+ .map(|code| code.to_string())
1307
+ .unwrap_or_else(|| "signal".to_string());
1308
+ format_launcher_failure(&status, process.status.code().is_none(), &process.stderr)
1309
+ }
1310
+
1311
+ fn format_launcher_failure(status: &str, signaled: bool, raw_stderr: &str) -> String {
1312
+ let stderr_class = classify_launcher_stderr(raw_stderr, signaled);
1313
+ let stderr = bounded_stderr_text(crate::redaction::redact_external_text(raw_stderr.trim()));
1314
+ let stderr = if stderr.is_empty() {
1315
+ "<empty>".to_string()
1316
+ } else {
1317
+ stderr
1318
+ };
1319
+ format!(
1320
+ "leader launcher exited with status {status}; stderr_class={stderr_class}; stderr_excerpt={stderr}"
1321
+ )
1322
+ }
1323
+
1324
+ fn bounded_stderr_text(stderr: String) -> String {
1325
+ if stderr.len() <= LEADER_STDERR_LIMIT {
1326
+ return stderr;
1327
+ }
1328
+ let mut start = stderr.len() - LEADER_STDERR_LIMIT;
1329
+ while !stderr.is_char_boundary(start) {
1330
+ start += 1;
1331
+ }
1332
+ stderr[start..].to_string()
1333
+ }
1334
+
1335
+ fn classify_launcher_stderr(stderr: &str, signaled: bool) -> &'static str {
1336
+ let stderr = stderr.to_ascii_lowercase();
1337
+ if stderr.contains("server exited") {
1338
+ "server_exited"
1339
+ } else if stderr.contains("no server running") || stderr.contains("failed to connect to server")
1340
+ {
1341
+ "no_server"
1342
+ } else if stderr.contains("not a terminal") {
1343
+ "not_a_terminal"
1344
+ } else if stderr.contains("can't find session") || stderr.contains("no sessions") {
1345
+ "target_missing"
1346
+ } else if signaled {
1347
+ "signal"
1348
+ } else {
1349
+ "non_zero"
1350
+ }
1255
1351
  }
1256
1352
 
1257
1353
  fn spawn_exec_provider_startup_prompt_handler(provider: Provider, workspace: PathBuf) {
@@ -1447,8 +1543,9 @@ mod tests {
1447
1543
  };
1448
1544
 
1449
1545
  use super::{
1450
- ensure_managed_provider_live_after_attach, execute_leader_plan,
1451
- handle_exec_provider_startup_prompts, shlex_quote,
1546
+ ensure_managed_provider_live_after_attach, execute_leader_plan, format_launcher_failure,
1547
+ handle_exec_provider_startup_prompts, push_bounded_stderr, shlex_quote,
1548
+ LEADER_STDERR_LIMIT,
1452
1549
  };
1453
1550
 
1454
1551
  struct ScriptedTransport {
@@ -1981,4 +2078,28 @@ mod tests {
1981
2078
  external/attach paths; body excerpt: {body}"
1982
2079
  );
1983
2080
  }
2081
+
2082
+ #[test]
2083
+ fn launcher_failure_redacts_credentials_before_persistence() {
2084
+ let failure = format_launcher_failure(
2085
+ "1",
2086
+ false,
2087
+ "open terminal failed: not a terminal API_TOKEN=secret-value \
2088
+ Authorization: Bearer abcdefghijklmnopqrstuvwxyz",
2089
+ );
2090
+
2091
+ assert!(failure.contains("stderr_class=not_a_terminal"));
2092
+ assert!(failure.contains("[REDACTED]"));
2093
+ assert!(!failure.contains("secret-value"));
2094
+ assert!(!failure.contains("abcdefghijklmnopqrstuvwxyz"));
2095
+ }
2096
+
2097
+ #[test]
2098
+ fn launcher_stderr_capture_is_bounded_to_tail() {
2099
+ let mut captured = vec![b'a'; LEADER_STDERR_LIMIT - 2];
2100
+ push_bounded_stderr(&mut captured, b"XYZ");
2101
+
2102
+ assert_eq!(captured.len(), LEADER_STDERR_LIMIT);
2103
+ assert!(captured.ends_with(b"XYZ"));
2104
+ }
1984
2105
  }
@@ -226,6 +226,9 @@ fn leader_receiver_struct_serializes_with_python_field_names() {
226
226
  pane_tty: Some("/dev/ttys001".into()),
227
227
  pane_current_command: Some("claude".into()),
228
228
  tmux_socket: None,
229
+ scope_authority: None,
230
+ authorized_team_workspace: None,
231
+ binding_nonce: None,
229
232
  fingerprint: Some("fp".into()),
230
233
  leader_session_uuid: Some(uuid("fp", "/ws", "u", "default")),
231
234
  owner_epoch: Some(OwnerEpoch(3)),
@@ -263,6 +263,12 @@ pub struct LeaderReceiver {
263
263
  pub pane_current_command: Option<String>,
264
264
  #[serde(skip_serializing_if = "Option::is_none")]
265
265
  pub tmux_socket: Option<String>,
266
+ #[serde(skip_serializing_if = "Option::is_none")]
267
+ pub scope_authority: Option<String>,
268
+ #[serde(skip_serializing_if = "Option::is_none")]
269
+ pub authorized_team_workspace: Option<PathBuf>,
270
+ #[serde(skip_serializing_if = "Option::is_none")]
271
+ pub binding_nonce: Option<String>,
266
272
  /// `_target_fingerprint(pane_info)`。
267
273
  #[serde(skip_serializing_if = "Option::is_none")]
268
274
  pub fingerprint: Option<String>,
@@ -42,6 +42,7 @@ pub mod db;
42
42
 
43
43
  // step 4 (event_log) — events.jsonl 唯一审计流(append/tail/rotation + 稳定事件名)。
44
44
  pub mod event_log;
45
+ pub(crate) mod kill_audit;
45
46
 
46
47
  // step 5 (state) — state.json 持久化/identity/owner-gate/projection(bug-084 韧性)。
47
48
  pub mod state;
@@ -376,7 +376,7 @@ pub(super) fn add_agent_with_transport_at_paths(
376
376
  }
377
377
  let started = match crate::lifecycle::restart::start_agent_at_paths(
378
378
  run_workspace,
379
- team_dir,
379
+ spec_path.parent().unwrap_or(team_dir),
380
380
  agent_id,
381
381
  false,
382
382
  open_display,
@@ -28,6 +28,11 @@ pub(super) fn rollback_add_agent_atomic(
28
28
  agent_id: &AgentId,
29
29
  reason: &str,
30
30
  ) {
31
+ let _ = std::fs::remove_dir_all(
32
+ run_workspace
33
+ .join(".team/runtime/copilot-instructions")
34
+ .join(agent_id.as_str()),
35
+ );
31
36
  let spec_restored = if let Some(text) = pre_spec_text {
32
37
  std::fs::write(spec_path, text).is_ok()
33
38
  } else {
@@ -93,6 +93,10 @@ pub(super) fn running_agent_state(
93
93
  "attribution_confidence".to_string(),
94
94
  serde_json::Value::Null,
95
95
  );
96
+ state.insert(
97
+ "capture_state".to_string(),
98
+ serde_json::json!("pending_first_turn"),
99
+ );
96
100
  if let Some(started_agent) = started_agent {
97
101
  persist_started_agent_plan_state(&mut state, started_agent);
98
102
  if let Some(layout_window) = started_agent.layout_window.as_ref() {
@@ -0,0 +1,106 @@
1
+ use crate::lifecycle::*;
2
+ use crate::model::ids::AgentId;
3
+ use crate::provider::SessionId;
4
+ use std::path::Path;
5
+
6
+ use super::*;
7
+
8
+ pub fn clone_agent(
9
+ workspace: &Path,
10
+ source_agent_id: &AgentId,
11
+ as_agent_id: &AgentId,
12
+ label: Option<&str>,
13
+ open_display: bool,
14
+ team: Option<&str>,
15
+ ) -> Result<CloneAgentReport, LifecycleError> {
16
+ let selected = crate::state::selector::resolve_active_team(
17
+ workspace,
18
+ team,
19
+ crate::state::selector::SelectorMode::RequireSpec,
20
+ )
21
+ .map_err(|error| LifecycleError::TeamSelect(error.to_string()))?;
22
+ ensure_owner_allowed_for_state(&selected.state, Some(source_agent_id))?;
23
+ let source_agent = selected
24
+ .state
25
+ .get("agents")
26
+ .and_then(|agents| agents.get(source_agent_id.as_str()))
27
+ .ok_or_else(|| {
28
+ LifecycleError::RequirementUnmet(format!("unknown worker agent id: {source_agent_id}"))
29
+ })?;
30
+ let source_session_id = source_agent
31
+ .get("session_id")
32
+ .and_then(serde_json::Value::as_str)
33
+ .filter(|value| !value.is_empty())
34
+ .map(SessionId::new);
35
+ let mut materialized = materialize_latest_role(
36
+ &selected.run_workspace,
37
+ &selected.team_dir,
38
+ &selected.state,
39
+ source_agent_id,
40
+ as_agent_id,
41
+ label,
42
+ )?;
43
+ let spec_path = selected
44
+ .spec_path
45
+ .as_ref()
46
+ .ok_or_else(|| LifecycleError::TeamSelect("active team spec not found".to_string()))?;
47
+ let spec = crate::model::yaml::loads(
48
+ &std::fs::read_to_string(spec_path)
49
+ .map_err(|error| LifecycleError::Compile(error.to_string()))?,
50
+ )
51
+ .map_err(|error| LifecycleError::Compile(error.to_string()))?;
52
+ clamp_materialized_role_to_leader(materialized.path(), &spec)?;
53
+ let added = add_agent(
54
+ &selected.run_workspace,
55
+ as_agent_id,
56
+ materialized.path(),
57
+ open_display,
58
+ Some(selected.team_key.as_str()),
59
+ )?;
60
+ let verified = read_agent_session(
61
+ &selected.run_workspace,
62
+ selected.team_key.as_str(),
63
+ as_agent_id,
64
+ source_session_id.as_ref(),
65
+ );
66
+ let (session_id, backing_path, backing_state) = match verified {
67
+ Some((session_id, backing_path)) => (
68
+ Some(session_id),
69
+ Some(backing_path),
70
+ CloneBackingState::Verified,
71
+ ),
72
+ None => (None, None, CloneBackingState::PendingFirstTurn),
73
+ };
74
+ materialized.keep();
75
+ Ok(CloneAgentReport {
76
+ source_agent_id: source_agent_id.clone(),
77
+ new_agent_id: as_agent_id.clone(),
78
+ env: added.env,
79
+ session_id,
80
+ backing_path,
81
+ backing_state,
82
+ })
83
+ }
84
+
85
+ fn read_agent_session(
86
+ workspace: &Path,
87
+ team_key: &str,
88
+ agent_id: &AgentId,
89
+ source_session_id: Option<&SessionId>,
90
+ ) -> Option<(SessionId, std::path::PathBuf)> {
91
+ let state = crate::state::projection::select_runtime_state(workspace, Some(team_key)).ok()?;
92
+ let agent = state
93
+ .get("agents")
94
+ .and_then(|agents| agents.get(agent_id.as_str()))?;
95
+ let session = agent
96
+ .get("session_id")
97
+ .and_then(serde_json::Value::as_str)
98
+ .filter(|value| !value.is_empty())?;
99
+ let backing = agent
100
+ .get("rollout_path")
101
+ .and_then(serde_json::Value::as_str)
102
+ .filter(|value| !value.is_empty())
103
+ .map(std::path::PathBuf::from)?;
104
+ let distinct = source_session_id.is_none_or(|source| source.as_str() != session);
105
+ (distinct && backing.is_file()).then(|| (SessionId::new(session), backing))
106
+ }