@team-agent/installer 0.5.66 → 0.5.69

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 (178) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -0
  4. package/crates/team-agent/src/cli/adapters.rs +5 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +156 -0
  6. package/crates/team-agent/src/cli/emit.rs +6 -5
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +22 -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 +3 -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/missing_subcommands.rs +129 -19
  17. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  18. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  19. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  20. package/crates/team-agent/src/compiler.rs +121 -5
  21. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  22. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  23. package/crates/team-agent/src/coordinator/health.rs +173 -0
  24. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  25. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  26. package/crates/team-agent/src/coordinator/runtime_detectors.rs +31 -0
  27. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  29. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  30. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  31. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  32. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  33. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  34. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  35. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  36. package/crates/team-agent/src/coordinator/types.rs +49 -0
  37. package/crates/team-agent/src/db/message_store.rs +39 -5
  38. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  39. package/crates/team-agent/src/layout/worker_window_helpers.rs +3 -0
  40. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  41. package/crates/team-agent/src/leader/registry.rs +65 -0
  42. package/crates/team-agent/src/leader/start.rs +973 -75
  43. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  44. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  45. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +398 -26
  46. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +80 -0
  47. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +60 -0
  48. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  49. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  50. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +548 -0
  51. package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +120 -0
  52. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +208 -456
  53. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  54. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  55. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  56. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  57. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  58. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +482 -1
  59. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  60. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +830 -0
  61. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  62. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  63. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +87 -3
  64. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  65. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  66. package/crates/team-agent/src/lifecycle/launch/spawn.rs +127 -25
  67. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +140 -0
  68. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
  69. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  70. package/crates/team-agent/src/lifecycle/launch.rs +107 -29
  71. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  72. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  73. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  74. package/crates/team-agent/src/lifecycle/profile_launch.rs +99 -0
  75. package/crates/team-agent/src/lifecycle/profile_smoke.rs +52 -1
  76. package/crates/team-agent/src/lifecycle/restart/agent.rs +214 -160
  77. package/crates/team-agent/src/lifecycle/restart/common.rs +513 -37
  78. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  79. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  80. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
  81. package/crates/team-agent/src/lifecycle/restart/remove.rs +233 -6
  82. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  83. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  84. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  85. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
  86. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +24 -3
  87. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  88. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
  89. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
  90. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
  91. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +464 -0
  92. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +247 -0
  93. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  94. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_g0.stdout.txt +222 -0
  95. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +787 -0
  96. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  97. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  98. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +485 -0
  99. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  100. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  101. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
  102. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
  103. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
  104. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
  105. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  106. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
  107. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
  108. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +97 -0
  109. package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +128 -0
  110. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +207 -0
  111. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +838 -0
  112. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +320 -0
  113. package/crates/team-agent/src/lifecycle/tests/pi_lifecycle_doctor_red.rs +98 -0
  114. package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +216 -0
  115. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -0
  116. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
  117. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
  118. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  119. package/crates/team-agent/src/lifecycle/tests.rs +30 -13
  120. package/crates/team-agent/src/lifecycle/types.rs +61 -0
  121. package/crates/team-agent/src/lifecycle/worker_command_context.rs +143 -12
  122. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  123. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  124. package/crates/team-agent/src/messaging/delivery.rs +430 -41
  125. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  126. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  127. package/crates/team-agent/src/messaging/results.rs +4 -0
  128. package/crates/team-agent/src/messaging/send.rs +34 -0
  129. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  130. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  131. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  132. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  133. package/crates/team-agent/src/messaging/types.rs +4 -1
  134. package/crates/team-agent/src/model/enums.rs +17 -6
  135. package/crates/team-agent/src/model/permissions.rs +3 -0
  136. package/crates/team-agent/src/os_probe.rs +73 -2
  137. package/crates/team-agent/src/provider/adapter.rs +259 -6
  138. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +89 -0
  139. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  140. package/crates/team-agent/src/provider/adapters/mod.rs +3 -0
  141. package/crates/team-agent/src/provider/adapters/pi.rs +152 -0
  142. package/crates/team-agent/src/provider/bypass_flags.rs +47 -2
  143. package/crates/team-agent/src/provider/classify.rs +4 -2
  144. package/crates/team-agent/src/provider/faults.rs +2 -1
  145. package/crates/team-agent/src/provider/mod.rs +11 -0
  146. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  147. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  148. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  149. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  150. package/crates/team-agent/src/provider/session/context_fork.rs +78 -3
  151. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  152. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  153. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  154. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  155. package/crates/team-agent/src/provider/session_scan/common.rs +205 -2
  156. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  157. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  158. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  159. package/crates/team-agent/src/provider/session_scan/pi.rs +172 -0
  160. package/crates/team-agent/src/provider/session_scan.rs +12 -0
  161. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  162. package/crates/team-agent/src/provider/tests/adapter.rs +384 -0
  163. package/crates/team-agent/src/provider/types.rs +4 -2
  164. package/crates/team-agent/src/provider/wire.rs +29 -1
  165. package/crates/team-agent/src/state/persist.rs +301 -8
  166. package/crates/team-agent/src/state/repository.rs +5 -0
  167. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  168. package/crates/team-agent/src/tmux_backend.rs +1116 -75
  169. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  170. package/crates/team-agent/src/transport.rs +248 -1
  171. package/npm/install.mjs +129 -73
  172. package/package.json +4 -4
  173. package/skills/team-agent/SKILL.md +32 -239
  174. package/skills/team-agent/command-coverage.json +31 -0
  175. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  176. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
  177. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  178. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
