@team-agent/installer 0.5.85 → 0.5.89

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 (63) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +2 -2
  4. package/crates/team-agent/src/cli/emit.rs +56 -12
  5. package/crates/team-agent/src/cli/mod.rs +0 -1
  6. package/crates/team-agent/src/cli/status_port/brief.rs +187 -7
  7. package/crates/team-agent/src/compiler/tests.rs +13 -24
  8. package/crates/team-agent/src/compiler.rs +5 -57
  9. package/crates/team-agent/src/leader/start.rs +0 -2
  10. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +0 -1
  11. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -1
  12. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +0 -6
  13. package/crates/team-agent/src/lifecycle/launch/identity.rs +39 -22
  14. package/crates/team-agent/src/lifecycle/launch/layout.rs +0 -1
  15. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +0 -1
  16. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +0 -58
  17. package/crates/team-agent/src/lifecycle/launch/ownership.rs +0 -1
  18. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +0 -3
  19. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +14 -24
  20. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +0 -1
  21. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -1
  22. package/crates/team-agent/src/lifecycle/launch/spawn.rs +5 -9
  23. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +0 -1
  24. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +0 -1
  25. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +0 -1
  26. package/crates/team-agent/src/lifecycle/launch.rs +0 -9
  27. package/crates/team-agent/src/lifecycle/restart/common.rs +5 -9
  28. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +3 -3
  29. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +1 -3
  30. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +42 -46
  31. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +30 -44
  32. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +32 -77
  33. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -13
  34. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +0 -1
  35. package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +0 -2
  36. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +14 -85
  37. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -1
  38. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +0 -5
  39. package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +0 -1
  40. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +4 -4
  41. package/crates/team-agent/src/lifecycle/tests.rs +0 -1
  42. package/crates/team-agent/src/lifecycle/types.rs +0 -10
  43. package/crates/team-agent/src/lifecycle/worker_command_context.rs +4 -248
  44. package/crates/team-agent/src/model/enums.rs +17 -38
  45. package/crates/team-agent/src/model/mod.rs +0 -1
  46. package/crates/team-agent/src/model/spec.rs +31 -91
  47. package/crates/team-agent/src/provider/adapter.rs +48 -50
  48. package/crates/team-agent/src/provider/adapters/claude.rs +5 -30
  49. package/crates/team-agent/src/provider/adapters/codex.rs +5 -25
  50. package/crates/team-agent/src/provider/adapters/copilot.rs +8 -41
  51. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +4 -8
  52. package/crates/team-agent/src/provider/adapters/grok.rs +5 -58
  53. package/crates/team-agent/src/provider/adapters/mod.rs +5 -7
  54. package/crates/team-agent/src/provider/adapters/pi.rs +1 -56
  55. package/crates/team-agent/src/provider/bypass_flags.rs +1 -2
  56. package/crates/team-agent/src/provider/session/capture.rs +4 -4
  57. package/crates/team-agent/src/provider/tests/adapter.rs +7 -24
  58. package/crates/team-agent/src/provider/types.rs +1 -1
  59. package/crates/team-agent/src/provider/wire.rs +1 -1
  60. package/package.json +4 -4
  61. package/schemas/team.schema.json +4 -9
  62. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +0 -784
  63. package/crates/team-agent/src/model/permissions.rs +0 -721
@@ -1,17 +1,13 @@
1
1
  //! ---
2
- //! purpose: 从 spec 里的一个 agent 定义,编译出 worker 启动所需的系统提示词与工具串
2
+ //! purpose: 从 spec 里的一个 agent 定义,编译出 worker 启动所需的系统提示词
3
3
  //! contract:
4
4
  //! provides:
5
5
  //! - name: WorkerCommandAgent
6
6
  //! what: 从 YAML 或 JSON 读出的单 agent 命令上下文
7
7
  //! - name: compile_worker_system_prompt
8
8
  //! what: 按身份、runtime 契约、通信模式、角色正文、输出契约、权限说明拼系统提示词
9
- //! - name: resolved_tool_strings_for_command
10
- //! what: 解析出该 agent 的工具串,声明了 bypass 时追加 dangerous_auto_approve 哨兵
11
9
  //! depends:
12
- //! - crate::model::permissions
13
10
  //! - crate::communication_mode
14
- //! - crate::provider::bypass_flags
15
11
  //! boundary:
16
12
  //! - 只产出字符串,不 spawn 进程、不写盘
17
13
  //! - bypass 只认 agent 自身声明,不从 team/runtime/leader argv 继承
