@team-agent/installer 0.5.84 → 0.5.86

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 (64) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/mod.rs +0 -1
  4. package/crates/team-agent/src/cli/profile.rs +4 -12
  5. package/crates/team-agent/src/cli/tests/verb_profile.rs +66 -0
  6. package/crates/team-agent/src/compiler/tests.rs +13 -24
  7. package/crates/team-agent/src/compiler.rs +1 -50
  8. package/crates/team-agent/src/leader/start.rs +0 -2
  9. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +0 -1
  10. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -1
  11. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +0 -6
  12. package/crates/team-agent/src/lifecycle/launch/identity.rs +0 -1
  13. package/crates/team-agent/src/lifecycle/launch/layout.rs +0 -1
  14. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +0 -1
  15. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +1 -89
  16. package/crates/team-agent/src/lifecycle/launch/ownership.rs +0 -1
  17. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +0 -3
  18. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +14 -24
  19. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +0 -1
  20. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -1
  21. package/crates/team-agent/src/lifecycle/launch/spawn.rs +4 -8
  22. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +0 -1
  23. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +0 -1
  24. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +0 -1
  25. package/crates/team-agent/src/lifecycle/launch.rs +0 -9
  26. package/crates/team-agent/src/lifecycle/profile_launch.rs +1 -1
  27. package/crates/team-agent/src/lifecycle/restart/common.rs +6 -9
  28. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +1 -3
  29. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +42 -46
  30. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +209 -52
  31. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +32 -77
  32. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -13
  33. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +0 -1
  34. package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +0 -2
  35. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +14 -85
  36. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -1
  37. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +0 -5
  38. package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +0 -1
  39. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +4 -4
  40. package/crates/team-agent/src/lifecycle/tests.rs +0 -1
  41. package/crates/team-agent/src/lifecycle/types.rs +0 -10
  42. package/crates/team-agent/src/lifecycle/worker_command_context.rs +4 -248
  43. package/crates/team-agent/src/messaging/results/collection_tests.rs +719 -0
  44. package/crates/team-agent/src/messaging/results.rs +104 -27
  45. package/crates/team-agent/src/model/enums.rs +0 -35
  46. package/crates/team-agent/src/model/mod.rs +0 -1
  47. package/crates/team-agent/src/model/spec.rs +23 -84
  48. package/crates/team-agent/src/provider/adapter.rs +48 -50
  49. package/crates/team-agent/src/provider/adapters/claude.rs +5 -30
  50. package/crates/team-agent/src/provider/adapters/codex.rs +5 -25
  51. package/crates/team-agent/src/provider/adapters/copilot.rs +9 -42
  52. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +4 -8
  53. package/crates/team-agent/src/provider/adapters/grok.rs +5 -58
  54. package/crates/team-agent/src/provider/adapters/mod.rs +5 -7
  55. package/crates/team-agent/src/provider/adapters/pi.rs +1 -56
  56. package/crates/team-agent/src/provider/bypass_flags.rs +1 -2
  57. package/crates/team-agent/src/provider/session/capture.rs +4 -4
  58. package/crates/team-agent/src/provider/tests/adapter.rs +7 -24
  59. package/crates/team-agent/src/provider/types.rs +1 -1
  60. package/crates/team-agent/src/state/persist.rs +15 -1
  61. package/package.json +4 -4
  62. package/schemas/team.schema.json +4 -9
  63. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +0 -784
  64. package/crates/team-agent/src/model/permissions.rs +0 -721