@@ -1,6 +1,34 @@
1
+ //! ---
2
+ //! purpose: 各 provider 的 MCP 配置落盘与 argv 指向,含 grok 目录作用域配置的清洗与前置检查
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: resolve_mcp_config
6
+ //! what: 把 MCP 配置里的 workspace、agent_id 与 team_id 占位符替换掉
7
+ //! - name: write_worker_mcp_config_for_provider
8
+ //! what: 写出 per-agent 的 MCP 配置文件,copilot 走字段名翻译
9
+ //! - name: apply_grok_mcp_overlay
10
+ //! what: 写 workspace 下 grok 的项目作用域配置,是该文件的唯一写者
11
+ //! - name: reconcile_grok_toml_per_seat_keys
12
+ //! what: 起 grok 席前清掉共享 toml 里遗留的 per-seat 键并留审计事件
13
+ //! - name: ensure_grok_login_and_folder_trust
14
+ //! what: grok 未登录时拒绝起席;目录信任与用户同意由可见 Grok TUI 负责
15
+ //! - name: point_native_mcp_config_at_file
16
+ //! what: 把 argv 里的 MCP 配置参数改指到已落盘的文件
17
+ //! depends:
18
+ //! - crate::provider::McpConfig
19
+ //! - crate::model::permissions
20
+ //! - crate::event_log::EventLog
21
+ //! - crate::lifecycle::profile_launch
22
+ //! - std::fs
23
+ //! boundary:
24
+ //! - 不读也不写 provider 的凭据文件,只判断登录态文件是否存在且非空
25
+ //! - 审计事件只落键名,不落键值
26
+ //! - 清洗共享 toml 时不删未知键,未知不等于可删
27
+ //! - 清洗后校验失败一律拒绝起席,不带着脏配置继续
28
+ //! maturity: wired
29
+ //! ---
1
30
  use std::collections::{BTreeMap, BTreeSet};
2
31
  use std::path::{Path, PathBuf};
3
- use std::process::Command;
4
32
 
5
33
  use crate::lifecycle::*;
6
34
  use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
@@ -14,6 +42,10 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
14
42
 
15
43
  use super::*;
16
44
 
45
+ /// ---
46
+ /// purpose: 把 MCP 配置里的占位符替换成本次 workspace、席位与团队
47
+ /// returns: 替换后的配置
48
+ /// ---
17
49
  pub(crate) fn resolve_mcp_config(
18
50
  config: crate::provider::McpConfig,
19
51
  workspace: &Path,
@@ -25,6 +57,10 @@ pub(crate) fn resolve_mcp_config(
25
57
  }
26
58
  }
27
59
 
60
+ /// ---
61
+ /// purpose: 递归替换 JSON 里字符串中的三个占位符
62
+ /// returns: 同结构的新值,非字符串标量原样返回
63
+ /// ---
28
64
  pub(super) fn resolve_mcp_placeholders(
29
65
  value: serde_json::Value,
30
66
  workspace: &Path,
@@ -57,6 +93,12 @@ pub(super) fn resolve_mcp_placeholders(
57
93
  }
58
94
  }
59
95
 
