@team-agent/installer 0.5.65 → 0.5.68

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 (208) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +6 -0
  4. package/crates/team-agent/src/cli/adapters.rs +6 -1
  5. package/crates/team-agent/src/cli/diagnose.rs +82 -0
  6. package/crates/team-agent/src/cli/emit.rs +3 -3
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +21 -7
  9. package/crates/team-agent/src/cli/leaders.rs +125 -1
  10. package/crates/team-agent/src/cli/mod.rs +20 -0
  11. package/crates/team-agent/src/cli/send/presentation.rs +7 -1
  12. package/crates/team-agent/src/cli/spec.rs +2 -0
  13. package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
  14. package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
  15. package/crates/team-agent/src/cli/tests/base.rs +26 -3
  16. package/crates/team-agent/src/cli/tests/compile.rs +1 -1
  17. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  18. package/crates/team-agent/src/cli/tests/mod.rs +2 -2
  19. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
  20. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  21. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  22. package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
  23. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  24. package/crates/team-agent/src/compiler/tests.rs +23 -20
  25. package/crates/team-agent/src/compiler.rs +84 -5
  26. package/crates/team-agent/src/conpty/backend.rs +16 -0
  27. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  28. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  29. package/crates/team-agent/src/coordinator/health.rs +173 -0
  30. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  31. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  32. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  33. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  34. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  35. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  36. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  37. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  38. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  39. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  40. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  41. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  42. package/crates/team-agent/src/coordinator/types.rs +49 -0
  43. package/crates/team-agent/src/db/message_store.rs +39 -5
  44. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  45. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  46. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  47. package/crates/team-agent/src/leader/registry.rs +65 -0
  48. package/crates/team-agent/src/leader/start.rs +1080 -58
  49. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
  50. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  51. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  52. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -14
  53. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +69 -2
  54. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +196 -2
  55. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  56. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  57. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  58. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -457
  59. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  60. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  61. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  62. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  63. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  64. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  65. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  66. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  67. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  68. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  69. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  70. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  71. package/crates/team-agent/src/lifecycle/launch/spawn.rs +101 -4
  72. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +244 -48
  73. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -2
  74. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  75. package/crates/team-agent/src/lifecycle/launch.rs +209 -43
  76. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  77. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  78. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  79. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  80. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  81. package/crates/team-agent/src/lifecycle/restart/agent.rs +213 -3
  82. package/crates/team-agent/src/lifecycle/restart/common.rs +373 -18
  83. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  84. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  85. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -15
  86. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  87. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  88. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  89. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  90. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
  91. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +3 -4
  92. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
  93. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
  95. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  96. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +159 -174
  97. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +15 -13
  99. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -18
  100. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
  101. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  102. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  103. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  104. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
  105. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
  106. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  107. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  108. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  109. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  110. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  111. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  112. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
  113. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
  114. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +65 -40
  115. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -32
  116. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +17 -9
  117. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  118. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +24 -23
  119. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +98 -62
  120. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
  121. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
  122. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
  123. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -1
  124. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
  125. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
  126. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
  127. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +41 -8
  128. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
  129. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
  130. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
  131. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
  132. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
  133. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
  134. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
  135. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +60 -2
  136. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
  137. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
  138. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  139. package/crates/team-agent/src/lifecycle/tests.rs +23 -15
  140. package/crates/team-agent/src/lifecycle/types.rs +61 -2
  141. package/crates/team-agent/src/lifecycle/worker_command_context.rs +200 -33
  142. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  143. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  144. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  145. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  146. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  147. package/crates/team-agent/src/messaging/results.rs +4 -0
  148. package/crates/team-agent/src/messaging/send.rs +34 -0
  149. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  150. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  151. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  152. package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
  153. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  154. package/crates/team-agent/src/messaging/types.rs +4 -1
  155. package/crates/team-agent/src/model/enums.rs +14 -5
  156. package/crates/team-agent/src/model/permissions.rs +3 -0
  157. package/crates/team-agent/src/model/spec.rs +129 -6
  158. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
  159. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
  160. package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
  161. package/crates/team-agent/src/os_probe.rs +73 -2
  162. package/crates/team-agent/src/provider/adapter.rs +231 -4
  163. package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
  164. package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
  165. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
  166. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  167. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  168. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  169. package/crates/team-agent/src/provider/bypass_flags.rs +124 -0
  170. package/crates/team-agent/src/provider/classify.rs +4 -2
  171. package/crates/team-agent/src/provider/faults.rs +2 -1
  172. package/crates/team-agent/src/provider/mod.rs +13 -0
  173. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  174. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  175. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  176. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  177. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  178. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  179. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  180. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  181. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  182. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  183. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  184. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  185. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  186. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  187. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  188. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  189. package/crates/team-agent/src/provider/types.rs +4 -2
  190. package/crates/team-agent/src/provider/wire.rs +23 -1
  191. package/crates/team-agent/src/state/persist.rs +301 -8
  192. package/crates/team-agent/src/state/repository.rs +5 -0
  193. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  194. package/crates/team-agent/src/tmux_backend.rs +1083 -75
  195. package/crates/team-agent/src/transport/test_support.rs +20 -0
  196. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  197. package/crates/team-agent/src/transport.rs +265 -1
  198. package/npm/install.mjs +129 -73
  199. package/package.json +4 -4
  200. package/skills/team-agent/SKILL.md +33 -238
  201. package/skills/team-agent/command-coverage.json +31 -0
  202. package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
  203. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  204. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -483
  205. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  206. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
  207. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
  208. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
