@team-agent/installer 0.2.10 → 0.3.0

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 (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1077 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1141 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +436 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1063 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +487 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +685 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +388 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +537 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +582 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +656 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +90 -106
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -83
  203. package/src/team_agent/coordinator/lifecycle.py +0 -363
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -200
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -111
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -254
  255. package/src/team_agent/messaging/delivery.py +0 -473
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -457
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -86
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1239
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -143
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -602
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -0,0 +1,567 @@
1
+ //! §19/§3 移植 `permissions.py`:role/provider/alias → 规范化权限矩阵。
2
+ //!
3
+ //! Python 全程裸 `str` + `dict[str, Any]`(`expand_tools`/`resolve_permissions`/
4
+ //! `missing_tools`)。这里把"工具名"收成 `enums::Tool` 穷尽集,"provider×tool→强度"
5
+ //! 收成 `enums::Enforcement` 查表。
6
+ //!
7
+ //! **与 Python 的一处刻意收紧**(§3):Python `expand_tools` 对**未知工具串**原样
8
+ //! passthrough(`['banana']` → `['banana']`)。typed 层无法表示未知 `Tool`,故
9
+ //! 未知串 → `ModelError::Validation`(混传/拼错编不过 → 提前炸,而非把脏串带进矩阵)。
10
+ //! 已知别名(`fs_*`/`@builtin`/`@team-orchestrator`/`@cao-mcp-server`/`*`)与 8 个
11
+ //! 规范名照常展开。
12
+ //!
13
+ //! **排序差异说明**:Python 按工具**字符串**字母序排;`BTreeSet<Tool>` 按 `Tool`
14
+ //! 的 `Ord`(即 `enums.rs` 声明序)排,两者不同。集合**成员**完全一致(行为对拍锁
15
+ //! 死);需要字节级对齐 Python `tools` 数组时用 `sorted_tool_strings()` 取字母序。
16
+
17
+ use std::collections::BTreeSet;
18
+
19
+ use crate::model::enums::{Enforcement, Provider, Tool};
20
+ use crate::model::errors::ModelError;
21
+ use crate::model::ids::AgentId;
22
+
23
+ impl Tool {
24
+ /// 规范名串 → `Tool`(`CANONICAL_TOOLS` `permissions.py:5-14`)。未知串 → `None`。
25
+ fn from_canonical_str(s: &str) -> Option<Self> {
26
+ Some(match s {
27
+ "fs_read" => Self::FsRead,
28
+ "fs_write" => Self::FsWrite,
29
+ "fs_list" => Self::FsList,
30
+ "execute_bash" => Self::ExecuteBash,
31
+ "git_diff" => Self::GitDiff,
32
+ "network" => Self::Network,
33
+ "mcp_team" => Self::McpTeam,
34
+ "provider_builtin" => Self::ProviderBuiltin,
35
+ _ => return None,
36
+ })
37
+ }
38
+
39
+ /// 规范名串(serde rename 的等价值;查表 / 字节对拍用)。
40
+ fn canonical_str(self) -> &'static str {
41
+ match self {
42
+ Self::FsRead => "fs_read",
43
+ Self::FsWrite => "fs_write",
44
+ Self::FsList => "fs_list",
45
+ Self::ExecuteBash => "execute_bash",
46
+ Self::GitDiff => "git_diff",
47
+ Self::Network => "network",
48
+ Self::McpTeam => "mcp_team",
49
+ Self::ProviderBuiltin => "provider_builtin",
50
+ }
51
+ }
52
+ }
53
+
54
+ /// `ROLE_DEFAULTS`(`permissions.py:16-40`)+ fallback 到 `developer`
55
+ /// (`default_tools_for_role` `permissions.py:84-85`)。未知 role → `developer` 集。
56
+ ///
57
+ /// 注:返回**声明序集合**;Python 返回的是 role 字面顺序的 list,但下游 `resolve_*`
58
+ /// 立即过 `expand_tools`(去重+排序),顺序不参与对拍,故此处用 `BTreeSet` 即可。
59
+ pub fn default_tools_for_role(role: &str) -> BTreeSet<Tool> {
60
+ use Tool::*;
61
+ let tools: &[Tool] = match role {
62
+ "leader" | "supervisor" => &[FsRead, FsList, McpTeam, ProviderBuiltin],
63
+ "researcher" => &[FsRead, FsList, Network, McpTeam, ProviderBuiltin],
64
+ "reviewer" | "code_reviewer" => &[FsRead, FsList, GitDiff, McpTeam, ProviderBuiltin],
65
+ // implementation_engineer / developer / 未知 role 全落 developer 集。
66
+ _ => &[
67
+ FsRead,
68
+ FsWrite,
69
+ FsList,
70
+ ExecuteBash,
71
+ GitDiff,
72
+ McpTeam,
73
+ ProviderBuiltin,
74
+ ],
75
+ };
76
+ tools.iter().copied().collect()
77
+ }
78
+
79
+ /// 别名/规范名串展开为 `Tool` 集(`expand_tools` `permissions.py:68-81`)。
80
+ ///
81
+ /// - `fs_*` → {fs_read, fs_write, fs_list}
82
+ /// - `@builtin` → {provider_builtin}
83
+ /// - `@team-orchestrator` / `@cao-mcp-server` → {mcp_team}
84
+ /// - `*` → 全部 8 个规范工具
85
+ /// - 规范名 → 自身
86
+ /// - 其余(未知串)→ `Err(Validation)`(见模块头注:刻意收紧)
87
+ pub fn expand_tools<I, S>(tools: I) -> Result<BTreeSet<Tool>, ModelError>
88
+ where
89
+ I: IntoIterator<Item = S>,
90
+ S: AsRef<str>,
91
+ {
92
+ use Tool::*;
93
+ let mut out: BTreeSet<Tool> = BTreeSet::new();
94
+ for raw in tools {
95
+ let t = raw.as_ref();
96
+ match t {
97
+ "fs_*" => out.extend([FsRead, FsWrite, FsList]),
98
+ "@builtin" => {
99
+ out.insert(ProviderBuiltin);
100
+ }
101
+ "@team-orchestrator" | "@cao-mcp-server" => {
102
+ out.insert(McpTeam);
103
+ }
104
+ "*" => out.extend([
105
+ FsRead,
106
+ FsWrite,
107
+ FsList,
108
+ ExecuteBash,
109
+ GitDiff,
110
+ Network,
111
+ McpTeam,
112
+ ProviderBuiltin,
113
+ ]),
114
+ other => match Tool::from_canonical_str(other) {
115
+ Some(tool) => {
116
+ out.insert(tool);
117
+ }
118
+ None => {
119
+ return Err(ModelError::Validation(format!(
120
+ "unknown tool or alias: {other:?}"
121
+ )))
122
+ }
123
+ },
124
+ }
125
+ }
126
+ Ok(out)
127
+ }
128
+
129
+ /// Python `expand_tools` 的**字符串级**镜像(`permissions.py:68-81`):别名展开 + 未知串
130
+ /// **passthrough** + `sorted(set(...))`。`validate_spec` 用它(再逐个查 canonical 报
131
+ /// `unknown tool`);typed [`expand_tools`] 则对未知串 `Err`(见模块头注的刻意收紧)。
132
+ pub fn expand_tool_strings<I, S>(tools: I) -> Vec<String>
133
+ where
134
+ I: IntoIterator<Item = S>,
135
+ S: AsRef<str>,
136
+ {
137
+ let mut out: Vec<String> = Vec::new();
138
+ for raw in tools {
139
+ match raw.as_ref() {
140
+ "fs_*" => out.extend(["fs_read", "fs_write", "fs_list"].into_iter().map(String::from)),
141
+ "@builtin" => out.push("provider_builtin".to_string()),
142
+ "@team-orchestrator" | "@cao-mcp-server" => out.push("mcp_team".to_string()),
143
+ "*" => out.extend(
144
+ [
145
+ "fs_read",
146
+ "fs_write",
147
+ "fs_list",
148
+ "execute_bash",
149
+ "git_diff",
150
+ "network",
151
+ "mcp_team",
152
+ "provider_builtin",
153
+ ]
154
+ .into_iter()
155
+ .map(String::from),
156
+ ),
157
+ other => out.push(other.to_string()),
158
+ }
159
+ }
160
+ out.sort();
161
+ out.dedup();
162
+ out
163
+ }
164
+
165
+ /// 是否 8 个规范工具之一(`CANONICAL_TOOLS`)。
166
+ pub fn is_canonical_tool(name: &str) -> bool {
167
+ Tool::from_canonical_str(name).is_some()
168
+ }
169
+
170
+ /// provider×tool → 强度(`PROVIDER_ENFORCEMENT` `permissions.py:42-65`)。
171
+ ///
172
+ /// 表里没有的 provider(如 `claude`)= Python 的 `.get(provider, {})` → 空表 →
173
+ /// 每个 tool 再 `.get(tool, "prompt_only")` → 全 `prompt_only`(对拍 `R-unknownprov`)。
174
+ pub fn provider_enforcement(provider: Provider, tool: Tool) -> Enforcement {
175
+ use Enforcement::{Hard, PromptOnly};
176
+ use Tool::*;
177
+ match provider {
178
+ Provider::ClaudeCode => match tool {
179
+ Network => PromptOnly,
180
+ FsRead | FsWrite | FsList | ExecuteBash | GitDiff | McpTeam | ProviderBuiltin => Hard,
181
+ },
182
+ Provider::GeminiCli => match tool {
183
+ Network | McpTeam => PromptOnly,
184
+ FsRead | FsWrite | FsList | ExecuteBash | GitDiff | ProviderBuiltin => Hard,
185
+ },
186
+ Provider::Fake => Hard,
187
+ // codex: 全 prompt_only。claude: 不在表中 → 全 prompt_only(同 fallback)。
188
+ Provider::Codex | Provider::Claude => PromptOnly,
189
+ }
190
+ }
191
+
192
+ /// 单条已解析工具(对应 Python `resolved_tools[]` 的 `{tool, enforcement}`)。
193
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
194
+ pub struct ResolvedTool {
195
+ pub tool: Tool,
196
+ pub enforcement: Enforcement,
197
+ }
198
+
199
+ /// `resolve_permissions` 的最小 typed 入参(替 Python `agent: dict[str, Any]`)。
200
+ ///
201
+ /// `tools = Some(..)` 对应显式 `agent["tools"]`;`None`(或 Python 的空 list →
202
+ /// `or` falsy)对应回退 `default_tools_for_role(role)`。
203
+ #[derive(Debug, Clone)]
204
+ pub struct AgentPermissionInput {
205
+ pub id: Option<AgentId>,
206
+ pub provider: Provider,
207
+ pub role: Option<String>,
208
+ /// 显式工具/别名串;`None` → 按 role 取默认。
209
+ pub tools: Option<Vec<String>>,
210
+ }
211
+
212
+ /// `resolve_permissions` 的结果(对应 Python 返回 dict)。
213
+ #[derive(Debug, Clone, PartialEq, Eq)]
214
+ pub struct ResolvedPermissions {
215
+ pub agent_id: Option<AgentId>,
216
+ pub provider: Provider,
217
+ /// 展开+去重的工具集(`BTreeSet` 序;字节对齐用 `sorted_tool_strings`)。
218
+ pub tools: BTreeSet<Tool>,
219
+ pub resolved_tools: Vec<ResolvedTool>,
220
+ pub has_prompt_only: bool,
221
+ }
222
+
223
+ impl ResolvedPermissions {
224
+ /// 工具名按**字符串字母序**(= Python `tools` 数组顺序),字节对拍用。
225
+ pub fn sorted_tool_strings(&self) -> Vec<&'static str> {
226
+ let mut v: Vec<&'static str> = self.tools.iter().map(|t| t.canonical_str()).collect();
227
+ v.sort_unstable();
228
+ v
229
+ }
230
+ }
231
+
232
+ /// `resolve_permissions`(`permissions.py:88-106`)。
233
+ ///
234
+ /// Python `tools = agent.get("tools") or default_tools_for_role(role)`:空 list 在
235
+ /// Python 是 falsy → 也回退默认。这里 `Some(vec![])` 同样视作"无显式工具"回退默认。
236
+ pub fn resolve_permissions(
237
+ agent: &AgentPermissionInput,
238
+ ) -> Result<ResolvedPermissions, ModelError> {
239
+ let resolved: BTreeSet<Tool> = match &agent.tools {
240
+ Some(tools) if !tools.is_empty() => expand_tools(tools)?,
241
+ _ => {
242
+ let role = agent.role.as_deref().unwrap_or("developer");
243
+ default_tools_for_role(role)
244
+ }
245
+ };
246
+
247
+ let resolved_tools: Vec<ResolvedTool> = resolved
248
+ .iter()
249
+ .map(|&tool| ResolvedTool {
250
+ tool,
251
+ enforcement: provider_enforcement(agent.provider, tool),
252
+ })
253
+ .collect();
254
+
255
+ let has_prompt_only = resolved_tools
256
+ .iter()
257
+ .any(|e| e.enforcement == Enforcement::PromptOnly);
258
+
259
+ Ok(ResolvedPermissions {
260
+ agent_id: agent.id.clone(),
261
+ provider: agent.provider,
262
+ tools: resolved,
263
+ resolved_tools,
264
+ has_prompt_only,
265
+ })
266
+ }
267
+
268
+ /// task 类型 → 附加必需工具(`task_required_tools` `permissions.py:109-118`)。
269
+ /// 未知/缺失 type 不附加。`requires_tools` 也走 `expand_tools`(同未知串收紧规则)。
270
+ pub fn task_required_tools(
271
+ task_type: Option<&str>,
272
+ requires_tools: &[String],
273
+ ) -> Result<BTreeSet<Tool>, ModelError> {
274
+ let mut required: Vec<String> = requires_tools.to_vec();
275
+ match task_type {
276
+ Some("implementation") | Some("bug_fix") | Some("test") => {
277
+ required.push("fs_write".to_string());
278
+ required.push("execute_bash".to_string());
279
+ }
280
+ Some("review") | Some("risk_check") => {
281
+ required.push("fs_read".to_string());
282
+ required.push("git_diff".to_string());
283
+ }
284
+ Some("research") | Some("architecture") => {
285
+ required.push("fs_read".to_string());
286
+ }
287
+ _ => {}
288
+ }
289
+ expand_tools(required)
290
+ }
291
+
292
+ /// `missing_tools`(`permissions.py:121-123`):task 需要但 agent 不被允许的工具。
293
+ /// 返回**字母序**(= Python list 顺序)。
294
+ pub fn missing_tools(
295
+ agent: &AgentPermissionInput,
296
+ task_type: Option<&str>,
297
+ requires_tools: &[String],
298
+ ) -> Result<Vec<Tool>, ModelError> {
299
+ let allowed = resolve_permissions(agent)?.tools;
300
+ let required = task_required_tools(task_type, requires_tools)?;
301
+ // required 已是去重集;按字符串字母序输出,对齐 Python。
302
+ let mut missing: Vec<Tool> = required.into_iter().filter(|t| !allowed.contains(t)).collect();
303
+ missing.sort_unstable_by_key(|t| t.canonical_str());
304
+ Ok(missing)
305
+ }
306
+
307
+ #[cfg(test)]
308
+ mod tests {
309
+ #![allow(clippy::unwrap_used)]
310
+ use super::*;
311
+
312
+ /// `BTreeSet<Tool>` → 字母序串(对齐 Python `sorted(...)` list)。
313
+ fn strs(set: &BTreeSet<Tool>) -> Vec<&'static str> {
314
+ let mut v: Vec<&'static str> = set.iter().map(|t| t.canonical_str()).collect();
315
+ v.sort_unstable();
316
+ v
317
+ }
318
+
319
+ fn agent(provider: Provider, role: Option<&str>, tools: Option<&[&str]>) -> AgentPermissionInput {
320
+ AgentPermissionInput {
321
+ id: Some(AgentId::new("x")),
322
+ provider,
323
+ role: role.map(str::to_string),
324
+ tools: tools.map(|t| t.iter().map(|s| s.to_string()).collect()),
325
+ }
326
+ }
327
+
328
+ // ---- expand_tools 行为对拍(golden from team-agent-public@439bef8) ----
329
+ #[test]
330
+ fn expand_tools_matches_python_golden() {
331
+ // E1
332
+ assert_eq!(strs(&expand_tools(["fs_*"]).unwrap()), ["fs_list", "fs_read", "fs_write"]);
333
+ // E2
334
+ assert_eq!(strs(&expand_tools(["@builtin"]).unwrap()), ["provider_builtin"]);
335
+ // E3
336
+ assert_eq!(
337
+ strs(&expand_tools(["*"]).unwrap()),
338
+ [
339
+ "execute_bash",
340
+ "fs_list",
341
+ "fs_read",
342
+ "fs_write",
343
+ "git_diff",
344
+ "mcp_team",
345
+ "network",
346
+ "provider_builtin"
347
+ ]
348
+ );
349
+ // E4
350
+ assert_eq!(
351
+ strs(&expand_tools(["@team-orchestrator", "@cao-mcp-server"]).unwrap()),
352
+ ["mcp_team"]
353
+ );
354
+ // E5 — 去重
355
+ assert_eq!(
356
+ strs(&expand_tools(["fs_read", "fs_read", "network"]).unwrap()),
357
+ ["fs_read", "network"]
358
+ );
359
+ // E6
360
+ assert!(expand_tools(Vec::<String>::new()).unwrap().is_empty());
361
+ // E7
362
+ assert_eq!(
363
+ strs(&expand_tools(["fs_*", "@builtin", "network"]).unwrap()),
364
+ ["fs_list", "fs_read", "fs_write", "network", "provider_builtin"]
365
+ );
366
+ // E8 — * 已含 fs_read,去重无变化
367
+ assert_eq!(
368
+ strs(&expand_tools(["*", "fs_read"]).unwrap()),
369
+ [
370
+ "execute_bash",
371
+ "fs_list",
372
+ "fs_read",
373
+ "fs_write",
374
+ "git_diff",
375
+ "mcp_team",
376
+ "network",
377
+ "provider_builtin"
378
+ ]
379
+ );
380
+ }
381
+
382
+ /// §3 刻意收紧:未知工具串 → Validation(Python 是 passthrough,见模块头注)。
383
+ #[test]
384
+ fn expand_tools_rejects_unknown_string() {
385
+ let err = expand_tools(["banana"]).unwrap_err();
386
+ assert!(matches!(err, ModelError::Validation(_)));
387
+ // 别名/规范名混入未知串也整体失败。
388
+ assert!(expand_tools(["fs_read", "fs_*", "banana"]).is_err());
389
+ }
390
+
391
+ // ---- default_tools_for_role 行为对拍 ----
392
+ #[test]
393
+ fn default_tools_for_role_matches_python_golden() {
394
+ assert_eq!(
395
+ strs(&default_tools_for_role("leader")),
396
+ ["fs_list", "fs_read", "mcp_team", "provider_builtin"]
397
+ );
398
+ assert_eq!(
399
+ strs(&default_tools_for_role("supervisor")),
400
+ ["fs_list", "fs_read", "mcp_team", "provider_builtin"]
401
+ );
402
+ assert_eq!(
403
+ strs(&default_tools_for_role("researcher")),
404
+ ["fs_list", "fs_read", "mcp_team", "network", "provider_builtin"]
405
+ );
406
+ assert_eq!(
407
+ strs(&default_tools_for_role("reviewer")),
408
+ ["fs_list", "fs_read", "git_diff", "mcp_team", "provider_builtin"]
409
+ );
410
+ assert_eq!(
411
+ strs(&default_tools_for_role("code_reviewer")),
412
+ ["fs_list", "fs_read", "git_diff", "mcp_team", "provider_builtin"]
413
+ );
414
+ let dev = [
415
+ "execute_bash",
416
+ "fs_list",
417
+ "fs_read",
418
+ "fs_write",
419
+ "git_diff",
420
+ "mcp_team",
421
+ "provider_builtin",
422
+ ];
423
+ assert_eq!(strs(&default_tools_for_role("developer")), dev);
424
+ assert_eq!(strs(&default_tools_for_role("implementation_engineer")), dev);
425
+ // 未知 role → developer 集。
426
+ assert_eq!(strs(&default_tools_for_role("qa")), dev);
427
+ }
428
+
429
+ // ---- provider_enforcement 行为对拍(矩阵逐格) ----
430
+ #[test]
431
+ fn provider_enforcement_matches_python_matrix() {
432
+ use Enforcement::{Hard, PromptOnly};
433
+ // claude_code: network=prompt_only,其余 hard。
434
+ assert_eq!(provider_enforcement(Provider::ClaudeCode, Tool::Network), PromptOnly);
435
+ for t in [Tool::FsRead, Tool::FsWrite, Tool::FsList, Tool::ExecuteBash, Tool::GitDiff, Tool::McpTeam, Tool::ProviderBuiltin] {
436
+ assert_eq!(provider_enforcement(Provider::ClaudeCode, t), Hard);
437
+ }
438
+ // gemini_cli: network + mcp_team = prompt_only,其余 hard。
439
+ assert_eq!(provider_enforcement(Provider::GeminiCli, Tool::Network), PromptOnly);
440
+ assert_eq!(provider_enforcement(Provider::GeminiCli, Tool::McpTeam), PromptOnly);
441
+ for t in [Tool::FsRead, Tool::FsWrite, Tool::FsList, Tool::ExecuteBash, Tool::GitDiff, Tool::ProviderBuiltin] {
442
+ assert_eq!(provider_enforcement(Provider::GeminiCli, t), Hard);
443
+ }
444
+ // codex: 全 prompt_only。fake: 全 hard。
445
+ for t in [Tool::FsRead, Tool::Network, Tool::McpTeam, Tool::ProviderBuiltin] {
446
+ assert_eq!(provider_enforcement(Provider::Codex, t), PromptOnly);
447
+ assert_eq!(provider_enforcement(Provider::Fake, t), Hard);
448
+ // claude 不在表中 → fallback prompt_only。
449
+ assert_eq!(provider_enforcement(Provider::Claude, t), PromptOnly);
450
+ }
451
+ }
452
+
453
+ // ---- resolve_permissions 行为对拍(golden) ----
454
+ #[test]
455
+ fn resolve_permissions_leader_claude_code() {
456
+ // R-leader-cc
457
+ let r = resolve_permissions(&agent(Provider::ClaudeCode, Some("leader"), None)).unwrap();
458
+ assert_eq!(r.sorted_tool_strings(), ["fs_list", "fs_read", "mcp_team", "provider_builtin"]);
459
+ assert!(!r.has_prompt_only);
460
+ for e in &r.resolved_tools {
461
+ assert_eq!(e.enforcement, Enforcement::Hard);
462
+ }
463
+ }
464
+
465
+ #[test]
466
+ fn resolve_permissions_developer_codex_all_prompt_only() {
467
+ // R-dev-codex
468
+ let r = resolve_permissions(&agent(Provider::Codex, Some("developer"), None)).unwrap();
469
+ assert_eq!(
470
+ r.sorted_tool_strings(),
471
+ ["execute_bash", "fs_list", "fs_read", "fs_write", "git_diff", "mcp_team", "provider_builtin"]
472
+ );
473
+ assert!(r.has_prompt_only);
474
+ for e in &r.resolved_tools {
475
+ assert_eq!(e.enforcement, Enforcement::PromptOnly);
476
+ }
477
+ }
478
+
479
+ #[test]
480
+ fn resolve_permissions_impl_gemini_mcp_team_prompt_only() {
481
+ // R-impl-gem:mcp_team=prompt_only,其余 hard → has_prompt_only=true。
482
+ let r = resolve_permissions(&agent(Provider::GeminiCli, Some("implementation_engineer"), None)).unwrap();
483
+ assert!(r.has_prompt_only);
484
+ let mcp = r.resolved_tools.iter().find(|e| e.tool == Tool::McpTeam).unwrap();
485
+ assert_eq!(mcp.enforcement, Enforcement::PromptOnly);
486
+ let gd = r.resolved_tools.iter().find(|e| e.tool == Tool::GitDiff).unwrap();
487
+ assert_eq!(gd.enforcement, Enforcement::Hard);
488
+ }
489
+
490
+ #[test]
491
+ fn resolve_permissions_explicit_tools_and_norole_and_unknownprov() {
492
+ // R-explicit:显式 tools 走 expand_tools,provider=claude_code。
493
+ let r = resolve_permissions(&agent(Provider::ClaudeCode, None, Some(&["fs_*", "network"]))).unwrap();
494
+ assert_eq!(r.sorted_tool_strings(), ["fs_list", "fs_read", "fs_write", "network"]);
495
+ assert!(r.has_prompt_only); // network=prompt_only
496
+
497
+ // R-norole:无 role → developer 默认,全 hard(claude_code)。
498
+ let r2 = resolve_permissions(&AgentPermissionInput {
499
+ id: Some(AgentId::new("a6")),
500
+ provider: Provider::ClaudeCode,
501
+ role: None,
502
+ tools: None,
503
+ })
504
+ .unwrap();
505
+ assert_eq!(
506
+ r2.sorted_tool_strings(),
507
+ ["execute_bash", "fs_list", "fs_read", "fs_write", "git_diff", "mcp_team", "provider_builtin"]
508
+ );
509
+ assert!(!r2.has_prompt_only);
510
+
511
+ // R-unknownprov:provider=claude 不在矩阵 → 全 prompt_only。
512
+ let r3 = resolve_permissions(&agent(Provider::Claude, Some("developer"), None)).unwrap();
513
+ assert!(r3.has_prompt_only);
514
+ for e in &r3.resolved_tools {
515
+ assert_eq!(e.enforcement, Enforcement::PromptOnly);
516
+ }
517
+
518
+ // agent_id passthrough。
519
+ assert_eq!(r.agent_id, Some(AgentId::new("x")));
520
+ }
521
+
522
+ #[test]
523
+ fn resolve_permissions_empty_tools_falls_back_to_role_default() {
524
+ // Python: agent.get("tools") or default → 空 list falsy → 用 role 默认。
525
+ let r = resolve_permissions(&agent(Provider::ClaudeCode, Some("leader"), Some(&[]))).unwrap();
526
+ assert_eq!(r.sorted_tool_strings(), ["fs_list", "fs_read", "mcp_team", "provider_builtin"]);
527
+ }
528
+
529
+ // ---- task_required_tools 行为对拍(golden) ----
530
+ #[test]
531
+ fn task_required_tools_matches_python_golden() {
532
+ // T1
533
+ assert_eq!(strs(&task_required_tools(Some("implementation"), &[]).unwrap()), ["execute_bash", "fs_write"]);
534
+ // T2
535
+ assert_eq!(strs(&task_required_tools(Some("review"), &[]).unwrap()), ["fs_read", "git_diff"]);
536
+ // T3
537
+ assert_eq!(strs(&task_required_tools(Some("research"), &[]).unwrap()), ["fs_read"]);
538
+ // T4
539
+ assert_eq!(
540
+ strs(&task_required_tools(Some("bug_fix"), &["network".to_string()]).unwrap()),
541
+ ["execute_bash", "fs_write", "network"]
542
+ );
543
+ // T5 未知 type
544
+ assert!(task_required_tools(Some("unknown_type"), &[]).unwrap().is_empty());
545
+ // T6 无 type
546
+ assert!(task_required_tools(None, &[]).unwrap().is_empty());
547
+ // T7 requires_tools 走 expand_tools(fs_* 展开)
548
+ assert_eq!(
549
+ strs(&task_required_tools(Some("architecture"), &["fs_*".to_string()]).unwrap()),
550
+ ["fs_list", "fs_read", "fs_write"]
551
+ );
552
+ }
553
+
554
+ // ---- missing_tools 行为对拍(golden) ----
555
+ #[test]
556
+ fn missing_tools_matches_python_golden() {
557
+ // M1:reviewer(claude_code)缺 implementation 所需 fs_write/execute_bash。
558
+ let m1 = missing_tools(&agent(Provider::ClaudeCode, Some("reviewer"), None), Some("implementation"), &[]).unwrap();
559
+ assert_eq!(m1, [Tool::ExecuteBash, Tool::FsWrite]);
560
+ // M2:developer 不缺。
561
+ let m2 = missing_tools(&agent(Provider::ClaudeCode, Some("developer"), None), Some("implementation"), &[]).unwrap();
562
+ assert!(m2.is_empty());
563
+ // M3:researcher 缺 git_diff(review 需要),但有 fs_read。
564
+ let m3 = missing_tools(&agent(Provider::ClaudeCode, Some("researcher"), None), Some("review"), &[]).unwrap();
565
+ assert_eq!(m3, [Tool::GitDiff]);
566
+ }
567
+ }