96
+ /// ---
97
+ /// purpose: 写出 per-agent 的 MCP 配置文件,不做 provider 特化
98
+ /// returns: 写出的文件路径
99
+ /// errors: 建目录、序列化或写文件失败时返回 StatePersist
100
+ /// contract_id: lifecycle.mcp_config.write_worker_config
101
+ /// ---
60
102
  pub(crate) fn write_worker_mcp_config(
61
103
  workspace: &Path,
62
104
  agent_id: &str,
@@ -65,6 +107,14 @@ pub(crate) fn write_worker_mcp_config(
65
107
  write_worker_mcp_config_for_provider(workspace, agent_id, config, None)
66
108
  }
67
109
 
110
+ /// ---
111
+ /// purpose: 写出 per-agent 的 MCP 配置文件,copilot 先把 type 字段翻成 transport
112
+ /// params:
113
+ /// provider: 为 copilot 时做字段名翻译,其余原样写
114
+ /// returns: 写出的文件路径
115
+ /// errors: 建目录、序列化或写文件失败时返回 StatePersist
116
+ /// contract_id: lifecycle.mcp_config.write_worker_config
117
+ /// ---
68
118
  /// C-3-4 cr verdict v2 — Copilot 的 mcp config schema 字段名是 `transport`
69
119
  /// (实测 cmd-mcp-add 原文取值 stdio|http|sse),不是 canonical 的 `type`。当
70
120
  /// provider==Copilot 时写出文件前先做 type→transport 翻译;其它 provider 不动。
@@ -95,6 +145,426 @@ pub(crate) fn write_worker_mcp_config_for_provider(
95
145
  Ok(path)
96
146
  }
97
147
 
148
+ /// Grok MCP 是**目录作用域**(`mcp_injection: dir_scoped`):CLI 只读
149
+ /// `<cwd>/.grok/config.toml`。同目录后起席位会覆盖这份文件,先起席位的
150
+ /// 身份被回溯性改写且不报错。本版 worker cwd 恒为 workspace(D5),
151
+ /// exclusive 检查也不读 per-agent cwd ⇒ 一个 workspace 只支持一个 grok 席。
152
+ /// 这是 grok 的 provider 能力边界,不是框架通则(claude/codex 走 argv
153
+ /// `--mcp-config`,同目录多席没有这个问题)。
154
+ struct GrokOccupant {
155
+ id: String,
156
+ spawned_at: Option<String>,
157
+ status: String,
158
+ }
159
+
160
+ fn agent_is_grok(agent: &Value) -> bool {
161
+ agent
162
+ .get("provider")
163
+ .and_then(Value::as_str)
164
+ .and_then(crate::lifecycle::profile_launch::parse_provider)
165
+ == Some(Provider::Grok)
166
+ }
167
+
168
+ fn status_is_live(status: &str) -> bool {
169
+ !matches!(
170
+ status,
171
+ "stopped" | "stopping" | "removed" | "spawn_failed" | "failed"
172
+ )
173
+ }
174
+
175
+ fn occupant_from_state_row(id: &str, agent: &serde_json::Value) -> Option<GrokOccupant> {
176
+ let provider = agent
177
+ .get("provider")
178
+ .and_then(serde_json::Value::as_str)
179
+ .and_then(crate::lifecycle::profile_launch::parse_provider);
180
+ if provider != Some(Provider::Grok) {
181
+ return None;
182
+ }
183
+ let status = agent
184
+ .get("status")
185
+ .and_then(serde_json::Value::as_str)
186
+ .unwrap_or("running");
187
+ if !status_is_live(status) {
188
+ return None;
189
+ }
190
+ Some(GrokOccupant {
191
+ id: id.to_string(),
192
+ spawned_at: agent
193
+ .get("spawned_at")
194
+ .and_then(serde_json::Value::as_str)
195
+ .map(str::to_string),
196
+ status: status.to_string(),
197
+ })
198
+ }
199
+
200
+ fn live_grok_occupants_from_state(workspace: &Path) -> Result<Vec<GrokOccupant>, LifecycleError> {
201
+ let path = crate::state::persist::runtime_state_path(workspace);
202
+ if !path.exists() {
203
+ return Ok(Vec::new());
204
+ }
205
+ let state = crate::state::persist::load_runtime_state(workspace).map_err(|e| {
206
+ LifecycleError::StatePersist(format!(
207
+ "cannot count live grok seats (state unreadable): {e}"
208
+ ))
209
+ })?;
210
+ let mut out = Vec::new();
211
+ if let Some(agents) = state.get("agents").and_then(serde_json::Value::as_object) {
212
+ for (id, agent) in agents {
213
+ if let Some(row) = occupant_from_state_row(id, agent) {
214
+ out.push(row);
215
+ }
216
+ }
217
+ }
218
+ if let Some(teams) = state.get("teams").and_then(serde_json::Value::as_object) {
219
+ for team in teams.values() {
220
+ let Some(agents) = team.get("agents").and_then(serde_json::Value::as_object) else {
221
+ continue;
222
+ };
223
+ for (id, agent) in agents {
224
+ if out.iter().any(|o| o.id == *id) {
225
+ continue;
226
+ }
227
+ if let Some(row) = occupant_from_state_row(id, agent) {
228
+ out.push(row);
229
+ }
230
+ }
231
+ }
232
+ }
233
+ Ok(out)
234
+ }
235
+
236
+ fn spec_grok_ids(spec: &Value) -> Vec<String> {
237
+ let mut ids = Vec::new();
238
+ for agent in spec_agent_values(spec) {
239
+ if agent_is_paused(agent) || !agent_is_grok(agent) {
240
+ continue;
241
+ }
242
+ if let Some(id) = agent.get("id").and_then(Value::as_str) {
243
+ if !ids.iter().any(|existing| existing == id) {
244
+ ids.push(id.to_string());
245
+ }
246
+ }
247
+ }
248
+ ids
249
+ }
250
+
251
+ /// ---
252
+ /// purpose: 清掉 grok 共享 toml 里遗留的 per-seat 键,并写一条只含键名的审计事件
253
+ /// returns: 文件不存在或本就没有 per-seat 键时直接成功
254
+ /// errors: 文件读不出、清洗后仍残留或审计事件写不出时返回 RequirementUnmet
255
+ /// ---
256
+ /// Reconcile `<cwd>/.grok/config.toml` before any grok start: drop leftover
257
+ /// framework per-seat keys (`is_per_seat_env_key`), leave user/shared keys,
258
+ /// emit an audit event with names only. Hung only from
259
+ /// [`apply_grok_mcp_overlay`] — the unique writer, so launch/restart/resume
260
+ /// all pass through. Clean-failure keeps the previous refuse shape.
261
+ pub(crate) fn reconcile_grok_toml_per_seat_keys(workspace: &Path) -> Result<(), LifecycleError> {
262
+ let path = workspace.join(".grok").join("config.toml");
263
+ if !path.exists() {
264
+ return Ok(());
265
+ }
266
+ let text = std::fs::read_to_string(&path).map_err(|error| {
267
+ LifecycleError::RequirementUnmet(format!(
268
+ "error: cannot judge grok shared slot (unreadable {})\n\
269
+ reason: {error}\n\
270
+ action: fix permissions on that file before adding another grok seat",
271
+ path.display()
272
+ ))
273
+ })?;
274
+ let keys = super::per_seat_keys_in_toml(&text);
275
+ if keys.is_empty() {
276
+ return Ok(());
277
+ }
278
+ let (cleaned, removed) = super::strip_per_seat_keys_from_toml(&text);
279
+ let dir = workspace.join(".grok");
280
+ if let Err(_error) = write_grok_config_toml(&dir, &cleaned) {
281
+ return Err(refuse_dirty_grok_toml(workspace, &keys));
282
+ }
283
+ let after =
284
+ std::fs::read_to_string(&path).map_err(|_| refuse_dirty_grok_toml(workspace, &keys))?;
285
+ if !super::per_seat_keys_in_toml(&after).is_empty() {
286
+ return Err(refuse_dirty_grok_toml(workspace, &keys));
287
+ }
288
+ crate::event_log::EventLog::new(workspace)
289
+ .write(
290
+ crate::lifecycle::types::event_names::GROK_TOML_PER_SEAT_KEYS_CLEARED,
291
+ serde_json::json!({
292
+ "path": path.display().to_string(),
293
+ "keys": removed,
294
+ }),
295
+ )
296
+ .map_err(|error| {
297
+ LifecycleError::RequirementUnmet(format!(
298
+ "error: cannot audit grok shared-slot cleanup ({})\n\
299
+ reason: {error}\n\
300
+ action: fix permissions on .team/logs then retry",
301
+ path.display()
302
+ ))
303
+ })?;
304
+ Ok(())
305
+ }
306
+
307
+ fn refuse_dirty_grok_toml(workspace: &Path, keys: &[(String, String)]) -> LifecycleError {
308
+ let named = keys
309
+ .iter()
310
+ .map(|(key, _value)| key.as_str())
311
+ .collect::<Vec<_>>()
312
+ .join(", ");
313
+ LifecycleError::RequirementUnmet(format!(
314
+ "error: grok shared slot still carries per-seat keys ({named})\n\
315
+ reason: .grok/config.toml is directory-scoped; per-seat keys would be inherited by every grok seat\n\
316
+ workspace: {}\n\
317
+ action: remove per-seat keys from the toml (identity belongs on pane env)",
318
+ workspace.display()
319
+ ))
320
+ }
321
+
322
+ fn write_grok_config_toml(dir: &Path, body: &str) -> Result<PathBuf, std::io::Error> {
323
+ std::fs::create_dir_all(dir)?;
324
+ let path = dir.join("config.toml");
325
+ let tmp = dir.join("config.toml.tmp");
326
+ std::fs::write(&tmp, body.as_bytes())?;
327
+ std::fs::rename(&tmp, &path)?;
328
+ Ok(path)
329
+ }
330
+
331
+ /// ---
332
+ /// purpose: 构造同一 workspace 已被别的 grok 席占用的拒绝错误
333
+ /// params:
334
+ /// seats: 已占用该目录的席位名,写进错误正文
335
+ /// returns: 带 error/reason/workspace/grok_seats 的 RequirementUnmet
336
+ /// ---
337
+ pub(crate) fn grok_shared_cwd_error(cwd: &Path, seats: &[String]) -> LifecycleError {
338
+ LifecycleError::RequirementUnmet(format!(
339
+ "error: grok seat already occupies this workspace\n\
340
+ reason: grok MCP is directory-scoped (<cwd>/.grok/config.toml); a second seat overwrites TEAM_AGENT_ID\n\
341
+ workspace: {}\n\
342
+ grok_seats: {}",
343
+ cwd.display(),
344
+ seats.join(", "),
345
+ ))
346
+ }
347
+
348
+ fn grok_occupied_cwd_error(
349
+ cwd: &Path,
350
+ incoming: &str,
351
+ occupants: &[GrokOccupant],
352
+ ) -> LifecycleError {
353
+ let holder = occupants
354
+ .iter()
355
+ .find(|o| o.id != incoming)
356
+ .or_else(|| occupants.first());
357
+ let holder_id = holder.map(|o| o.id.as_str()).unwrap_or("unknown");
358
+ let started = holder
359
+ .and_then(|o| o.spawned_at.as_deref())
360
+ .unwrap_or("unknown");
361
+ let names = occupants
362
+ .iter()
363
+ .map(|o| {
364
+ if let Some(at) = &o.spawned_at {
365
+ format!("{} (status={}, started {})", o.id, o.status, at)
366
+ } else {
367
+ format!("{} (status={})", o.id, o.status)
368
+ }
369
+ })
370
+ .collect::<Vec<_>>()
371
+ .join(", ");
372
+ LifecycleError::RequirementUnmet(format!(
373
+ "error: grok seat {holder_id} already occupies this workspace (started {started})\n\
374
+ incoming: {incoming}\n\
375
+ reason: grok MCP is directory-scoped (<cwd>/.grok/config.toml); a second seat overwrites TEAM_AGENT_ID and the first seat's first turn would inherit the wrong identity\n\
376
+ workspace: {}\n\
377
+ grok_seats: {names}",
378
+ cwd.display(),
379
+ ))
380
+ }
381
+
382
+ /// ---
383
+ /// purpose: 起 grok 席前确认已登录;目录信任与用户同意交给可见 Grok TUI
384
+ /// params:
385
+ /// _cwd: 席位的工作目录;保留参数以维持现有调用契约
386
+ /// returns: 登录态存在时返回空值
387
+ /// errors: HOME 未设、登录态文件缺失或为空时返回 RequirementUnmet
388
+ /// ---
389
+ /// 未登录时不许起出「能收信、没有手」的 grok 席。登录态只看
390
+ /// `$HOME/.grok/auth.json`;Team Agent 不判断或改变目录信任,Grok TUI
391
+ /// 如需用户同意会在可见 pane 内自行询问。
392
+ pub(crate) fn ensure_grok_login_and_folder_trust(_cwd: &Path) -> Result<(), LifecycleError> {
393
+ let home = std::env::var_os("HOME").map(PathBuf::from).ok_or_else(|| {
394
+ LifecycleError::RequirementUnmet(
395
+ "error: HOME is unset; cannot verify grok login\n\
396
+ action: export HOME and retry"
397
+ .to_string(),
398
+ )
399
+ })?;
400
+ let grok_home = home.join(".grok");
401
+ if !grok_auth_present(&grok_home) {
402
+ return Err(LifecycleError::RequirementUnmet(format!(
403
+ "error: grok is not logged in (missing or empty {})\n\
404
+ action: run `grok login` then retry add-agent/launch",
405
+ grok_home.join("auth.json").display()
406
+ )));
407
+ }
408
+ Ok(())
409
+ }
410
+
411
+ fn grok_auth_present(grok_home: &Path) -> bool {
412
+ let path = grok_home.join("auth.json");
413
+ let Ok(text) = std::fs::read_to_string(path) else {
414
+ return false;
415
+ };
416
+ let Ok(value) = serde_json::from_str::<serde_json::Value>(&text) else {
417
+ return false;
418
+ };
419
+ value.as_object().is_some_and(|map| !map.is_empty())
420
+ }
421
+
422
+ /// ---
423
+ /// purpose: 写 workspace 下 grok 的项目作用域 MCP 配置,服务名固定为 team_orchestrator
424
+ /// params:
425
+ /// mcp_config: 已解析的 MCP 配置,须含 team_orchestrator 条目与 command
426
+ /// returns: 成功返回空值;写前先清 per-seat 键,已有表里的未知环境键会被保留
427
+ /// errors: 缺条目或缺 command 时返回 StatePersist,清洗失败透传 RequirementUnmet,写文件失败返回 StatePersist
428
+ /// ---
429
+ /// Grok CLI 没有 `--mcp-config`。同 `apply_cursor_agent_rules_overlay`:launch
430
+ /// 路径写一份 provider 实际会读的文件。Grok 只认项目作用域
431
+ /// `<cwd>/.grok/config.toml`(`grok mcp add --scope project` 的产物)。
432
+ ///
433
+ /// Unique writer of `<cwd>/.grok/config.toml`. Reconcile leftover per-seat
434
+ /// keys here so restart/resume cannot skip the upgrade migration.
435
+ ///
436
+ /// `McpConfig.raw` 与写出的 grok 表名都必须是 `team_orchestrator`,与
437
+ /// `worker_command_context` 契约(grok: `team_orchestrator__send_message`)对齐。
438
+ /// grok 按 server 名给工具加命名空间,写成 `team-agent` 会变成 `team-agent__*`。
439
+ pub fn apply_grok_mcp_overlay(
440
+ workspace: &Path,
441
+ mcp_config: &crate::provider::McpConfig,
442
+ ) -> Result<(), LifecycleError> {
443
+ // adapter.rs 只产出 team_orchestrator。team-agent 是 0.5.67 overlay
444
+ // 误用的旧 inbound key,读侧留一版以免夹具还带旧名;0.5.68 若无引用再删。
445
+ let server = mcp_config
446
+ .raw
447
+ .get("team_orchestrator")
448
+ .or_else(|| mcp_config.raw.get("team-agent"))
449
+ .ok_or_else(|| {
450
+ LifecycleError::StatePersist(
451
+ "grok MCP overlay requires team_orchestrator in resolved mcp config".to_string(),
452
+ )
453
+ })?;
454
+ let command = server
455
+ .get("command")
456
+ .and_then(serde_json::Value::as_str)
457
+ .filter(|value| !value.is_empty())
458
+ .ok_or_else(|| {
459
+ LifecycleError::StatePersist("grok MCP overlay missing command".to_string())
460
+ })?;
461
+ let args = server
462
+ .get("args")
463
+ .and_then(serde_json::Value::as_array)
464
+ .map(|items| {
465
+ items
466
+ .iter()
467
+ .filter_map(serde_json::Value::as_str)
468
+ .map(str::to_string)
469
+ .collect::<Vec<_>>()
470
+ })
471
+ .unwrap_or_default();
472
+ let env = server
473
+ .get("env")
474
+ .and_then(serde_json::Value::as_object)
475
+ .map(|map| {
476
+ map.iter()
477
+ .filter_map(|(key, value)| {
478
+ value.as_str().map(|text| (key.clone(), text.to_string()))
479
+ })
480
+ .collect::<BTreeMap<_, _>>()
481
+ })
482
+ .unwrap_or_default();
483
+
484
+ // Per-seat keys live on the pane env. Unknown keys on the existing
485
+ // table stay: a full stanza rewrite must not treat "not in our list"
486
+ // as permission to delete.
487
+ reconcile_grok_toml_per_seat_keys(workspace)?;
488
+ let dir = workspace.join(".grok");
489
+ let path = dir.join("config.toml");
490
+ let existing = std::fs::read_to_string(&path).unwrap_or_default();
491
+ let preserved = super::non_per_seat_env_in_tables(
492
+ &existing,
493
+ &["mcp_servers.team_orchestrator", "mcp_servers.team-agent"],
494
+ );
495
+ let mut env = env;
496
+ env.retain(|key, value| !super::is_per_seat_env_key(key) && !value.trim().is_empty());
497
+ for (key, value) in preserved {
498
+ env.entry(key).or_insert(value);
499
+ }
500
+
501
+ let stanza = render_grok_team_agent_stanza(command, &args, &env);
502
+ // 同时摘掉新表和 0.5.67 误写的 [mcp_servers.team-agent],否则改名后两套
503
+ // team MCP 并存,grok 会列出两份指向同一进程的工具。
504
+ let body = upsert_toml_table_prefixes(
505
+ &existing,
506
+ &["mcp_servers.team_orchestrator", "mcp_servers.team-agent"],
507
+ &stanza,
508
+ );
509
+ write_grok_config_toml(&dir, &body)
510
+ .map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", path.display())))?;
511
+ Ok(())
512
+ }
513
+
514
+ fn render_grok_team_agent_stanza(
515
+ command: &str,
516
+ args: &[String],
517
+ env: &BTreeMap<String, String>,
518
+ ) -> String {
519
+ let mut out = String::from("[mcp_servers.team_orchestrator]\n");
520
+ out.push_str(&format!("command = {}\n", toml_quote(command)));
521
+ out.push_str("args = [\n");
522
+ for arg in args {
523
+ out.push_str(&format!(" {},\n", toml_quote(arg)));
524
+ }
525
+ out.push_str("]\n");
526
+ out.push_str("enabled = true\n");
527
+ if !env.is_empty() {
528
+ out.push_str("\n[mcp_servers.team_orchestrator.env]\n");
529
+ for (key, value) in env {
530
+ out.push_str(&format!("{key} = {}\n", toml_quote(value)));
531
+ }
532
+ }
533
+ out
534
+ }
535
+
536
+ fn toml_quote(value: &str) -> String {
537
+ format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
538
+ }
539
+
540
+ fn upsert_toml_table_prefixes(existing: &str, tables: &[&str], stanza: &str) -> String {
541
+ let mut out = String::new();
542
+ let mut skip = false;
543
+ for line in existing.lines() {
544
+ let trimmed = line.trim();
545
+ if trimmed.starts_with('[') && trimmed.ends_with(']') {
546
+ let name = &trimmed[1..trimmed.len() - 1];
547
+ skip = tables
548
+ .iter()
549
+ .any(|table| name == *table || name.starts_with(&format!("{table}.")));
550
+ }
551
+ if !skip {
552
+ out.push_str(line);
553
+ out.push('\n');
554
+ }
555
+ }
556
+ let trimmed = out.trim_end();
557
+ if trimmed.is_empty() {
558
+ stanza.to_string()
559
+ } else {
560
+ format!("{trimmed}\n\n{stanza}")
561
+ }
562
+ }
563
+
564
+ /// ---
565
+ /// purpose: 把 MCP 配置里每个 server 的 type 字段名换成 transport
566
+ /// returns: 同结构的新值,其余字段全部保留;非对象原样返回
567
+ /// ---
98
568
  /// C-3-4 cr verdict v2 — McpConfig.raw 是 `{name: {type, command, args, env}}` 形;
99
569
  /// copilot mcp add schema 取 `transport` 替 `type`(stdio|http|sse 同值)。仅
100
570
  /// 字段名变换,其余字段全保留。
@@ -121,6 +591,12 @@ pub(super) fn copilot_translate_mcp_servers(raw: &serde_json::Value) -> serde_js
121
591
  serde_json::Value::Object(translated)
122
592
  }
