@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
@@ -1,721 +0,0 @@
1
- //!
2
- //! §19/§3 移植 `permissions.py`:role/provider/alias → 规范化权限矩阵。
3
- //!
4
- //! Python 全程裸 `str` + `dict[str, Any]`(`expand_tools`/`resolve_permissions`/
5
- //! `missing_tools`)。这里把"工具名"收成 `enums::Tool` 穷尽集,"provider×tool→强度"
6
- //! 收成 `enums::Enforcement` 查表。
7
- //!
8
- //! **与 Python 的一处刻意收紧**(§3):Python `expand_tools` 对**未知工具串**原样
9
- //! passthrough(`['banana']` → `['banana']`)。typed 层无法表示未知 `Tool`,故
10
- //! 未知串 → `ModelError::Validation`(混传/拼错编不过 → 提前炸,而非把脏串带进矩阵)。
11
- //! 已知别名(`fs_*`/`@builtin`/`@team-orchestrator`/`@cao-mcp-server`/`*`)与 8 个
12
- //! 规范名照常展开。
13
- //!
14
- //! **排序差异说明**:Python 按工具**字符串**字母序排;`BTreeSet<Tool>` 按 `Tool`
15
- //! 的 `Ord`(即 `enums.rs` 声明序)排,两者不同。集合**成员**完全一致(行为对拍锁
16
- //! 死);需要字节级对齐 Python `tools` 数组时用 `sorted_tool_strings()` 取字母序。
17
-
18
- use std::collections::BTreeSet;
19
-
20
- use crate::model::enums::{Enforcement, Provider, Tool};
21
- use crate::model::errors::ModelError;
22
- use crate::model::ids::AgentId;
23
-
24
- impl Tool {
25
- /// 规范名串 → `Tool`(`CANONICAL_TOOLS` `permissions.py:5-14`)。未知串 → `None`。
26
- fn from_canonical_str(s: &str) -> Option<Self> {
27
- Some(match s {
28
- "fs_read" => Self::FsRead,
29
- "fs_write" => Self::FsWrite,
30
- "fs_list" => Self::FsList,
31
- "execute_bash" => Self::ExecuteBash,
32
- "git_diff" => Self::GitDiff,
33
- "network" => Self::Network,
34
- "mcp_team" => Self::McpTeam,
35
- "provider_builtin" => Self::ProviderBuiltin,
36
- _ => return None,
37
- })
38
- }
39
-
40
- /// 规范名串(serde rename 的等价值;查表 / 字节对拍用)。
41
- fn canonical_str(self) -> &'static str {
42
- match self {
43
- Self::FsRead => "fs_read",
44
- Self::FsWrite => "fs_write",
45
- Self::FsList => "fs_list",
46
- Self::ExecuteBash => "execute_bash",
47
- Self::GitDiff => "git_diff",
48
- Self::Network => "network",
49
- Self::McpTeam => "mcp_team",
50
- Self::ProviderBuiltin => "provider_builtin",
51
- }
52
- }
53
- }
54
-
55
- /// `ROLE_DEFAULTS`(`permissions.py:16-40`)+ fallback 到 `developer`
56
- /// (`default_tools_for_role` `permissions.py:84-85`)。未知 role → `developer` 集。
57
- ///
58
- /// 注:返回**声明序集合**;Python 返回的是 role 字面顺序的 list,但下游 `resolve_*`
59
- /// 立即过 `expand_tools`(去重+排序),顺序不参与对拍,故此处用 `BTreeSet` 即可。
60
- fn default_tools_for_role(role: &str) -> BTreeSet<Tool> {
61
- use Tool::*;
62
- let tools: &[Tool] = match role {
63
- "leader" | "supervisor" => &[FsRead, FsList, McpTeam, ProviderBuiltin],
64
- "researcher" => &[FsRead, FsList, Network, McpTeam, ProviderBuiltin],
65
- "reviewer" | "code_reviewer" => &[FsRead, FsList, GitDiff, McpTeam, ProviderBuiltin],
66
- // implementation_engineer / developer / 未知 role 全落 developer 集。
67
- _ => &[
68
- FsRead,
69
- FsWrite,
70
- FsList,
71
- ExecuteBash,
72
- GitDiff,
73
- McpTeam,
74
- ProviderBuiltin,
75
- ],
76
- };
77
- tools.iter().copied().collect()
78
- }
79
-
80
- /// 别名/规范名串展开为 `Tool` 集(`expand_tools` `permissions.py:68-81`)。
81
- ///
82
- /// - `fs_*` → {fs_read, fs_write, fs_list}
83
- /// - `@builtin` → {provider_builtin}
84
- /// - `@team-orchestrator` / `@cao-mcp-server` → {mcp_team}
85
- /// - `*` → 全部 8 个规范工具
86
- /// - 规范名 → 自身
87
- /// - 其余(未知串)→ `Err(Validation)`(见模块头注:刻意收紧)
88
- pub fn expand_tools<I, S>(tools: I) -> Result<BTreeSet<Tool>, ModelError>
89
- where
90
- I: IntoIterator<Item = S>,
91
- S: AsRef<str>,
92
- {
93
- use Tool::*;
94
- let mut out: BTreeSet<Tool> = BTreeSet::new();
95
- for raw in tools {
96
- let t = raw.as_ref();
97
- match t {
98
- "fs_*" => out.extend([FsRead, FsWrite, FsList]),
99
- "@builtin" => {
100
- out.insert(ProviderBuiltin);
101
- }
102
- "@team-orchestrator" | "@cao-mcp-server" => {
103
- out.insert(McpTeam);
104
- }
105
- "*" => out.extend([
106
- FsRead,
107
- FsWrite,
108
- FsList,
109
- ExecuteBash,
110
- GitDiff,
111
- Network,
112
- McpTeam,
113
- ProviderBuiltin,
114
- ]),
115
- other => match Tool::from_canonical_str(other) {
116
- Some(tool) => {
117
- out.insert(tool);
118
- }
119
- None => {
120
- return Err(ModelError::Validation(format!(
121
- "unknown tool or alias: {other:?}"
122
- )))
123
- }
124
- },
125
- }
126
- }
127
- Ok(out)
128
- }
129
-
130
- /// Python `expand_tools` 的**字符串级**镜像(`permissions.py:68-81`):别名展开 + 未知串
131
- /// **passthrough** + `sorted(set(...))`。`validate_spec` 用它(再逐个查 canonical 报
132
- /// `unknown tool`);typed [`expand_tools`] 则对未知串 `Err`(见模块头注的刻意收紧)。
133
- pub fn expand_tool_strings<I, S>(tools: I) -> Vec<String>
134
- where
135
- I: IntoIterator<Item = S>,
136
- S: AsRef<str>,
137
- {
138
- let mut out: Vec<String> = Vec::new();
139
- for raw in tools {
140
- match raw.as_ref() {
141
- "fs_*" => out.extend(
142
- ["fs_read", "fs_write", "fs_list"]
143
- .into_iter()
144
- .map(String::from),
145
- ),
146
- "@builtin" => out.push("provider_builtin".to_string()),
147
- "@team-orchestrator" | "@cao-mcp-server" => out.push("mcp_team".to_string()),
148
- "*" => out.extend(
149
- [
150
- "fs_read",
151
- "fs_write",
152
- "fs_list",
153
- "execute_bash",
154
- "git_diff",
155
- "network",
156
- "mcp_team",
157
- "provider_builtin",
158
- ]
159
- .into_iter()
160
- .map(String::from),
161
- ),
162
- other => out.push(other.to_string()),
163
- }
164
- }
165
- out.sort();
166
- out.dedup();
167
- out
168
- }
169
-
170
- /// 是否 8 个规范工具之一(`CANONICAL_TOOLS`)。
171
- pub fn is_canonical_tool(name: &str) -> bool {
172
- Tool::from_canonical_str(name).is_some()
173
- }
174
-
175
- /// provider×tool → 强度(`PROVIDER_ENFORCEMENT` `permissions.py:42-65`)。
176
- ///
177
- /// 表里没有的 provider(如 `claude`)= Python 的 `.get(provider, {})` → 空表 →
178
- /// 每个 tool 再 `.get(tool, "prompt_only")` → 全 `prompt_only`(对拍 `R-unknownprov`)。
179
- fn provider_enforcement(provider: Provider, tool: Tool) -> Enforcement {
180
- use Enforcement::{Hard, PromptOnly};
181
- use Tool::*;
182
- match provider {
183
- Provider::ClaudeCode => match tool {
184
- Network => PromptOnly,
185
- FsRead | FsWrite | FsList | ExecuteBash | GitDiff | McpTeam | ProviderBuiltin => Hard,
186
- },
187
- Provider::GeminiCli => match tool {
188
- Network | McpTeam => PromptOnly,
189
- FsRead | FsWrite | FsList | ExecuteBash | GitDiff | ProviderBuiltin => Hard,
190
- },
191
- Provider::Fake => Hard,
192
- // 0.5.67: grok/cursor_agent 无 deny-kind 概念 (同 codex 精神), 诚实
193
- // prompt_only, 不替 provider 决。
194
- Provider::Grok | Provider::CursorAgent | Provider::Pi => PromptOnly,
195
- // Copilot(C-2-1 cr verdict):execute_bash/fs_write/network/mcp_team = hard,
196
- // fs_read/fs_list/git_diff/provider_builtin = prompt_only(诚实:copilot 无
197
- // 对应 deny kind,framework 不替决,留给 provider prompt 控制;MUST-NOT-13)。
198
- Provider::Copilot => match tool {
199
- FsRead | FsList | GitDiff | ProviderBuiltin => PromptOnly,
200
- Network | FsWrite | ExecuteBash | McpTeam => Hard,
201
- },
202
- // codex: 全 prompt_only。claude: 不在表中 → 全 prompt_only(同 fallback)。
203
- Provider::Codex | Provider::Claude => PromptOnly,
204
- }
205
- }
206
-
207
- /// 单条已解析工具(对应 Python `resolved_tools[]` 的 `{tool, enforcement}`)。
208
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
209
- pub struct ResolvedTool {
210
- pub tool: Tool,
211
- pub enforcement: Enforcement,
212
- }
213
-
214
- /// `resolve_permissions` 的最小 typed 入参(替 Python `agent: dict[str, Any]`)。
215
- ///
216
- /// `tools = Some(..)` 对应显式 `agent["tools"]`;`None`(或 Python 的空 list →
217
- /// `or` falsy)对应回退 `default_tools_for_role(role)`。
218
- #[derive(Debug, Clone)]
219
- pub struct AgentPermissionInput {
220
- pub id: Option<AgentId>,
221
- pub provider: Provider,
222
- pub role: Option<String>,
223
- /// 显式工具/别名串;`None` → 按 role 取默认。
224
- pub tools: Option<Vec<String>>,
225
- }
226
-
227
- /// `resolve_permissions` 的结果(对应 Python 返回 dict)。
228
- #[derive(Debug, Clone, PartialEq, Eq)]
229
- pub struct ResolvedPermissions {
230
- pub agent_id: Option<AgentId>,
231
- pub provider: Provider,
232
- /// 展开+去重的工具集(`BTreeSet` 序;字节对齐用 `sorted_tool_strings`)。
233
- pub tools: BTreeSet<Tool>,
234
- pub resolved_tools: Vec<ResolvedTool>,
235
- pub has_prompt_only: bool,
236
- }
237
-
238
- impl ResolvedPermissions {
239
- /// 工具名按**字符串字母序**(= Python `tools` 数组顺序),字节对拍用。
240
- pub fn sorted_tool_strings(&self) -> Vec<&'static str> {
241
- let mut v: Vec<&'static str> = self.tools.iter().map(|t| t.canonical_str()).collect();
242
- v.sort_unstable();
243
- v
244
- }
245
- }
246
-
247
- /// `resolve_permissions`(`permissions.py:88-106`)。
248
- ///
249
- /// Python `tools = agent.get("tools") or default_tools_for_role(role)`:空 list 在
250
- /// Python 是 falsy → 也回退默认。这里 `Some(vec![])` 同样视作"无显式工具"回退默认。
251
- pub fn resolve_permissions(
252
- agent: &AgentPermissionInput,
253
- ) -> Result<ResolvedPermissions, ModelError> {
254
- let resolved: BTreeSet<Tool> = match &agent.tools {
255
- Some(tools) if !tools.is_empty() => expand_tools(tools)?,
256
- _ => {
257
- let role = agent.role.as_deref().unwrap_or("developer");
258
- default_tools_for_role(role)
259
- }
260
- };
261
-
262
- let resolved_tools: Vec<ResolvedTool> = resolved
263
- .iter()
264
- .map(|&tool| ResolvedTool {
265
- tool,
266
- enforcement: provider_enforcement(agent.provider, tool),
267
- })
268
- .collect();
269
-
270
- let has_prompt_only = resolved_tools
271
- .iter()
272
- .any(|e| e.enforcement == Enforcement::PromptOnly);
273
-
274
- Ok(ResolvedPermissions {
275
- agent_id: agent.id.clone(),
276
- provider: agent.provider,
277
- tools: resolved,
278
- resolved_tools,
279
- has_prompt_only,
280
- })
281
- }
282
-
283
- /// task 类型 → 附加必需工具(`task_required_tools` `permissions.py:109-118`)。
284
- /// 未知/缺失 type 不附加。`requires_tools` 也走 `expand_tools`(同未知串收紧规则)。
285
- pub fn task_required_tools(
286
- task_type: Option<&str>,
287
- requires_tools: &[String],
288
- ) -> Result<BTreeSet<Tool>, ModelError> {
289
- let mut required: Vec<String> = requires_tools.to_vec();
290
- match task_type {
291
- Some("implementation") | Some("bug_fix") | Some("test") => {
292
- required.push("fs_write".to_string());
293
- required.push("execute_bash".to_string());
294
- }
295
- Some("review") | Some("risk_check") => {
296
- required.push("fs_read".to_string());
297
- required.push("git_diff".to_string());
298
- }
299
- Some("research") | Some("architecture") => {
300
- required.push("fs_read".to_string());
301
- }
302
- _ => {}
303
- }
304
- expand_tools(required)
305
- }
306
-
307
- /// `missing_tools`(`permissions.py:121-123`):task 需要但 agent 不被允许的工具。
308
- /// 返回**字母序**(= Python list 顺序)。
309
- pub fn missing_tools(
310
- agent: &AgentPermissionInput,
311
- task_type: Option<&str>,
312
- requires_tools: &[String],
313
- ) -> Result<Vec<Tool>, ModelError> {
314
- let allowed = resolve_permissions(agent)?.tools;
315
- let required = task_required_tools(task_type, requires_tools)?;
316
- // required 已是去重集;按字符串字母序输出,对齐 Python。
317
- let mut missing: Vec<Tool> = required
318
- .into_iter()
319
- .filter(|t| !allowed.contains(t))
320
- .collect();
321
- missing.sort_unstable_by_key(|t| t.canonical_str());
322
- Ok(missing)
323
- }
324
-
325
- #[cfg(test)]
326
- mod tests {
327
- #![allow(clippy::unwrap_used)]
328
- use super::*;
329
-
330
- /// `BTreeSet<Tool>` → 字母序串(对齐 Python `sorted(...)` list)。
331
- fn strs(set: &BTreeSet<Tool>) -> Vec<&'static str> {
332
- let mut v: Vec<&'static str> = set.iter().map(|t| t.canonical_str()).collect();
333
- v.sort_unstable();
334
- v
335
- }
336
-
337
- fn agent(
338
- provider: Provider,
339
- role: Option<&str>,
340
- tools: Option<&[&str]>,
341
- ) -> AgentPermissionInput {
342
- AgentPermissionInput {
343
- id: Some(AgentId::new("x")),
344
- provider,
345
- role: role.map(str::to_string),
346
- tools: tools.map(|t| t.iter().map(|s| s.to_string()).collect()),
347
- }
348
- }
349
-
350
- // ---- expand_tools 行为对拍(golden from team-agent-public@439bef8) ----
351
- #[test]
352
- fn expand_tools_matches_python_golden() {
353
- // E1
354
- assert_eq!(
355
- strs(&expand_tools(["fs_*"]).unwrap()),
356
- ["fs_list", "fs_read", "fs_write"]
357
- );
358
- // E2
359
- assert_eq!(
360
- strs(&expand_tools(["@builtin"]).unwrap()),
361
- ["provider_builtin"]
362
- );
363
- // E3
364
- assert_eq!(
365
- strs(&expand_tools(["*"]).unwrap()),
366
- [
367
- "execute_bash",
368
- "fs_list",
369
- "fs_read",
370
- "fs_write",
371
- "git_diff",
372
- "mcp_team",
373
- "network",
374
- "provider_builtin"
375
- ]
376
- );
377
- // E4
378
- assert_eq!(
379
- strs(&expand_tools(["@team-orchestrator", "@cao-mcp-server"]).unwrap()),
380
- ["mcp_team"]
381
- );
382
- // E5 — 去重
383
- assert_eq!(
384
- strs(&expand_tools(["fs_read", "fs_read", "network"]).unwrap()),
385
- ["fs_read", "network"]
386
- );
387
- // E6
388
- assert!(expand_tools(Vec::<String>::new()).unwrap().is_empty());
389
- // E7
390
- assert_eq!(
391
- strs(&expand_tools(["fs_*", "@builtin", "network"]).unwrap()),
392
- [
393
- "fs_list",
394
- "fs_read",
395
- "fs_write",
396
- "network",
397
- "provider_builtin"
398
- ]
399
- );
400
- // E8 — * 已含 fs_read,去重无变化
401
- assert_eq!(
402
- strs(&expand_tools(["*", "fs_read"]).unwrap()),
403
- [
404
- "execute_bash",
405
- "fs_list",
406
- "fs_read",
407
- "fs_write",
408
- "git_diff",
409
- "mcp_team",
410
- "network",
411
- "provider_builtin"
412
- ]
413
- );
414
- }
415
-
416
- /// §3 刻意收紧:未知工具串 → Validation(Python 是 passthrough,见模块头注)。
417
- #[test]
418
- fn expand_tools_rejects_unknown_string() {
419
- let err = expand_tools(["banana"]).unwrap_err();
420
- assert!(matches!(err, ModelError::Validation(_)));
421
- // 别名/规范名混入未知串也整体失败。
422
- assert!(expand_tools(["fs_read", "fs_*", "banana"]).is_err());
423
- }
424
-
425
- // ---- default_tools_for_role 行为对拍 ----
426
- #[test]
427
- fn default_tools_for_role_matches_python_golden() {
428
- assert_eq!(
429
- strs(&default_tools_for_role("leader")),
430
- ["fs_list", "fs_read", "mcp_team", "provider_builtin"]
431
- );
432
- assert_eq!(
433
- strs(&default_tools_for_role("supervisor")),
434
- ["fs_list", "fs_read", "mcp_team", "provider_builtin"]
435
- );
436
- assert_eq!(
437
- strs(&default_tools_for_role("researcher")),
438
- [
439
- "fs_list",
440
- "fs_read",
441
- "mcp_team",
442
- "network",
443
- "provider_builtin"
444
- ]
445
- );
446
- assert_eq!(
447
- strs(&default_tools_for_role("reviewer")),
448
- [
449
- "fs_list",
450
- "fs_read",
451
- "git_diff",
452
- "mcp_team",
453
- "provider_builtin"
454
- ]
455
- );
456
- assert_eq!(
457
- strs(&default_tools_for_role("code_reviewer")),
458
- [
459
- "fs_list",
460
- "fs_read",
461
- "git_diff",
462
- "mcp_team",
463
- "provider_builtin"
464
- ]
465
- );
466
- let dev = [
467
- "execute_bash",
468
- "fs_list",
469
- "fs_read",
470
- "fs_write",
471
- "git_diff",
472
- "mcp_team",
473
- "provider_builtin",
474
- ];
475
- assert_eq!(strs(&default_tools_for_role("developer")), dev);
476
- assert_eq!(
477
- strs(&default_tools_for_role("implementation_engineer")),
478
- dev
479
- );
480
- // 未知 role → developer 集。
481
- assert_eq!(strs(&default_tools_for_role("qa")), dev);
482
- }
483
-
484
- // ---- provider_enforcement 行为对拍(矩阵逐格) ----
485
- #[test]
486
- fn provider_enforcement_matches_python_matrix() {
487
- use Enforcement::{Hard, PromptOnly};
488
- // claude_code: network=prompt_only,其余 hard。
489
- assert_eq!(
490
- provider_enforcement(Provider::ClaudeCode, Tool::Network),
491
- PromptOnly
492
- );
493
- for t in [
494
- Tool::FsRead,
495
- Tool::FsWrite,
496
- Tool::FsList,
497
- Tool::ExecuteBash,
498
- Tool::GitDiff,
499
- Tool::McpTeam,
500
- Tool::ProviderBuiltin,
501
- ] {
502
- assert_eq!(provider_enforcement(Provider::ClaudeCode, t), Hard);
503
- }
504
- // gemini_cli: network + mcp_team = prompt_only,其余 hard。
505
- assert_eq!(
506
- provider_enforcement(Provider::GeminiCli, Tool::Network),
507
- PromptOnly
508
- );
509
- assert_eq!(
510
- provider_enforcement(Provider::GeminiCli, Tool::McpTeam),
511
- PromptOnly
512
- );
513
- for t in [
514
- Tool::FsRead,
515
- Tool::FsWrite,
516
- Tool::FsList,
517
- Tool::ExecuteBash,
518
- Tool::GitDiff,
519
- Tool::ProviderBuiltin,
520
- ] {
521
- assert_eq!(provider_enforcement(Provider::GeminiCli, t), Hard);
522
- }
523
- // codex: 全 prompt_only。fake: 全 hard。
524
- for t in [
525
- Tool::FsRead,
526
- Tool::Network,
527
- Tool::McpTeam,
528
- Tool::ProviderBuiltin,
529
- ] {
530
- assert_eq!(provider_enforcement(Provider::Codex, t), PromptOnly);
531
- assert_eq!(provider_enforcement(Provider::Fake, t), Hard);
532
- // claude 不在表中 → fallback prompt_only。
533
- assert_eq!(provider_enforcement(Provider::Claude, t), PromptOnly);
534
- }
535
- }
536
-
537
- // ---- resolve_permissions 行为对拍(golden) ----
538
- #[test]
539
- fn resolve_permissions_leader_claude_code() {
540
- // R-leader-cc
541
- let r = resolve_permissions(&agent(Provider::ClaudeCode, Some("leader"), None)).unwrap();
542
- assert_eq!(
543
- r.sorted_tool_strings(),
544
- ["fs_list", "fs_read", "mcp_team", "provider_builtin"]
545
- );
546
- assert!(!r.has_prompt_only);
547
- for e in &r.resolved_tools {
548
- assert_eq!(e.enforcement, Enforcement::Hard);
549
- }
550
- }
551
-
552
- #[test]
553
- fn resolve_permissions_developer_codex_all_prompt_only() {
554
- // R-dev-codex
555
- let r = resolve_permissions(&agent(Provider::Codex, Some("developer"), None)).unwrap();
556
- assert_eq!(
557
- r.sorted_tool_strings(),
558
- [
559
- "execute_bash",
560
- "fs_list",
561
- "fs_read",
562
- "fs_write",
563
- "git_diff",
564
- "mcp_team",
565
- "provider_builtin"
566
- ]
567
- );
568
- assert!(r.has_prompt_only);
569
- for e in &r.resolved_tools {
570
- assert_eq!(e.enforcement, Enforcement::PromptOnly);
571
- }
572
- }
573
-
574
- #[test]
575
- fn resolve_permissions_impl_gemini_mcp_team_prompt_only() {
576
- // R-impl-gem:mcp_team=prompt_only,其余 hard → has_prompt_only=true。
577
- let r = resolve_permissions(&agent(
578
- Provider::GeminiCli,
579
- Some("implementation_engineer"),
580
- None,
581
- ))
582
- .unwrap();
583
- assert!(r.has_prompt_only);
584
- let mcp = r
585
- .resolved_tools
586
- .iter()
587
- .find(|e| e.tool == Tool::McpTeam)
588
- .unwrap();
589
- assert_eq!(mcp.enforcement, Enforcement::PromptOnly);
590
- let gd = r
591
- .resolved_tools
592
- .iter()
593
- .find(|e| e.tool == Tool::GitDiff)
594
- .unwrap();
595
- assert_eq!(gd.enforcement, Enforcement::Hard);
596
- }
597
-
598
- #[test]
599
- fn resolve_permissions_explicit_tools_and_norole_and_unknownprov() {
600
- // R-explicit:显式 tools 走 expand_tools,provider=claude_code。
601
- let r = resolve_permissions(&agent(
602
- Provider::ClaudeCode,
603
- None,
604
- Some(&["fs_*", "network"]),
605
- ))
606
- .unwrap();
607
- assert_eq!(
608
- r.sorted_tool_strings(),
609
- ["fs_list", "fs_read", "fs_write", "network"]
610
- );
611
- assert!(r.has_prompt_only); // network=prompt_only
612
-
613
- // R-norole:无 role → developer 默认,全 hard(claude_code)。
614
- let r2 = resolve_permissions(&AgentPermissionInput {
615
- id: Some(AgentId::new("a6")),
616
- provider: Provider::ClaudeCode,
617
- role: None,
618
- tools: None,
619
- })
620
- .unwrap();
621
- assert_eq!(
622
- r2.sorted_tool_strings(),
623
- [
624
- "execute_bash",
625
- "fs_list",
626
- "fs_read",
627
- "fs_write",
628
- "git_diff",
629
- "mcp_team",
630
- "provider_builtin"
631
- ]
632
- );
633
- assert!(!r2.has_prompt_only);
634
-
635
- // R-unknownprov:provider=claude 不在矩阵 → 全 prompt_only。
636
- let r3 = resolve_permissions(&agent(Provider::Claude, Some("developer"), None)).unwrap();
637
- assert!(r3.has_prompt_only);
638
- for e in &r3.resolved_tools {
639
- assert_eq!(e.enforcement, Enforcement::PromptOnly);
640
- }
641
-
642
- // agent_id passthrough。
643
- assert_eq!(r.agent_id, Some(AgentId::new("x")));
644
- }
645
-
646
- #[test]
647
- fn resolve_permissions_empty_tools_falls_back_to_role_default() {
648
- // Python: agent.get("tools") or default → 空 list falsy → 用 role 默认。
649
- let r =
650
- resolve_permissions(&agent(Provider::ClaudeCode, Some("leader"), Some(&[]))).unwrap();
651
- assert_eq!(
652
- r.sorted_tool_strings(),
653
- ["fs_list", "fs_read", "mcp_team", "provider_builtin"]
654
- );
655
- }
656
-
657
- // ---- task_required_tools 行为对拍(golden) ----
658
- #[test]
659
- fn task_required_tools_matches_python_golden() {
660
- // T1
661
- assert_eq!(
662
- strs(&task_required_tools(Some("implementation"), &[]).unwrap()),
663
- ["execute_bash", "fs_write"]
664
- );
665
- // T2
666
- assert_eq!(
667
- strs(&task_required_tools(Some("review"), &[]).unwrap()),
668
- ["fs_read", "git_diff"]
669
- );
670
- // T3
671
- assert_eq!(
672
- strs(&task_required_tools(Some("research"), &[]).unwrap()),
673
- ["fs_read"]
674
- );
675
- // T4
676
- assert_eq!(
677
- strs(&task_required_tools(Some("bug_fix"), &["network".to_string()]).unwrap()),
678
- ["execute_bash", "fs_write", "network"]
679
- );
680
- // T5 未知 type
681
- assert!(task_required_tools(Some("unknown_type"), &[])
682
- .unwrap()
683
- .is_empty());
684
- // T6 无 type
685
- assert!(task_required_tools(None, &[]).unwrap().is_empty());
686
- // T7 requires_tools 走 expand_tools(fs_* 展开)
687
- assert_eq!(
688
- strs(&task_required_tools(Some("architecture"), &["fs_*".to_string()]).unwrap()),
689
- ["fs_list", "fs_read", "fs_write"]
690
- );
691
- }
692
-
693
- // ---- missing_tools 行为对拍(golden) ----
694
- #[test]
695
- fn missing_tools_matches_python_golden() {
696
- // M1:reviewer(claude_code)缺 implementation 所需 fs_write/execute_bash。
697
- let m1 = missing_tools(
698
- &agent(Provider::ClaudeCode, Some("reviewer"), None),
699
- Some("implementation"),
700
- &[],
701
- )
702
- .unwrap();
703
- assert_eq!(m1, [Tool::ExecuteBash, Tool::FsWrite]);
704
- // M2:developer 不缺。
705
- let m2 = missing_tools(
706
- &agent(Provider::ClaudeCode, Some("developer"), None),
707
- Some("implementation"),
708
- &[],
709
- )
710
- .unwrap();
711
- assert!(m2.is_empty());
712
- // M3:researcher 缺 git_diff(review 需要),但有 fs_read。
713
- let m3 = missing_tools(
714
- &agent(Provider::ClaudeCode, Some("researcher"), None),
715
- Some("review"),
716
- &[],
717
- )
718
- .unwrap();
719
- assert_eq!(m3, [Tool::GitDiff]);
720
- }
721
- }