@@ -22,9 +18,7 @@ use std::path::Path;
22
18
 
23
19
  use crate::communication_mode::CommunicationMode;
24
20
  use crate::lifecycle::types::LifecycleError;
25
- use crate::model::enums::{Enforcement, Provider};
26
- use crate::model::ids::AgentId;
27
- use crate::model::permissions::{resolve_permissions, AgentPermissionInput};
21
+ use crate::model::enums::Provider;
28
22
 
29
23
  const RUNTIME_CONTRACT_SECTION: &str = r#"# Team Agent Teammate Runtime Contract
30
24
 
@@ -57,7 +51,6 @@ pub(crate) struct WorkerCommandAgent {
57
51
  id: Option<String>,
58
52
  provider: Provider,
59
53
  role: Option<String>,
60
- declared_tools: Option<Vec<String>>,
61
54
  system_prompt_inline: Option<String>,
62
55
  system_prompt_file: Option<String>,
63
56
  output_contract_format: Option<String>,
@@ -94,16 +87,6 @@ impl WorkerCommandAgent {
94
87
  .get("role")
95
88
  .and_then(crate::model::yaml::Value::as_str)
96
89
  .map(str::to_string),
97
- declared_tools: agent
98
- .get("tools")
99
- .and_then(crate::model::yaml::Value::as_list)
100
- .map(|items| {
101
- items
102
- .iter()
103
- .filter_map(crate::model::yaml::Value::as_str)
104
- .map(str::to_string)
105
- .collect()
106
- }),
107
90
  system_prompt_inline: system_prompt
108
91
  .and_then(|prompt| prompt.get("inline"))
109
92
  .and_then(crate::model::yaml::Value::as_str)
@@ -158,16 +141,6 @@ impl WorkerCommandAgent {
158
141
  .get("role")
159
142
  .and_then(serde_json::Value::as_str)
160
143
  .map(str::to_string),
161
- declared_tools: agent
162
- .get("tools")
163
- .and_then(serde_json::Value::as_array)
164
- .map(|items| {
165
- items
166
- .iter()
167
- .filter_map(serde_json::Value::as_str)
168
- .map(str::to_string)
169
- .collect()
170
- }),
171
144
  system_prompt_inline: system_prompt
172
145
  .and_then(|prompt| prompt.get("inline"))
173
146
  .and_then(serde_json::Value::as_str)
@@ -233,9 +206,6 @@ pub(crate) fn compile_worker_system_prompt(
233
206
  if let Some(contract) = output_contract(agent, &report_result) {
234
207
  chunks.push(contract);
235
208
  }
236
- if let Some(notes) = permission_notes(agent)? {
237
- chunks.push(notes);
238
- }
239
209
  Ok(chunks
240
210
  .into_iter()
241
211
  .filter(|chunk| !chunk.is_empty())
@@ -254,43 +224,6 @@ Do not invent a second team protocol or assume that a configured lazy MCP server
254
224
  .to_string()
255
225
  }
256
226
 
257
- /// ---
258
- /// purpose: 解析该 agent 最终生效的工具串
259
- /// params:
260
- /// provider: 用于查 bypass argv 定义的 provider
261
- /// returns: 排序后的工具串;agent 声明 bypass 时末尾追加 dangerous_auto_approve
262
- /// errors: 权限解析失败,或声明了 bypass 但该 provider 没有 bypass argv 定义时返回 RequirementUnmet
263
- /// ---
264
- pub(crate) fn resolved_tool_strings_for_command(
265
- agent: &WorkerCommandAgent,
266
- provider: Provider,
267
- ) -> Result<Vec<String>, LifecycleError> {
268
- let mut tools: Vec<String> = resolve_agent_permissions(agent, provider)?
269
- .sorted_tool_strings()
270
- .into_iter()
271
- .map(str::to_string)
272
- .collect();
273
- // 0.5.66 bypass 单源:读 agent 自己的 `dangerously_skip_permissions` bool,
274
- // 不再收 team/runtime/leader argv 派生的 `DangerousApproval`。true → 加
275
- // `dangerous_auto_approve` 哨兵(adapter 消费点不变);provider 无 bypass
276
- // argv 定义 → fail-loud,不静默 fallback。
277
- if agent.dangerously_skip_permissions && provider != Provider::Pi {
278
- let flag = crate::provider::bypass_flags::provider_bypass_flag(provider).ok_or_else(
279
- || {
280
- LifecycleError::RequirementUnmet(format!(
281
- "provider {} has no bypass argv flag defined; dangerously_skip_permissions=true cannot be honored",
282
- provider_display_name(provider)
283
- ))
284
- },
285
- )?;
286
- debug_assert!(flag.starts_with("--"));
287
- if !tools.iter().any(|tool| tool == "dangerous_auto_approve") {
288
- tools.push("dangerous_auto_approve".to_string());
289
- }
290
- }
291
- Ok(tools)
292
- }
293
-
294
227
  /// Provider-facing MCP tool name. Only verified call forms are filled in.
295
228
  /// Unverified providers keep the historical dotted spelling.
296
229
  fn mcp_tool_name(provider: Provider, server: &str, tool: &str) -> String {
@@ -308,33 +241,6 @@ fn mcp_tool_name(provider: Provider, server: &str, tool: &str) -> String {
308
241
  }
309
242
  }
310
243
 
311
- fn provider_display_name(provider: Provider) -> &'static str {
312
- match provider {
313
- Provider::Claude => "claude",
314
- Provider::ClaudeCode => "claude_code",
315
- Provider::Codex => "codex",
316
- Provider::Copilot => "copilot",
317
- Provider::GeminiCli => "gemini_cli",
318
- Provider::Grok => "grok",
319
- Provider::CursorAgent => "cursor_agent",
320
- Provider::Pi => "pi",
321
- Provider::Fake => "fake",
322
- }
323
- }
324
-
325
- fn resolve_agent_permissions(
326
- agent: &WorkerCommandAgent,
327
- provider: Provider,
328
- ) -> Result<crate::model::permissions::ResolvedPermissions, LifecycleError> {
329
- resolve_permissions(&AgentPermissionInput {
330
- id: agent.id.as_deref().map(AgentId::new),
331
- provider,
332
- role: agent.role.clone(),
333
- tools: agent.declared_tools.clone(),
334
- })
335
- .map_err(|e| LifecycleError::Compile(e.to_string()))
336
- }
337
-
338
244
  fn runtime_contract_section(send_message: &str, report_result: &str) -> String {
339
245
  RUNTIME_CONTRACT_SECTION
340
246
  .replace("{send_message}", send_message)
@@ -405,161 +311,14 @@ fn output_contract(agent: &WorkerCommandAgent, report_result: &str) -> Option<St
405
311
  .then(|| RESULT_ENVELOPE_OUTPUT_CONTRACT.replace("{report_result}", report_result))
406
312
  }
407
313
 
408
- fn permission_notes(agent: &WorkerCommandAgent) -> Result<Option<String>, LifecycleError> {
409
- let permissions = resolve_agent_permissions(agent, agent.provider)?;
410
- // C-2-1/C-2-2 cr verdict — Copilot 一期 framework 不替决 fs_read/fs_list/git_diff/
411
- // provider_builtin(provider prompt 控);为诚实(MUST-NOT-13)在 system prompt 内
412
- // 总是声明这些 provider-level prompt_only 工具,即便角色未显式声明。
413
- let provider_prompt_only_extras = provider_default_prompt_only_tools(agent.provider);
414
- let mut prompt_only: std::collections::BTreeSet<String> = permissions
415
- .resolved_tools
416
- .iter()
417
- .filter(|tool| tool.enforcement == Enforcement::PromptOnly)
418
- .filter_map(|tool| serde_json::to_value(tool.tool).ok())
419
- .filter_map(|value| value.as_str().map(str::to_string))
420
- .collect();
421
- for tool in provider_prompt_only_extras {
422
- prompt_only.insert((*tool).to_string());
423
- }
424
- if prompt_only.is_empty() {
425
- return Ok(None);
426
- }
427
- let prompt_only: Vec<String> = prompt_only.into_iter().collect();
428
- Ok(Some(format!(
429
- "Permission note: these tools are prompt-only for this provider and not hard-enforced: {}",
430
- prompt_only.join(", ")
431
- )))
432
- }
433
-
434
- /// C-2-1 cr verdict — provider-level prompt_only tools that the framework cannot
435
- /// hard-enforce. The system prompt declares them so the worker is honest about
436
- /// where consent gates actually live (provider prompt vs framework).
437
- fn provider_default_prompt_only_tools(provider: Provider) -> &'static [&'static str] {
438
- match provider {
439
- // C-2-1: fs_read / fs_list / git_diff / provider_builtin 由 provider prompt
440
- // 控制,framework 不替决(prompt_only 诚实声明)。
441
- Provider::Copilot => &["fs_list", "fs_read", "git_diff", "provider_builtin"],
442
- _ => &[],
443
- }
444
- }
445
-
446
314
  #[cfg(test)]
447
315
  mod tests {
448
316
  use super::*;
449
317
 
450
- #[test]
451
- fn empty_tools_use_role_defaults_and_aliases_resolve_before_command() {
452
- let agent = WorkerCommandAgent {
453
- id: Some("dev".to_string()),
454
- provider: Provider::ClaudeCode,
455
- role: Some("developer".to_string()),
456
- declared_tools: Some(Vec::new()),
457
- system_prompt_inline: None,
458
- system_prompt_file: None,
459
- output_contract_format: None,
460
- communication_mode: CommunicationMode::default(),
461
- dangerously_skip_permissions: false,
462
- };
463
- let tools =
464
- resolved_tool_strings_for_command(&agent, Provider::ClaudeCode).unwrap();
465
- assert_eq!(
466
- tools,
467
- [
468
- "execute_bash",
469
- "fs_list",
470
- "fs_read",
471
- "fs_write",
472
- "git_diff",
473
- "mcp_team",
474
- "provider_builtin"
475
- ]
476
- );
477
-
478
- let agent = WorkerCommandAgent {
479
- declared_tools: Some(vec!["fs_*".to_string(), "@team-orchestrator".to_string()]),
480
- ..agent
481
- };
482
- let tools =
483
- resolved_tool_strings_for_command(&agent, Provider::ClaudeCode).unwrap();
484
- assert_eq!(tools, ["fs_list", "fs_read", "fs_write", "mcp_team"]);
485
- }
486
-
487
- // 0.5.66 bypass 单源 §4.1:true → tools 追加 `dangerous_auto_approve` 哨兵。
488
- #[test]
489
- fn test_true_field_produces_bypass_flag_argv() {
490
- let agent = WorkerCommandAgent {
491
- id: Some("dev".to_string()),
492
- provider: Provider::ClaudeCode,
493
- role: Some("developer".to_string()),
494
- declared_tools: Some(vec!["mcp_team".to_string()]),
495
- system_prompt_inline: None,
496
- system_prompt_file: None,
497
- output_contract_format: None,
498
- communication_mode: CommunicationMode::default(),
499
- dangerously_skip_permissions: true,
500
- };
501
- let tools = resolved_tool_strings_for_command(&agent, Provider::ClaudeCode).unwrap();
502
- assert!(
503
- tools.iter().any(|tool| tool == "dangerous_auto_approve"),
504
- "true field must append the dangerous_auto_approve sentinel; tools={tools:?}"
505
- );
506
- assert!(
507
- tools.iter().any(|tool| tool == "mcp_team"),
508
- "normal tools must remain; tools={tools:?}"
509
- );
510
- }
511
-
512
- // 0.5.66 bypass 单源 §4.1:false → 不追加哨兵。
513
- #[test]
514
- fn test_false_field_produces_no_bypass_flag_argv() {
515
- let agent = WorkerCommandAgent {
516
- id: Some("dev".to_string()),
517
- provider: Provider::ClaudeCode,
518
- role: Some("developer".to_string()),
519
- declared_tools: Some(vec!["mcp_team".to_string()]),
520
- system_prompt_inline: None,
521
- system_prompt_file: None,
522
- output_contract_format: None,
523
- communication_mode: CommunicationMode::default(),
524
- dangerously_skip_permissions: false,
525
- };
526
- let tools = resolved_tool_strings_for_command(&agent, Provider::ClaudeCode).unwrap();
527
- assert!(
528
- !tools.iter().any(|tool| tool == "dangerous_auto_approve"),
529
- "false field must not append the sentinel; tools={tools:?}"
530
- );
531
- }
532
-
533
- // 0.5.66 bypass 单源 §4.1:provider 未定义 bypass 参数 + true → fail-loud。
534
- #[test]
535
- fn test_provider_without_bypass_flag_definition_errors() {
536
- let agent = WorkerCommandAgent {
537
- id: Some("dev".to_string()),
538
- provider: Provider::GeminiCli,
539
- role: Some("developer".to_string()),
540
- declared_tools: Some(vec!["mcp_team".to_string()]),
541
- system_prompt_inline: None,
542
- system_prompt_file: None,
543
- output_contract_format: None,
544
- communication_mode: CommunicationMode::default(),
545
- dangerously_skip_permissions: true,
546
- };
547
- let err = resolved_tool_strings_for_command(&agent, Provider::GeminiCli).unwrap_err();
548
- let msg = err.to_string();
549
- assert!(
550
- msg.contains("no bypass argv flag defined"),
551
- "provider without bypass flag + true must fail-loud; got {msg}"
552
- );
553
- assert!(
554
- msg.contains("gemini_cli"),
555
- "error must name the provider; got {msg}"
556
- );
557
- }
558
-
559
318
  #[test]
560
319
  fn system_prompt_uses_identity_then_runtime_contract_python_order() {
561
320
  // #264 D6: Python truth source (prompt.py:39, live ps confirmed) builds
562
- // chunks = [identity, TEAMMATE_SYSTEM_PROMPT, role_body, output, permissions].
321
+ // chunks = [identity, TEAMMATE_SYSTEM_PROMPT, role_body, output].
563
322
  // The previous assertion locked the inverted contract-first order with no
564
323
  // Python evidence; this is the corrected golden.
565
324
  // 0.3.5 union (copilot v2 C-1 / B2 MUST-4 行为层守): identity 必须 FIRST —
@@ -569,7 +328,6 @@ mod tests {
569
328
  id: Some("coder".to_string()),
570
329
  provider: Provider::Codex,
571
330
  role: Some("Runtime Developer".to_string()),
572
- declared_tools: Some(vec!["mcp_team".to_string()]),
573
331
  system_prompt_inline: Some("Implement the assigned slice.".to_string()),
574
332
  system_prompt_file: None,
575
333
  output_contract_format: Some("result_envelope_v1".to_string()),
@@ -590,8 +348,7 @@ mod tests {
590
348
  let output = prompt
591
349
  .find("Final completion must call team_orchestrator.report_result exactly once")
592
350
  .unwrap();
593
- let permissions = prompt.find("Permission note:").unwrap();
594
- assert!(identity < runtime && runtime < role && role < output && output < permissions);
351
+ assert!(identity < runtime && runtime < role && role < output);
595
352
  let slowdown_phrase = format!("500/{}", 500 + 29);
596
353
  assert!(prompt.contains(&slowdown_phrase));
597
354
  assert!(prompt.contains("Runtime Developer"));
@@ -605,7 +362,6 @@ mod tests {
605
362
  id: Some("worker".to_string()),
606
363
  provider,
607
364
  role: Some("developer".to_string()),
608
- declared_tools: Some(vec!["mcp_team".to_string()]),
609
365
  system_prompt_inline: Some("worker body".to_string()),
610
366
  system_prompt_file: None,
611
367
  output_contract_format: Some("result_envelope_v1".to_string()),
@@ -86,9 +86,8 @@ impl ProviderEffort {
86
86
  matches!(self, Self::Max)
87
87
  }
88
88
 
89
- /// True when the given provider supports this effort level. `max` is
90
- /// Claude-only; other levels are supported by Claude, Codex, and Grok.
91
- /// Copilot/Gemini/CursorAgent/Fake ignore the field (warning at runtime).
89
+ /// Native command support. Claude/Pi accept max; Codex/Grok accept
90
+ /// the other levels. Use resolve_for_provider for admission/ignore policy.
92
91
  pub fn is_supported_by(self, provider: Provider) -> bool {
93
92
  match provider {
94
93
  Provider::Claude | Provider::ClaudeCode | Provider::Pi => true,
@@ -98,6 +97,21 @@ impl ProviderEffort {
98
97
  }
99
98
  }
100
99
  }
100
+
101
+ /// One admission policy for compiler, direct specs and runtime commands.
102
+ /// Ok(None) preserves the legacy warning-and-ignore providers.
103
+ pub fn resolve_for_provider(self, provider: Provider) -> Result<Option<Self>, &'static str> {
104
+ if provider == Provider::CursorAgent {
105
+ return Err("cursor_agent does not support effort; the Cursor CLI has no --effort flag");
106
+ }
107
+ if self.is_supported_by(provider) {
108
+ Ok(Some(self))
109
+ } else if self.is_claude_only() {
110
+ Err("effort 'max' is only supported by claude/claude_code/pi")
111
+ } else {
112
+ Ok(None)
113
+ }
114
+ }
101
115
  }
102
116
 
103
117
  /// auth 模式(`AUTH_MODES` `profiles/constants.py:6`)。
@@ -227,29 +241,6 @@ pub enum PermissionMode {
227
241
  Trusted,
228
242
  }
229
243
 
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
244
  /// tmux pane 存活态(`state.py:336-341`)。**`Unknown` 既不可当 dead 也不可当 live**
254
245
  /// (owner-gate `state.py:382` 用 `!= LIVE`)—— 穷尽 match,不 fallthrough(§11)。
255
246
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -345,18 +336,6 @@ mod tests {
345
336
  serde_json::to_string(&DisplayBackend::Adaptive).unwrap(),
346
337
  "\"adaptive\"",
347
338
  ),
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
339
  (
361
340
  serde_json::to_string(&PaneLiveness::Unknown).unwrap(),
362
341
  "\"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,21 +6,19 @@
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;
23
- use crate::provider::wire::{is_claude_family, parse_canonical_provider};
20
+ use crate::model::yaml;
21
+ use crate::provider::wire::parse_canonical_provider;
24
22
 
25
23
  /// result_envelope_v1 顶层 required(= allowed)。
26
24
  const RESULT_REQUIRED: &[&str] = &[
@@ -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",
@@ -465,15 +463,16 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
465
463
  "{path}/effort: unknown effort '{raw}' (allowed: low|medium|high|xhigh|max)"
466
464
  ));
467
465
  }
468
- Some(effort) if effort.is_claude_only() => {
466
+ Some(effort) => {
469
467
  let provider = agent.get("provider").and_then(Yaml::as_str).unwrap_or("");
470
- if !parse_canonical_provider(provider).is_some_and(is_claude_family) {
471
- errors.push(format!(
472
- "{path}/effort: effort '{raw}' is only supported by claude/claude_code (provider: {provider})"
473
- ));
468
+ if let Some(provider_enum) = parse_canonical_provider(provider) {
469
+ if let Err(reason) = effort.resolve_for_provider(provider_enum) {
470
+ errors.push(format!(
471
+ "{path}/effort: {reason} (effort: {raw}; provider: {provider})"
472
+ ));
473
+ }
474
474
  }
475
475
  }
476
- Some(_) => {}
477
476
  }
478
477
  }
479
478
  check_keys_y(
@@ -483,7 +482,6 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
483
482
  &["inline", "file"],
484
483
  errors,
485
484
  );
486
- check_list_y(agent.get("tools"), &format!("{path}/tools"), errors);
487
485
  check_list_y(
488
486
  agent.get("preferred_for"),
489
487
  &format!("{path}/preferred_for"),
@@ -787,67 +785,6 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
787
785
  ));
788
786
  }
789
787
  }
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
788
  }