123
593
 
594
+ /// ---
595
+ /// purpose: 把 argv 里 provider 原生的 MCP 配置参数改指到已落盘的文件
596
+ /// params:
597
+ /// argv: 就地改写;claude 系改 --mcp-config 的值,copilot 改 --additional-mcp-config 并加 @ 前缀
598
+ /// returns: argv 里没有对应参数时什么都不做
599
+ /// ---
124
600
  pub(crate) fn point_native_mcp_config_at_file(
125
601
  argv: &mut [String],
126
602
  provider: Provider,
@@ -150,6 +626,11 @@ pub(crate) fn point_native_mcp_config_at_file(
150
626
  }
151
627
  }
152
628
 
629
+ /// ---
630
+ /// purpose: 解析该席位的权限并序列化成 JSON
631
+ /// returns: 含 agent_id、provider、排序后的工具串、逐工具的执行强度与是否有仅提示项
632
+ /// errors: 权限解析失败时返回 ModelError
633
+ /// ---
153
634
  pub(super) fn permissions_json(
154
635
  agent: &Value,
155
636
  id: &str,
@@ -1,3 +1,23 @@
1
+ //! ---
2
+ //! purpose: 生命周期操作前的 owner 门,判断调用方是否有权动这个团队
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: ensure_owner_allowed
6
+ //! what: 读 runtime state 后过 owner 门
7
+ //! - name: ensure_owner_allowed_for_state
8
+ //! what: 对给定 state 过 owner 门,席位对自己的操作直接放行
9
+ //! - name: state_path
10
+ //! what: 给出该 workspace 的 runtime state 路径
11
+ //! depends:
12
+ //! - crate::state::owner_gate
13
+ //! - crate::state::identity
14
+ //! - crate::state::projection
15
+ //! - crate::state::persist
16
+ //! boundary:
17
+ //! - 只判权限,不改 state
18
+ //! - 本文件的 pane 存活探针恒报存活,存活判定不在这一层
19
+ //! maturity: wired
20
+ //! ---
1
21
  use std::collections::{BTreeMap, BTreeSet};
2
22
  use std::path::{Path, PathBuf};
3
23
  use std::process::Command;
@@ -14,12 +34,24 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
14
34
 
15
35
  use super::*;
16
36
 
37
+ /// ---
38
+ /// purpose: 读出 runtime state 后过 owner 门
39
+ /// returns: 通过返回空值
40
+ /// errors: 读 state 失败返回 StatePersist,owner 门拒绝返回 OwnerRefused
41
+ /// ---
17
42
  pub(crate) fn ensure_owner_allowed(workspace: &Path) -> Result<(), LifecycleError> {
18
43
  let state = crate::state::persist::load_runtime_state(workspace)
19
44
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
20
45
  ensure_owner_allowed_for_state(&state, None)
21
46
  }
22
47
 
48
+ /// ---
49
+ /// purpose: 对给定 state 过 owner 门
50
+ /// params:
51
+ /// target_role: 操作目标席位;调用方就是该席位且团队相符时直接放行
52
+ /// returns: 通过返回空值
53
+ /// errors: 取调用方身份失败返回 StatePersist,owner 门给出拒绝理由时返回 OwnerRefused
54
+ /// ---
23
55
  pub(crate) fn ensure_owner_allowed_for_state(
24
56
  state: &serde_json::Value,
25
57
  target_role: Option<&AgentId>,
@@ -50,6 +82,10 @@ pub(crate) fn ensure_owner_allowed_for_state(
50
82
  Ok(())
51
83
  }
52
84
 
85
+ /// ---
86
+ /// purpose: 判断调用方是否就是目标席位本人
87
+ /// returns: 环境里的席位 id 与团队 id 同时与目标相符时为 true
88
+ /// ---
53
89
  pub(super) fn caller_is_target_role_in_team(
54
90
  target_team: &str,
55
91
  target_role: Option<&AgentId>,
@@ -61,6 +97,10 @@ pub(super) fn caller_is_target_role_in_team(
61
97
  && std::env::var("TEAM_AGENT_TEAM_ID").ok().as_deref() == Some(target_team)
62
98
  }
63
99
 
100
+ /// ---
101
+ /// purpose: 给出该 workspace 的 runtime state 文件路径
102
+ /// returns: runtime state 路径
103
+ /// ---
64
104
  pub(crate) fn state_path(workspace: &Path) -> std::path::PathBuf {
65
105
  crate::state::persist::runtime_state_path(workspace)
66
106
  }