@@ -130,6 +130,12 @@ fn collect_scoped(
130
130
  owner_team_id: Option<&str>,
131
131
  ) -> Result<serde_json::Value, MessagingError> {
132
132
  let paths = collect_paths(workspace)?;
133
+ #[cfg(test)]
134
+ collection_tests::boundary("before_lock")?;
135
+ // Serialize collectors, but do not hold a SQLite write transaction while
136
+ // committing state. A process exit releases this existing OS file lock.
137
+ let collection_lock =
138
+ crate::state::persist::acquire_result_collection_lock(&paths.run_workspace)?;
133
139
  let log = EventLog::new(&paths.run_workspace);
134
140
  let resolved_owner_team_id = match owner_team_id.filter(|team| !team.is_empty()) {
135
141
  Some(team) => Some(resolve_owner_team_for_read(
@@ -159,7 +165,11 @@ fn collect_scoped(
159
165
  let store = MessageStore::open(&paths.run_workspace)?;
160
166
  let conn = crate::db::schema::open_db(store.db_path())?;
161
167
  if let Some(path) = result_file {
168
+ #[cfg(test)]
169
+ collection_tests::boundary("before_ingest")?;
162
170
  ingest_result_file(&conn, path, owner_team_id)?;
171
+ #[cfg(test)]
172
+ collection_tests::boundary("after_ingest")?;
163
173
  }
164
174
  let sql = match owner_team_id {
165
175
  Some(_) => {
@@ -197,7 +207,7 @@ fn collect_scoped(
197
207
  let mut collected_results = Vec::new();
198
208
  let mut invalid_results = Vec::new();
199
209
  let mut fatal_invalid_results = 0usize;
200
- let mut state_dirty = false;
210
+ let mut ready = Vec::new();
201
211
  let mut task_updates = Vec::new();
202
212
  for row in rows {
203
213
  let envelope: serde_json::Value = match serde_json::from_str(&row.envelope) {
@@ -242,25 +252,75 @@ fn collect_scoped(
242
252
  )?;
243
253
  continue;
244
254
  };
245
- match owner_team_id {
246
- Some(team) => {
247
- conn.execute(
248
- "update results set status = 'collected' where result_id = ?1 and owner_team_id = ?2",
249
- params![row.result_id.as_str(), team],
250
- )?;
251
- }
252
- None => {
253
- conn.execute(
254
- "update results set status = 'collected' where result_id = ?1",
255
- params![row.result_id.as_str()],
256
- )?;
257
- }
258
- }
259
255
  if scope == "task" {
260
- mark_task_done(&mut state, &row.task_id, &row.result_id);
261
256
  task_updates.push((row.task_id.clone(), row.result_id.clone()));
262
- state_dirty = true;
263
257
  }
258
+ ready.push((row, envelope, scope));
259
+ }
260
+ // Pending DB rows remain eligible until the complete task delta is durable.
261
+ // Preserve the original created_at/result_id order: its last row per task wins.
262
+ if !task_updates.is_empty() {
263
+ let mut task_winners = Vec::new();
264
+ let mut updated = std::collections::BTreeSet::new();
265
+ for (task_id, result_id) in task_updates.iter().rev() {
266
+ if updated.insert(task_id) {
267
+ task_winners.push((task_id.clone(), result_id.clone()));
268
+ }
269
+ }
270
+ #[cfg(test)]
271
+ collection_tests::boundary("before_state")?;
272
+ state = crate::state::repository::StateRepository::new(&paths.run_workspace).commit(
273
+ crate::state::repository::StateWriteIntent::ResultCollection { owner_team_id },
274
+ |latest| {
275
+ for (task_id, result_id) in &task_winners {
276
+ let already_applied = latest
277
+ .get("tasks")
278
+ .and_then(serde_json::Value::as_array)
279
+ .is_some_and(|tasks| {
280
+ tasks.iter().any(|task| {
281
+ task.get("id").and_then(serde_json::Value::as_str)
282
+ == Some(task_id.as_str())
283
+ && task.get("status").and_then(serde_json::Value::as_str)
284
+ == Some("done")
285
+ && task
286
+ .get("accepted_result_id")
287
+ .and_then(serde_json::Value::as_str)
288
+ == Some(result_id.as_str())
289
+ })
290
+ });
291
+ if !already_applied {
292
+ mark_task_done(latest, task_id, result_id);
293
+ }
294
+ }
295
+ },
296
+ )?;
297
+ for (task_id, result_id) in &task_winners {
298
+ let applied = state
299
+ .get("tasks")
300
+ .and_then(serde_json::Value::as_array)
301
+ .is_some_and(|tasks| {
302
+ tasks.iter().any(|task| {
303
+ task.get("id").and_then(serde_json::Value::as_str) == Some(task_id.as_str())
304
+ && task.get("status").and_then(serde_json::Value::as_str)
305
+ == Some("done")
306
+ && task
307
+ .get("accepted_result_id")
308
+ .and_then(serde_json::Value::as_str)
309
+ == Some(result_id.as_str())
310
+ })
311
+ });
312
+ if !applied {
313
+ return Err(MessagingError::Validation(format!(
314
+ "task result projection missing after commit: task_id={task_id} result_id={result_id}"
315
+ )));
316
+ }
317
+ }
318
+ #[cfg(test)]
319
+ collection_tests::boundary("after_state")?;
320
+ }
321
+ for (row, envelope, scope) in ready {
322
+ #[cfg(test)]
323
+ collection_tests::boundary("before_event")?;
264
324
  log.write(
265
325
  "collect.result",
266
326
  serde_json::json!({
@@ -270,6 +330,29 @@ fn collect_scoped(
270
330
  "scope": scope,
271
331
  }),
272
332
  )?;
333
+ #[cfg(test)]
334
+ collection_tests::boundary("after_event")?;
335
+ #[cfg(test)]
336
+ collection_tests::boundary("before_finalize")?;
337
+ let finalized = match owner_team_id {
338
+ Some(team) => {
339
+ conn.execute(
340
+ "update results set status = 'collected' where result_id = ?1 and owner_team_id = ?2 and status not in ('collected', 'invalid')",
341
+ params![row.result_id.as_str(), team],
342
+ )?
343
+ }
344
+ None => {
345
+ conn.execute(
346
+ "update results set status = 'collected' where result_id = ?1 and status not in ('collected', 'invalid')",
347
+ params![row.result_id.as_str()],
348
+ )?
349
+ }
350
+ };
351
+ #[cfg(test)]
352
+ collection_tests::boundary("after_finalize")?;
353
+ if finalized == 0 {
354
+ continue;
355
+ }
273
356
  collected.push(envelope.clone());
274
357
  let summary = serde_json::json!({
275
358
  "result_id": row.result_id,
@@ -286,17 +369,8 @@ fn collect_scoped(
286
369
  });
287
370
  collected_results.push(summary);
288
371
  }
289
- if state_dirty {
290
- state = crate::state::repository::StateRepository::new(&paths.run_workspace).commit(
291
- crate::state::repository::StateWriteIntent::ResultCollection { owner_team_id },
292
- |latest| {
293
- for (task_id, result_id) in &task_updates {
294
- mark_task_done(latest, task_id, result_id);
295
- }
296
- },
297
- )?;
298
- }
299
372
  let counts = result_counts(&conn, owner_team_id)?;
373
+ drop(collection_lock);
300
374
  // results.py:157 — ensure_coordinator=true runs the REAL ensure step; the
301
375
  // `{ok:false,status:"not_required"}` literal is ONLY the ensure=false branch.
302
376
  let coordinator = if ensure_coordinator {
@@ -1483,6 +1557,9 @@ pub fn collect_results_and_notify_watchers(
1483
1557
  }))
1484
1558
  }
1485
1559
 
1560
+ #[cfg(test)]
1561
+ mod collection_tests;
1562
+
1486
1563
  #[cfg(test)]
1487
1564
  mod tests {
1488
1565
  use super::{format_report_result_notification, report_result};
@@ -227,29 +227,6 @@ pub enum PermissionMode {
227
227
  Trusted,
228
228
  }
229
229
 
230
- /// 规范化 tool(`CANONICAL_TOOLS` `permissions.py:5-14`)。别名(`fs_*`/`@builtin`/`*`)
231
- /// 在 `expand_tools` 展开 —— 那是单独的 alias 输入枚举,不在此(step 2 后续)。
232
- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
233
- #[serde(rename_all = "snake_case")]
234
- pub enum Tool {
235
- FsRead,
236
- FsWrite,
237
- FsList,
238
- ExecuteBash,
239
- GitDiff,
240
- Network,
241
- McpTeam,
242
- ProviderBuiltin,
243
- }
244
-
245
- /// provider×tool enforcement(`PROVIDER_ENFORCEMENT` `permissions.py:44-65`)。
246
- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
247
- #[serde(rename_all = "snake_case")]
248
- pub enum Enforcement {
249
- Hard,
250
- PromptOnly,
251
- }
252
-
253
230
  /// tmux pane 存活态(`state.py:336-341`)。**`Unknown` 既不可当 dead 也不可当 live**
254
231
  /// (owner-gate `state.py:382` 用 `!= LIVE`)—— 穷尽 match,不 fallthrough(§11)。
255
232
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -345,18 +322,6 @@ mod tests {
345
322
  serde_json::to_string(&DisplayBackend::Adaptive).unwrap(),
346
323
  "\"adaptive\"",
347
324
  ),
348
- (
349
- serde_json::to_string(&Tool::ExecuteBash).unwrap(),
350
- "\"execute_bash\"",
351
- ),
352
- (
353
- serde_json::to_string(&Tool::ProviderBuiltin).unwrap(),
354
- "\"provider_builtin\"",
355
- ),
356
- (
357
- serde_json::to_string(&Enforcement::PromptOnly).unwrap(),
358
- "\"prompt_only\"",
359
- ),
360
325
  (
361
326
  serde_json::to_string(&PaneLiveness::Unknown).unwrap(),
362
327
  "\"unknown\"",
@@ -21,7 +21,6 @@ pub mod ids;
21
21
  pub(crate) mod name_similarity;
22
22
  pub mod pane_authority_refusal;
23
23
  pub mod paths;
24
- pub mod permissions;
25
24
  pub mod routing;
26
25
  pub mod spec;
27
26
  pub mod task_graph;
@@ -6,20 +6,18 @@
6
6
  //! serde_json `preserve_order` 保证 child 迭代序 == Python 插入序。
7
7
  //!
8
8
  //! 本文件先落地 `validate_result_envelope`(自包含,step 7/11 的门)。`validate_spec`
9
- //! 依赖 `expand_tools`(permissions)+ `find_dependency_cycle`(task_graph)+ yaml Value,
10
- //! 待那几个叶子模块集成后再加。
9
+ //! 依赖 `find_dependency_cycle`(task_graph)+ yaml Value。
11
10
 
12
11
  use std::path::Path;
13
12
 
14
13
  use serde_json::Value;
15
14
 
16
- use crate::model::enums::{Provider, TaskStatus};
15
+ use crate::model::enums::TaskStatus;
17
16
  use crate::model::errors::ModelError;
18
17
  use crate::model::ids::TaskId;
19
18
  use crate::model::task_graph::{find_dependency_cycle, TaskNode};
20
19
  use crate::model::yaml::Value as Yaml;
21
- use crate::model::{permissions, yaml};
22
- use crate::provider::adapters::pi::first_unsupported_pi_tool_category;
20
+ use crate::model::yaml;
23
21
  use crate::provider::wire::{is_claude_family, parse_canonical_provider};
24
22
 
25
23
  /// result_envelope_v1 顶层 required(= allowed)。
@@ -333,12 +331,13 @@ fn basic_schema_errors(spec: &Yaml) -> Vec<String> {
333
331
  if !matches!(mode, Some("supervisor_worker" | "swarm_limited")) {
334
332
  e.push("/team/mode: invalid mode".to_string());
335
333
  }
336
- let leader_keys = &["id", "role", "provider", "model", "tools", "context_policy"];
334
+ let leader_keys = &["id", "role", "provider", "model", "context_policy"];
335
+ let leader_allowed = &["id", "role", "provider", "model", "tools", "context_policy"];
337
336
  check_keys_y(
338
337
  spec.get("leader"),
339
338
  "/leader",
340
339
  leader_keys,
341
- leader_keys,
340
+ leader_allowed,
342
341
  &mut e,
343
342
  );
344
343
  let cp_keys = &[
@@ -407,7 +406,6 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
407
406
  "model",
408
407
  "working_directory",
409
408
  "system_prompt",
410
- "tools",
411
409
  "dangerously_skip_permissions",
412
410
  "preferred_for",
413
411
  "avoid_for",
@@ -483,7 +481,6 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
483
481
  &["inline", "file"],
484
482
  errors,
485
483
  );
486
- check_list_y(agent.get("tools"), &format!("{path}/tools"), errors);
487
484
  check_list_y(
488
485
  agent.get("preferred_for"),
489
486
  &format!("{path}/preferred_for"),
@@ -787,67 +784,6 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
787
784
  ));
788
785
  }
789
786
  }
790
- let tools: Vec<&str> = agent
791
- .get("tools")
792
- .and_then(Yaml::as_list)
793
- .unwrap_or(&[])
794
- .iter()
795
- .filter_map(Yaml::as_str)
796
- .collect();
797
- let expanded_tools = permissions::expand_tool_strings(tools);
798
- for tool in &expanded_tools {
799
- if !permissions::is_canonical_tool(tool) {
800
- e.push(format!(
801
- "/agents/{idx}/tools: unknown tool {}",
802
- py_repr_str(tool)
803
- ));
804
- }
805
- }
806
- if parse_canonical_provider(provider.and_then(Yaml::as_str).unwrap_or(""))
807
- == Some(Provider::Pi)
808
- {
809
- if let Some(category) = first_unsupported_pi_tool_category(
810
- expanded_tools
811
- .iter()
812
- .filter(|tool| permissions::is_canonical_tool(tool))
813
- .map(String::as_str),
814
- ) {
815
- e.push(format!(
816
- "/agents/{idx}/tools: Pi does not support Team Agent tool category {category:?}; remove it from this agent's tools"
817
- ));
818
- }
819
- }
820
- }
821
-
822
- let leader_tools: Vec<&str> = leader
823
- .and_then(|l| l.get("tools"))
824
- .and_then(Yaml::as_list)
825
- .unwrap_or(&[])
826
- .iter()
827
- .filter_map(Yaml::as_str)
828
- .collect();
829
- let expanded_leader_tools = permissions::expand_tool_strings(leader_tools);
830
- for tool in &expanded_leader_tools {
831
- if !permissions::is_canonical_tool(tool) {
832
- e.push(format!(
833
- "/leader/tools: unknown tool {}",
834
- py_repr_str(tool)
835
- ));
836
- }
837
- }
838
- if parse_canonical_provider(leader_provider.and_then(Yaml::as_str).unwrap_or(""))
839
- == Some(Provider::Pi)
840
- {
841
- if let Some(category) = first_unsupported_pi_tool_category(
842
- expanded_leader_tools
843
- .iter()
844
- .filter(|tool| permissions::is_canonical_tool(tool))
845
- .map(String::as_str),
846
- ) {
847
- e.push(format!(
848
- "/leader/tools: Pi does not support Team Agent tool category {category:?}; remove it from the leader's tools"
849
- ));
850
- }
851
787
  }
852
788
 
853
789
  let routing = spec.get("routing");
@@ -1070,7 +1006,7 @@ mod tests {
1070
1006
  }
1071
1007
 
1072
1008
  #[test]
1073
- fn pi_spec_accepts_native_categories_and_rejects_provider_builtin() {
1009
+ fn tools_are_transparent_for_all_providers() {
1074
1010
  let pi_text = include_str!("testdata/team.spec.yaml")
1075
1011
  .lines()
1076
1012
  .filter(|line| {
@@ -1093,22 +1029,26 @@ mod tests {
1093
1029
  1,
1094
1030
  );
1095
1031
  let bad_agent = yaml::loads(&bad_agent_text).unwrap();
1096
- let error = validate_spec(&bad_agent, Path::new(TD)).unwrap_err().to_string();
1097
1032
  assert!(
1098
- error.contains("/agents/0/tools: Pi does not support Team Agent tool category \"provider_builtin\""),
1099
- "spec validation must reject the Pi agent category: {error}"
1033
+ validate_spec(&bad_agent, Path::new(TD)).is_ok(),
1034
+ "Pi tools metadata must be ignored"
1100
1035
  );
1101
1036
 
1102
- let bad_leader_text = pi_text.replacen(
1103
- " - mcp_team\n",
1104
- " - mcp_team\n - provider_builtin\n",
1105
- 1,
1106
- );
1107
- let bad_leader = yaml::loads(&bad_leader_text).unwrap();
1108
- let error = validate_spec(&bad_leader, Path::new(TD)).unwrap_err().to_string();
1037
+ let missing_tools = pi_text
1038
+ .replacen(
1039
+ " tools:\n - fs_read\n - fs_list\n - mcp_team\n",
1040
+ "",
1041
+ 1,
1042
+ )
1043
+ .replacen(
1044
+ " tools:\n - fs_read\n - fs_write\n - fs_list\n - execute_bash\n - mcp_team\n",
1045
+ "",
1046
+ 1,
1047
+ );
1048
+ let missing_tools = yaml::loads(&missing_tools).unwrap();
1109
1049
  assert!(
1110
- error.contains("/leader/tools: Pi does not support Team Agent tool category \"provider_builtin\""),
1111
- "spec validation must reject the Pi leader category: {error}"
1050
+ validate_spec(&missing_tools, Path::new(TD)).is_ok(),
1051
+ "omitted tools metadata must be accepted"
1112
1052
  );
1113
1053
  }
1114
1054
 
@@ -1121,7 +1061,6 @@ mod tests {
1121
1061
  "/version: must equal 1",
1122
1062
  "/team/mode: invalid mode",
1123
1063
  "/leader/provider: unknown provider 'badprov'",
1124
- "/agents/0/tools: unknown tool 'banana'",
1125
1064
  ]
1126
1065
  );
1127
1066
  }