852
789
 
853
790
  let routing = spec.get("routing");
@@ -1070,7 +1007,7 @@ mod tests {
1070
1007
  }
1071
1008
 
1072
1009
  #[test]
1073
- fn pi_spec_accepts_native_categories_and_rejects_provider_builtin() {
1010
+ fn tools_are_transparent_for_all_providers() {
1074
1011
  let pi_text = include_str!("testdata/team.spec.yaml")
1075
1012
  .lines()
1076
1013
  .filter(|line| {
@@ -1093,22 +1030,26 @@ mod tests {
1093
1030
  1,
1094
1031
  );
1095
1032
  let bad_agent = yaml::loads(&bad_agent_text).unwrap();
1096
- let error = validate_spec(&bad_agent, Path::new(TD)).unwrap_err().to_string();
1097
1033
  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}"
1034
+ validate_spec(&bad_agent, Path::new(TD)).is_ok(),
1035
+ "Pi tools metadata must be ignored"
1100
1036
  );
1101
1037
 
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();
1038
+ let missing_tools = pi_text
1039
+ .replacen(
1040
+ " tools:\n - fs_read\n - fs_list\n - mcp_team\n",
1041
+ "",
1042
+ 1,
1043
+ )
1044
+ .replacen(
1045
+ " tools:\n - fs_read\n - fs_write\n - fs_list\n - execute_bash\n - mcp_team\n",
1046
+ "",
1047
+ 1,
1048
+ );
1049
+ let missing_tools = yaml::loads(&missing_tools).unwrap();
1109
1050
  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}"
1051
+ validate_spec(&missing_tools, Path::new(TD)).is_ok(),
1052
+ "omitted tools metadata must be accepted"
1112
1053
  );
1113
1054
  }
1114
1055
 
@@ -1121,7 +1062,6 @@ mod tests {
1121
1062
  "/version: must equal 1",
1122
1063
  "/team/mode: invalid mode",
1123
1064
  "/leader/provider: unknown provider 'badprov'",
1124
- "/agents/0/tools: unknown tool 'banana'",
1125
1065
  ]
1126
1066
  );
1127
1067
  }