@@ -0,0 +1,784 @@
1
+ //! ---
2
+ //! purpose: g1 五个静默面到达性回归——clone 后 spawn/MCP 不得再夹工具
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: g1_face1_resolve_permissions
6
+ //! what: clone 后 worker_command_context 不得按 leader 三件套再截 argv
7
+ //! - name: g1_face2_disallowed_tools
8
+ //! what: Claude --disallowedTools 不得把源席已声明工具再禁回去
9
+ //! - name: g1_face3_mcp_tool_table
10
+ //! what: grok/claude/cursor 各自注入面仍按源席全集映射,不得只剩只读
11
+ //! - name: g1_face4_mcp_clone_entry
12
+ //! what: MCP clone_agent 与 CLI clone-agent 同一 tools 断言
13
+ //! - name: g1_face5_narrow_source_stays_narrow
14
+ //! what: 源席故意三件套时分身仍是三件,不得扩成 developer 默认
15
+ //! boundary:
16
+ //! - 不改 clone_agent.rs / add-agent / approval / tools 语义
17
+ //! - 不发明 --keep-tools;不把 cursor 未验证的 --allowed-tools 写进产物
18
+ //! maturity: wired
19
+ //! ---
20
+
21
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
22
+
23
+ use std::collections::BTreeSet;
24
+ use std::path::{Path, PathBuf};
25
+ use std::process::Output;
26
+
27
+ use serial_test::serial;
28
+
29
+ #[path = "../../../tests/support/hermetic.rs"]
30
+ mod hermetic_guard;
31
+ use hermetic_guard::HermeticTestEnv;
32
+
33
+ use crate::lifecycle::worker_command_context::{
34
+ resolved_tool_strings_for_command, WorkerCommandAgent,
35
+ };
36
+ use crate::lifecycle::{apply_cursor_mcp_overlay, apply_grok_mcp_overlay};
37
+ use crate::mcp_server::TeamOrchestratorTools;
38
+ use crate::model::enums::{AuthMode, Provider};
39
+ use crate::model::ids::{AgentId, TeamKey};
40
+ use crate::model::permissions::{resolve_permissions, AgentPermissionInput};
41
+ use crate::model::yaml::Value;
42
+ use crate::provider::adapters::claude::claude_disallowed_tools;
43
+ use crate::provider::adapters::grok::grok_disallowed_tools;
44
+ use crate::provider::{get_adapter, McpConfig, ProviderCommandContext};
45
+ use crate::state::persist::load_runtime_state;
46
+
47
+ const TEAM_NAME: &str = "g1sf";
48
+ const SOURCE: &str = "src_worker";
49
+ const CLONE: &str = "new_worker";
50
+ const NARROW: &str = "narrow_src";
51
+ const NARROW_CLONE: &str = "narrow_clone";
52
+
53
+ const SIX: &[&str] = &[
54
+ "execute_bash",
55
+ "fs_list",
56
+ "fs_read",
57
+ "fs_write",
58
+ "mcp_team",
59
+ "provider_builtin",
60
+ ];
61
+ const THREE: &[&str] = &["fs_list", "fs_read", "mcp_team"];
62
+ const WRITE_NATIVE: &[&str] = &["Bash", "Edit", "Write", "MultiEdit", "NotebookEdit"];
63
+
64
+ struct Case {
65
+ env: HermeticTestEnv,
66
+ workspace: PathBuf,
67
+ }
68
+
69
+ impl Case {
70
+ fn start() -> Self {
71
+ ensure_team_agent_cli();
72
+ let env = HermeticTestEnv::enter("g1sf");
73
+ let workspace = env.workspace("ws");
74
+ write_team_docs(&workspace);
75
+ Self { env, workspace }
76
+ }
77
+
78
+ fn ws(&self) -> &str {
79
+ self.workspace.to_str().expect("ws utf8")
80
+ }
81
+
82
+ fn run(&self, args: &[&str]) -> Output {
83
+ self.env.run_cli(&self.workspace, args)
84
+ }
85
+
86
+ fn shutdown(&self) {
87
+ drop(self.run(&[
88
+ "shutdown",
89
+ "--workspace",
90
+ self.ws(),
91
+ "--team",
92
+ TEAM_NAME,
93
+ "--json",
94
+ ]));
95
+ }
96
+ }
97
+
98
+ fn ensure_team_agent_cli() {
99
+ if let Ok(path) = std::env::var("CARGO_BIN_EXE_team-agent") {
100
+ assert!(
101
+ std::path::Path::new(&path).is_file(),
102
+ "CARGO_BIN_EXE_team-agent missing: {path}"
103
+ );
104
+ return;
105
+ }
106
+ let bin = std::env::current_exe()
107
+ .expect("test exe")
108
+ .parent()
109
+ .and_then(|deps| deps.parent())
110
+ .map(|target| target.join("team-agent"))
111
+ .expect("target/debug/team-agent");
112
+ if bin.is_file() {
113
+ return;
114
+ }
115
+ let manifest = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
116
+ let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
117
+ let status = std::process::Command::new(&cargo)
118
+ .args([
119
+ "build",
120
+ "-p",
121
+ "team-agent",
122
+ "--bin",
123
+ "team-agent",
124
+ "--manifest-path",
125
+ ])
126
+ .arg(std::path::Path::new(&manifest).join("Cargo.toml"))
127
+ .status()
128
+ .unwrap_or_else(|e| panic!("spawn {cargo} build: {e}"));
129
+ assert!(
130
+ status.success(),
131
+ "{cargo} build -p team-agent --bin team-agent failed: {status}"
132
+ );
133
+ }
134
+
135
+ fn write_team_docs(workspace: &Path) {
136
+ std::fs::create_dir_all(workspace.join("agents")).expect("agents dir");
137
+ std::fs::write(
138
+ workspace.join("TEAM.md"),
139
+ format!(
140
+ "---\nname: {TEAM_NAME}\nobjective: g1 silent faces\nprovider: fake\ndisplay_backend: none\n---\n"
141
+ ),
142
+ )
143
+ .expect("TEAM.md");
144
+ write_role(workspace, SOURCE, SIX);
145
+ write_role(workspace, NARROW, THREE);
146
+ }
147
+
148
+ fn write_role(workspace: &Path, name: &str, tools: &[&str]) {
149
+ let tools_yaml = tools
150
+ .iter()
151
+ .map(|tool| format!(" - {tool}"))
152
+ .collect::<Vec<_>>()
153
+ .join("\n");
154
+ std::fs::write(
155
+ workspace.join("agents").join(format!("{name}.md")),
156
+ format!(
157
+ "---\nname: {name}\nrole: {name}\nprovider: fake\nmodel: fake\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n{tools_yaml}\n---\n\n{name} body.\n"
158
+ ),
159
+ )
160
+ .expect("role doc");
161
+ }
162
+
163
+ fn all_runtime_specs(workspace: &Path) -> Vec<(std::path::PathBuf, Value)> {
164
+ let runtime = workspace.join(".team").join("runtime");
165
+ let mut parsed = Vec::new();
166
+ if let Ok(entries) = std::fs::read_dir(&runtime) {
167
+ for entry in entries.flatten() {
168
+ let path = entry.path().join("team.spec.yaml");
169
+ if path.is_file() {
170
+ let value =
171
+ crate::model::yaml::loads(&std::fs::read_to_string(&path).expect("spec"))
172
+ .expect("parse spec");
173
+ parsed.push((path, value));
174
+ }
175
+ }
176
+ }
177
+ parsed
178
+ }
179
+
180
+ fn load_runtime_spec(workspace: &Path) -> Value {
181
+ let parsed = all_runtime_specs(workspace);
182
+ assert!(
183
+ !parsed.is_empty(),
184
+ "runtime spec missing under {}",
185
+ workspace.join(".team").join("runtime").display()
186
+ );
187
+ parsed
188
+ .into_iter()
189
+ .max_by_key(|(_, value)| spec_agent_ids(value).len())
190
+ .expect("runtime spec")
191
+ .1
192
+ }
193
+
194
+ fn spec_agent_ids(spec: &Value) -> Vec<String> {
195
+ spec.get("agents")
196
+ .and_then(Value::as_list)
197
+ .unwrap_or(&[])
198
+ .iter()
199
+ .filter_map(|row| row.get("id").and_then(Value::as_str).map(str::to_string))
200
+ .collect()
201
+ }
202
+
203
+ fn runtime_team_key(workspace: &Path) -> String {
204
+ let runtime = workspace.join(".team").join("runtime");
205
+ if runtime.join(TEAM_NAME).join("team.spec.yaml").is_file() {
206
+ return TEAM_NAME.to_string();
207
+ }
208
+ std::fs::read_dir(&runtime)
209
+ .expect("runtime dir")
210
+ .flatten()
211
+ .find(|entry| entry.path().join("team.spec.yaml").is_file())
212
+ .map(|entry| entry.file_name().to_string_lossy().into_owned())
213
+ .unwrap_or_else(|| panic!("runtime team key missing"))
214
+ }
215
+
216
+ fn yaml_tool_set(node: &Value) -> BTreeSet<String> {
217
+ node.get("tools")
218
+ .and_then(Value::as_list)
219
+ .unwrap_or_else(|| panic!("tools list missing"))
220
+ .iter()
221
+ .filter_map(Value::as_str)
222
+ .map(str::to_string)
223
+ .collect()
224
+ }
225
+
226
+ fn spec_agent<'a>(spec: &'a Value, agent: &str) -> &'a Value {
227
+ let agents = spec
228
+ .get("agents")
229
+ .and_then(Value::as_list)
230
+ .expect("spec agents");
231
+ for row in agents {
232
+ if row.get("id").and_then(Value::as_str) == Some(agent) {
233
+ return row;
234
+ }
235
+ }
236
+ panic!(
237
+ "agent {agent} missing from runtime spec; have {:?}",
238
+ spec_agent_ids(spec)
239
+ );
240
+ }
241
+
242
+ fn spec_tools(workspace: &Path, agent: &str) -> BTreeSet<String> {
243
+ for (_, spec) in all_runtime_specs(workspace) {
244
+ if spec_agent_ids(&spec).iter().any(|id| id == agent) {
245
+ return yaml_tool_set(spec_agent(&spec, agent));
246
+ }
247
+ }
248
+ let inventory: Vec<(String, Vec<String>)> = all_runtime_specs(workspace)
249
+ .into_iter()
250
+ .map(|(path, spec)| (path.display().to_string(), spec_agent_ids(&spec)))
251
+ .collect();
252
+ panic!("agent {agent} missing from every runtime spec; inventory={inventory:?}");
253
+ }
254
+
255
+ fn leader_spec_tools(workspace: &Path) -> BTreeSet<String> {
256
+ yaml_tool_set(
257
+ load_runtime_spec(workspace)
258
+ .get("leader")
259
+ .expect("spec leader"),
260
+ )
261
+ }
262
+
263
+ fn set_of(items: &[&str]) -> BTreeSet<String> {
264
+ items.iter().map(|s| (*s).to_string()).collect()
265
+ }
266
+
267
+ fn role_tools(path: &Path) -> BTreeSet<String> {
268
+ let (meta, _) = crate::compiler::read_front_matter(path)
269
+ .unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
270
+ yaml_tool_set(&meta)
271
+ }
272
+
273
+ fn clone_ok(case: &Case, source: &str, dest: &str) {
274
+ let out = case.run(&[
275
+ "clone-agent",
276
+ source,
277
+ "--as",
278
+ dest,
279
+ "--workspace",
280
+ case.ws(),
281
+ "--team",
282
+ TEAM_NAME,
283
+ "--no-display",
284
+ "--json",
285
+ ]);
286
+ let stderr = String::from_utf8_lossy(&out.stderr);
287
+ let stdout = String::from_utf8_lossy(&out.stdout);
288
+ assert!(
289
+ out.status.success(),
290
+ "clone-agent {source} --as {dest} must exit 0; code={:?} stderr={stderr} stdout={stdout}",
291
+ out.status.code()
292
+ );
293
+ }
294
+
295
+ fn quick_start(case: &Case) {
296
+ let qs = case.run(&[
297
+ "quick-start",
298
+ case.ws(),
299
+ "--workspace",
300
+ case.ws(),
301
+ "--name",
302
+ TEAM_NAME,
303
+ "--yes",
304
+ "--json",
305
+ ]);
306
+ let spec_ok = case
307
+ .workspace
308
+ .join(".team")
309
+ .join("runtime")
310
+ .read_dir()
311
+ .ok()
312
+ .and_then(|entries| {
313
+ entries
314
+ .flatten()
315
+ .find(|e| e.path().join("team.spec.yaml").is_file())
316
+ });
317
+ assert!(
318
+ spec_ok.is_some(),
319
+ "quick-start must leave a runtime spec; stderr={} stdout={}",
320
+ String::from_utf8_lossy(&qs.stderr),
321
+ String::from_utf8_lossy(&qs.stdout)
322
+ );
323
+ assert_eq!(
324
+ spec_tools(&case.workspace, SOURCE),
325
+ set_of(SIX),
326
+ "fixture source must be the six-set"
327
+ );
328
+ assert_eq!(
329
+ leader_spec_tools(&case.workspace),
330
+ set_of(THREE),
331
+ "leader must stay the three-set so the conflict surface exists"
332
+ );
333
+ }
334
+
335
+ fn boot_six_clone() -> Case {
336
+ let case = Case::start();
337
+ quick_start(&case);
338
+ clone_ok(&case, SOURCE, CLONE);
339
+ case
340
+ }
341
+
342
+ fn command_tools(agent_row: &Value, id: &str, provider: Provider) -> Vec<String> {
343
+ let command_agent = WorkerCommandAgent::from_yaml(agent_row, Some(id), provider)
344
+ .unwrap_or_else(|e| panic!("WorkerCommandAgent::from_yaml {id}: {e}"));
345
+ resolved_tool_strings_for_command(&command_agent, provider)
346
+ .unwrap_or_else(|e| panic!("resolved_tool_strings_for_command {id}: {e}"))
347
+ }
348
+
349
+ fn permissions_tools_of(row: &serde_json::Value) -> Option<BTreeSet<String>> {
350
+ row.get("permissions")
351
+ .and_then(|permissions| permissions.get("tools"))
352
+ .and_then(|tools| tools.as_array())
353
+ .map(|tools| {
354
+ tools
355
+ .iter()
356
+ .filter_map(|v| v.as_str().map(str::to_string))
357
+ .collect()
358
+ })
359
+ }
360
+
361
+ fn collect_json_tools(value: &serde_json::Value, agent: &str) -> Option<BTreeSet<String>> {
362
+ if let Some(row) = value.pointer(&format!("/agents/{agent}")) {
363
+ if let Some(tools) = permissions_tools_of(row) {
364
+ return Some(tools);
365
+ }
366
+ }
367
+ if let Some(teams) = value.get("teams").and_then(|v| v.as_object()) {
368
+ for team in teams.values() {
369
+ if let Some(found) = collect_json_tools(team, agent) {
370
+ return Some(found);
371
+ }
372
+ }
373
+ }
374
+ None
375
+ }
376
+
377
+ fn flag_values<'a>(argv: &'a [String], flag: &str) -> Vec<&'a str> {
378
+ let mut out = Vec::new();
379
+ let mut i = 0;
380
+ while i < argv.len() {
381
+ if argv[i] == flag {
382
+ if let Some(value) = argv.get(i + 1) {
383
+ out.push(value.as_str());
384
+ i += 2;
385
+ continue;
386
+ }
387
+ }
388
+ i += 1;
389
+ }
390
+ out
391
+ }
392
+
393
+ fn assert_write_tools_not_disallowed(surface: &str, disallowed: &[&str]) {
394
+ let banned: Vec<&str> = disallowed
395
+ .iter()
396
+ .copied()
397
+ .filter(|name| WRITE_NATIVE.contains(name))
398
+ .collect();
399
+ assert!(
400
+ banned.is_empty(),
401
+ "{surface}: six-set clone must not re-ban write/exec native tools; banned={banned:?} disallowed={disallowed:?}"
402
+ );
403
+ }
404
+
405
+ fn overlay_config(agent_id: &str, workspace: &str) -> McpConfig {
406
+ McpConfig {
407
+ raw: serde_json::json!({
408
+ "team_orchestrator": {
409
+ "command": "/bin/team-agent-test",
410
+ "args": ["mcp-server", "--workspace", workspace],
411
+ "env": {
412
+ "TEAM_AGENT_ID": agent_id,
413
+ "TEAM_AGENT_WORKSPACE": workspace,
414
+ "TEAM_AGENT_OWNER_TEAM_ID": TEAM_NAME,
415
+ "TEAM_AGENT_AUTH_MODE": "subscription",
416
+ }
417
+ }
418
+ }),
419
+ }
420
+ }
421
+
422
+ fn assert_provider_mcp_table(case: &Case, provider: Provider, clone_id: &str) {
423
+ let spec = load_runtime_spec(&case.workspace);
424
+ let row = spec_agent(&spec, clone_id);
425
+ let declared = yaml_tool_set(row);
426
+ assert_eq!(
427
+ declared,
428
+ set_of(SIX),
429
+ "clone spec tools must still be six-set before provider mapping"
430
+ );
431
+
432
+ let resolved = command_tools(row, clone_id, provider);
433
+ let resolved_set: BTreeSet<String> = resolved.iter().cloned().collect();
434
+ assert_eq!(
435
+ resolved_set,
436
+ set_of(SIX),
437
+ "{provider:?} resolve_permissions on clone must equal source six-set, not leader three-set; got={resolved:?}"
438
+ );
439
+ assert_ne!(
440
+ resolved_set,
441
+ set_of(THREE),
442
+ "{provider:?} clone tools collapsed onto the leader ceiling"
443
+ );
444
+
445
+ let tool_refs: Vec<&str> = resolved.iter().map(String::as_str).collect();
446
+ match provider {
447
+ Provider::Claude | Provider::ClaudeCode => {
448
+ let disallowed = claude_disallowed_tools(&tool_refs);
449
+ assert_write_tools_not_disallowed("claude disallowedTools", &disallowed);
450
+ }
451
+ Provider::Grok => {
452
+ let disallowed = grok_disallowed_tools(&tool_refs);
453
+ assert_write_tools_not_disallowed("grok disallowedTools", &disallowed);
454
+ }
455
+ Provider::CursorAgent => {}
456
+ other => panic!("face3 unexpected provider {other:?}"),
457
+ }
458
+
459
+ let adapter = get_adapter(provider);
460
+ let mcp = adapter
461
+ .mcp_config(AuthMode::Subscription)
462
+ .unwrap_or_else(|e| panic!("{provider:?} mcp_config: {e}"));
463
+ let grok_model = (provider == Provider::Grok).then_some("grok-4.6");
464
+ let plan = adapter
465
+ .build_command_plan(ProviderCommandContext {
466
+ auth_mode: AuthMode::Subscription,
467
+ mcp_config: Some(&mcp),
468
+ system_prompt: Some("g1 silent face"),
469
+ model: grok_model,
470
+ tools: &tool_refs,
471
+ profile_launch: None,
472
+ agent_id_hint: Some(clone_id),
473
+ effort: None,
474
+ })
475
+ .unwrap_or_else(|e| panic!("{provider:?} build_command_plan: {e}"));
476
+ match provider {
477
+ Provider::Claude | Provider::ClaudeCode => {
478
+ let disallowed = flag_values(&plan.argv, "--disallowedTools");
479
+ assert_write_tools_not_disallowed("claude argv --disallowedTools", &disallowed);
480
+ }
481
+ Provider::Grok => {
482
+ let disallowed = flag_values(&plan.argv, "--disallowedTools");
483
+ assert_write_tools_not_disallowed("grok argv --disallowedTools", &disallowed);
484
+ }
485
+ Provider::CursorAgent => {
486
+ assert!(
487
+ !plan.argv.iter().any(|arg| arg == "--allowed-tools"),
488
+ "cursor must not invent unverified --allowed-tools (would silently whitelist a subset); argv={:?}",
489
+ plan.argv
490
+ );
491
+ }
492
+ other => panic!("face3 unexpected provider {other:?}"),
493
+ }
494
+
495
+ let overlay_root = case
496
+ .env
497
+ .root()
498
+ .join(format!("overlay-{}", provider_tag(provider)));
499
+ std::fs::create_dir_all(&overlay_root).expect("overlay dir");
500
+ let overlay_ws = overlay_root.to_string_lossy().into_owned();
501
+ let cfg = overlay_config(clone_id, &overlay_ws);
502
+ match provider {
503
+ Provider::Grok => {
504
+ apply_grok_mcp_overlay(&overlay_root, &cfg).expect("grok overlay");
505
+ let text = std::fs::read_to_string(overlay_root.join(".grok/config.toml"))
506
+ .expect("grok config.toml");
507
+ assert!(
508
+ text.contains("[mcp_servers.team_orchestrator]"),
509
+ "grok MCP injection must keep team_orchestrator, not a readonly stub; text={text}"
510
+ );
511
+ }
512
+ Provider::Claude | Provider::ClaudeCode => {
513
+ assert!(
514
+ mcp.raw.get("team_orchestrator").is_some(),
515
+ "claude MCP config must keep team_orchestrator server; raw={}",
516
+ mcp.raw
517
+ );
518
+ }
519
+ Provider::CursorAgent => {
520
+ apply_cursor_mcp_overlay(&overlay_root, &cfg).expect("cursor overlay");
521
+ let text = std::fs::read_to_string(overlay_root.join(".cursor/mcp.json"))
522
+ .expect("cursor mcp.json");
523
+ assert!(
524
+ text.contains("\"team_orchestrator\""),
525
+ "cursor MCP injection must keep team_orchestrator; text={text}"
526
+ );
527
+ }
528
+ other => panic!("face3 unexpected provider {other:?}"),
529
+ }
530
+ }
531
+
532
+ fn provider_tag(provider: Provider) -> &'static str {
533
+ match provider {
534
+ Provider::Grok => "grok",
535
+ Provider::Claude | Provider::ClaudeCode => "claude",
536
+ Provider::CursorAgent => "cursor",
537
+ _ => "other",
538
+ }
539
+ }
540
+
541
+ /// Face 1: spawn argv tools come from resolve_permissions on the clone row, not a leader ceiling.
542
+ #[test]
543
+ #[serial(env)]
544
+ fn g1_face1_resolve_permissions() {
545
+ let case = boot_six_clone();
546
+ let spec = load_runtime_spec(&case.workspace);
547
+ let clone_row = spec_agent(&spec, CLONE);
548
+ let resolved = command_tools(clone_row, CLONE, Provider::Fake);
549
+ let resolved_set: BTreeSet<String> = resolved.iter().cloned().collect();
550
+ assert_eq!(
551
+ resolved_set,
552
+ set_of(SIX),
553
+ "face1: resolved_tool_strings_for_command must keep the six-set; got={resolved:?} leader={:?}",
554
+ leader_spec_tools(&case.workspace)
555
+ );
556
+ assert!(
557
+ resolved_set.contains("execute_bash") && resolved_set.contains("fs_write"),
558
+ "face1: write/exec must survive worker_command_context; got={resolved:?}"
559
+ );
560
+
561
+ let direct = resolve_permissions(&AgentPermissionInput {
562
+ id: Some(AgentId::new(CLONE)),
563
+ provider: Provider::Fake,
564
+ role: clone_row
565
+ .get("role")
566
+ .and_then(Value::as_str)
567
+ .map(str::to_string),
568
+ tools: Some(resolved.clone()),
569
+ })
570
+ .expect("resolve_permissions on clone tools");
571
+ let direct_set: BTreeSet<String> = direct
572
+ .sorted_tool_strings()
573
+ .into_iter()
574
+ .map(str::to_string)
575
+ .collect();
576
+ assert_eq!(
577
+ direct_set,
578
+ set_of(SIX),
579
+ "face1: resolve_permissions itself must not intersect leader ceiling"
580
+ );
581
+
582
+ let state = load_runtime_state(&case.workspace).expect("runtime state after clone");
583
+ if let Some(persisted) = collect_json_tools(&state, CLONE) {
584
+ assert_eq!(
585
+ persisted,
586
+ set_of(SIX),
587
+ "face1: persisted spawn permissions must equal source six-set; got={persisted:?}"
588
+ );
589
+ }
590
+ case.shutdown();
591
+ }
592
+
593
+ /// Face 2: Claude --disallowedTools must not re-ban tools the clone still declares.
594
+ #[test]
595
+ #[serial(env)]
596
+ fn g1_face2_disallowed_tools() {
597
+ let case = boot_six_clone();
598
+ let spec = load_runtime_spec(&case.workspace);
599
+ let clone_row = spec_agent(&spec, CLONE);
600
+ let resolved = command_tools(clone_row, CLONE, Provider::Claude);
601
+ let tool_refs: Vec<&str> = resolved.iter().map(String::as_str).collect();
602
+ let disallowed = claude_disallowed_tools(&tool_refs);
603
+ assert_write_tools_not_disallowed("face2 claude_disallowed_tools", &disallowed);
604
+
605
+ let adapter = get_adapter(Provider::Claude);
606
+ let mcp = adapter
607
+ .mcp_config(AuthMode::Subscription)
608
+ .expect("claude mcp");
609
+ let plan = adapter
610
+ .build_command_plan(ProviderCommandContext {
611
+ auth_mode: AuthMode::Subscription,
612
+ mcp_config: Some(&mcp),
613
+ system_prompt: Some("g1 face2"),
614
+ model: None,
615
+ tools: &tool_refs,
616
+ profile_launch: None,
617
+ agent_id_hint: Some(CLONE),
618
+ effort: None,
619
+ })
620
+ .expect("claude plan");
621
+ let argv_disallowed = flag_values(&plan.argv, "--disallowedTools");
622
+ assert_write_tools_not_disallowed("face2 argv", &argv_disallowed);
623
+ assert!(
624
+ plan.argv.iter().any(|arg| arg == "--disallowedTools") || argv_disallowed.is_empty(),
625
+ "face2 must actually walk Claude argv construction; argv={:?}",
626
+ plan.argv
627
+ );
628
+ case.shutdown();
629
+ }
630
+
631
+ /// Face 3 umbrella: all three provider injection surfaces stay at the six-set.
632
+ #[test]
633
+ #[serial(env)]
634
+ fn g1_face3_mcp_tool_table() {
635
+ let case = boot_six_clone();
636
+ assert_provider_mcp_table(&case, Provider::Grok, CLONE);
637
+ assert_provider_mcp_table(&case, Provider::Claude, CLONE);
638
+ assert_provider_mcp_table(&case, Provider::CursorAgent, CLONE);
639
+ case.shutdown();
640
+ }
641
+
642
+ #[test]
643
+ #[serial(env)]
644
+ fn g1_face3_mcp_tool_table_grok() {
645
+ let case = boot_six_clone();
646
+ assert_provider_mcp_table(&case, Provider::Grok, CLONE);
647
+ case.shutdown();
648
+ }
649
+
650
+ #[test]
651
+ #[serial(env)]
652
+ fn g1_face3_mcp_tool_table_claude() {
653
+ let case = boot_six_clone();
654
+ assert_provider_mcp_table(&case, Provider::Claude, CLONE);
655
+ case.shutdown();
656
+ }
657
+
658
+ #[test]
659
+ #[serial(env)]
660
+ fn g1_face3_mcp_tool_table_cursor() {
661
+ let case = boot_six_clone();
662
+ assert_provider_mcp_table(&case, Provider::CursorAgent, CLONE);
663
+ case.shutdown();
664
+ }
665
+
666
+ /// Face 4: MCP clone_agent(...) and CLI clone-agent must keep the same tools set.
667
+ #[test]
668
+ #[serial(env)]
669
+ fn g1_face4_mcp_clone_entry() {
670
+ // Two isolated teams: a second clone on the same spec can rewrite the
671
+ // agent list from role docs and drop the first dest. Compare tools across
672
+ // workspaces, not two dests on one spec.
673
+ let cli_case = Case::start();
674
+ quick_start(&cli_case);
675
+ clone_ok(&cli_case, SOURCE, CLONE);
676
+ let cli_tools = spec_tools(&cli_case.workspace, CLONE);
677
+ let cli_spec = load_runtime_spec(&cli_case.workspace);
678
+ let cli_resolved = command_tools(spec_agent(&cli_spec, CLONE), CLONE, Provider::Fake);
679
+
680
+ let body_case = Case::start();
681
+ quick_start(&body_case);
682
+ crate::lifecycle::clone_agent(
683
+ &body_case.workspace,
684
+ &AgentId::new(SOURCE),
685
+ &AgentId::new(CLONE),
686
+ None,
687
+ false,
688
+ Some(TEAM_NAME),
689
+ )
690
+ .unwrap_or_else(|e| panic!("shared clone_agent body must succeed: {e}"));
691
+ let body_tools = spec_tools(&body_case.workspace, CLONE);
692
+ let body_spec = load_runtime_spec(&body_case.workspace);
693
+ let body_resolved = command_tools(spec_agent(&body_spec, CLONE), CLONE, Provider::Fake);
694
+
695
+ assert_eq!(cli_tools, set_of(SIX), "CLI clone tools");
696
+ assert_eq!(body_tools, set_of(SIX), "shared-body clone tools");
697
+ assert_eq!(
698
+ cli_tools, body_tools,
699
+ "face4: CLI and shared clone_agent body must keep one tools assertion"
700
+ );
701
+ assert_eq!(
702
+ cli_resolved, body_resolved,
703
+ "face4: spawn-time resolved tools must match across CLI and shared-body clones"
704
+ );
705
+
706
+ let team_key = runtime_team_key(&body_case.workspace);
707
+ let mcp_probe = TeamOrchestratorTools::with_identity(
708
+ &body_case.workspace,
709
+ Some(AgentId::new(SOURCE)),
710
+ Some(TeamKey::new(&team_key)),
711
+ )
712
+ .clone_agent(SOURCE, "mcp_probe", None);
713
+ match mcp_probe {
714
+ Ok(_) => {
715
+ assert_eq!(
716
+ spec_tools(&body_case.workspace, "mcp_probe"),
717
+ set_of(SIX),
718
+ "mcp_probe tools must equal the source six-set"
719
+ );
720
+ }
721
+ Err(error) => {
722
+ let text = format!("{error:?}");
723
+ assert!(
724
+ text.contains("not found")
725
+ || text.contains("unknown worker")
726
+ || text.contains("team select")
727
+ || text.contains("TeamSelect"),
728
+ "mcp_probe MCP clone_agent must persist six-set or fail-loud; error={text}"
729
+ );
730
+ }
731
+ }
732
+ cli_case.shutdown();
733
+ body_case.shutdown();
734
+ }
735
+
736
+ /// Face 5: a deliberately narrow source must stay narrow after clone (no widen-to-six).
737
+ #[test]
738
+ #[serial(env)]
739
+ fn g1_face5_narrow_source_stays_narrow() {
740
+ let case = Case::start();
741
+ quick_start(&case);
742
+ clone_ok(&case, NARROW, NARROW_CLONE);
743
+
744
+ let spec = load_runtime_spec(&case.workspace);
745
+ let clone_row = spec_agent(&spec, NARROW_CLONE);
746
+ let spec_set = yaml_tool_set(clone_row);
747
+ assert_eq!(
748
+ spec_set,
749
+ set_of(THREE),
750
+ "face5: spec tools must stay the three-set; got={spec_set:?}"
751
+ );
752
+
753
+ let resolved: BTreeSet<String> = command_tools(clone_row, NARROW_CLONE, Provider::Fake)
754
+ .into_iter()
755
+ .collect();
756
+ assert_eq!(
757
+ resolved,
758
+ set_of(THREE),
759
+ "face5: resolve_permissions must not fall back to developer defaults and widen; got={resolved:?}"
760
+ );
761
+ assert!(
762
+ !resolved.contains("execute_bash") && !resolved.contains("fs_write"),
763
+ "face5: narrow clone must not gain write/exec; got={resolved:?}"
764
+ );
765
+
766
+ let role = role_tools(
767
+ &case
768
+ .workspace
769
+ .join(".team")
770
+ .join("dynamic-role-files")
771
+ .join(format!("{NARROW_CLONE}.md")),
772
+ );
773
+ assert_eq!(role, set_of(THREE), "face5: role file must stay narrow");
774
+
775
+ let tool_refs: Vec<&str> = resolved.iter().map(String::as_str).collect();
776
+ let disallowed = claude_disallowed_tools(&tool_refs);
777
+ for required in ["Bash", "Edit", "Write"] {
778
+ assert!(
779
+ disallowed.contains(&required),
780
+ "face5: narrow clone MUST disallow {required}; disallowed={disallowed:?}"
781
+ );
782
+ }
783
+ case.shutdown();
784
+ }