@team-agent/installer 0.5.66 → 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 (164) 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 +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/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 +59 -0
  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 +30 -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 +2 -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 +920 -63
  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 +332 -12
  46. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +62 -0
  47. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +59 -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 +332 -0
  51. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -456
  52. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  53. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  54. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  55. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  56. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  57. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  58. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  59. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  60. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  61. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  62. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  63. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  64. package/crates/team-agent/src/lifecycle/launch/spawn.rs +62 -2
  65. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +139 -0
  66. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
  67. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  68. package/crates/team-agent/src/lifecycle/launch.rs +99 -29
  69. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  70. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  71. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  72. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  73. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  74. package/crates/team-agent/src/lifecycle/restart/agent.rs +124 -0
  75. package/crates/team-agent/src/lifecycle/restart/common.rs +369 -11
  76. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  77. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  78. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
  79. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  80. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  81. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  82. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  83. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
  84. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  85. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
  86. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
  88. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  89. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  90. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  91. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  92. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  93. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  94. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  95. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  96. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  97. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
  98. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
  99. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
  100. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
  103. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +2 -0
  104. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
  105. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
  106. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  107. package/crates/team-agent/src/lifecycle/tests.rs +23 -13
  108. package/crates/team-agent/src/lifecycle/types.rs +61 -0
  109. package/crates/team-agent/src/lifecycle/worker_command_context.rs +85 -11
  110. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  111. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  112. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  113. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  114. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  115. package/crates/team-agent/src/messaging/results.rs +4 -0
  116. package/crates/team-agent/src/messaging/send.rs +34 -0
  117. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  118. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  119. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  121. package/crates/team-agent/src/messaging/types.rs +4 -1
  122. package/crates/team-agent/src/model/enums.rs +14 -5
  123. package/crates/team-agent/src/model/permissions.rs +3 -0
  124. package/crates/team-agent/src/os_probe.rs +73 -2
  125. package/crates/team-agent/src/provider/adapter.rs +231 -4
  126. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  127. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  128. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  129. package/crates/team-agent/src/provider/bypass_flags.rs +46 -2
  130. package/crates/team-agent/src/provider/classify.rs +4 -2
  131. package/crates/team-agent/src/provider/faults.rs +2 -1
  132. package/crates/team-agent/src/provider/mod.rs +11 -0
  133. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  134. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  135. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  136. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  137. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  138. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  139. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  140. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  141. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  142. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  143. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  144. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  145. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  146. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  147. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  148. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  149. package/crates/team-agent/src/provider/types.rs +4 -2
  150. package/crates/team-agent/src/provider/wire.rs +23 -1
  151. package/crates/team-agent/src/state/persist.rs +301 -8
  152. package/crates/team-agent/src/state/repository.rs +5 -0
  153. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  154. package/crates/team-agent/src/tmux_backend.rs +1064 -75
  155. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  156. package/crates/team-agent/src/transport.rs +248 -1
  157. package/npm/install.mjs +129 -73
  158. package/package.json +4 -4
  159. package/skills/team-agent/SKILL.md +33 -238
  160. package/skills/team-agent/command-coverage.json +31 -0
  161. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  162. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
  163. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  164. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
@@ -0,0 +1,498 @@
1
+ //! purpose: grok 席位必须把 team-agent MCP 写进 grok 实际会读的项目配置
2
+ //! contract: grok worker spawn 之后 `<workspace>/.grok/config.toml` 含
3
+ //! `[mcp_servers.team_orchestrator]`,command/args/env 已替换成这次席位的真实值
4
+ //! (不是 `{workspace}` 占位,也不是 `.team/runtime/mcp/*.json`)
5
+ //! boundary: 只覆盖 grok launch 产物;不改 claude/codex/copilot 路径
6
+ //!
7
+ //! 修之前判红:基线只把 MCP 写到 `.team/runtime/mcp/<agent>.json`,grok CLI
8
+ //! 不读那份文件,所以项目作用域 config.toml 不会出现已解析的 team-agent 段。
9
+
10
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
11
+
12
+ #[path = "../../../tests/support/hermetic.rs"]
13
+ mod hermetic_guard;
14
+ #[allow(dead_code)]
15
+ fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
16
+
17
+ use std::path::{Path, PathBuf};
18
+ use std::sync::atomic::{AtomicU64, Ordering};
19
+
20
+ use serial_test::serial;
21
+ use team_agent::lifecycle::quick_start_with_transport_in_workspace;
22
+ use team_agent::lifecycle::{
23
+ apply_grok_mcp_overlay, ensure_grok_login_and_folder_trust, LifecycleError,
24
+ };
25
+ use team_agent::provider::McpConfig;
26
+ use team_agent::transport::test_support::OfflineTransport;
27
+
28
+ #[test]
29
+ fn grok_overlay_writes_canonical_team_orchestrator_server_name() {
30
+ let ws = tmp_dir("grok-mcp-name");
31
+ apply_grok_mcp_overlay(&ws, &sample_mcp_config("name-seat", "/ws-name"))
32
+ .expect("overlay write");
33
+ let text = std::fs::read_to_string(ws.join(".grok/config.toml")).unwrap();
34
+ assert!(
35
+ text.contains("[mcp_servers.team_orchestrator]"),
36
+ "grok overlay must use the canonical server name the runtime contract cites; text={text}"
37
+ );
38
+ assert!(
39
+ text.contains("[mcp_servers.team_orchestrator.env]"),
40
+ "env table must sit under team_orchestrator, not a second name; text={text}"
41
+ );
42
+ assert!(
43
+ !text.contains("mcp_servers.team-agent"),
44
+ "canonical name is team_orchestrator; leftover team-agent would namespace tools as team-agent__*; text={text}"
45
+ );
46
+ }
47
+
48
+ #[test]
49
+ fn grok_overlay_migrates_legacy_team_agent_table_away() {
50
+ let ws = tmp_dir("grok-mcp-migrate");
51
+ let grok = ws.join(".grok");
52
+ std::fs::create_dir_all(&grok).unwrap();
53
+ std::fs::write(
54
+ grok.join("config.toml"),
55
+ r#"[mcp_servers.keep-me]
56
+ command = "/bin/keep"
57
+
58
+ [mcp_servers.team-agent]
59
+ command = "/old/team-agent"
60
+ args = ["stale"]
61
+ enabled = true
62
+
63
+ [mcp_servers.team-agent.env]
64
+ TEAM_AGENT_WORKSPACE = "/stale-ws"
65
+ "#,
66
+ )
67
+ .unwrap();
68
+
69
+ apply_grok_mcp_overlay(&ws, &sample_mcp_config("migrated-seat", "/ws-migrated"))
70
+ .expect("overlay migrate");
71
+ let text = std::fs::read_to_string(grok.join("config.toml")).unwrap();
72
+
73
+ assert!(
74
+ !text.contains("mcp_servers.team-agent"),
75
+ "legacy [mcp_servers.team-agent] must be removed or grok exposes two Team MCP servers; text={text}"
76
+ );
77
+ assert!(
78
+ text.contains("[mcp_servers.team_orchestrator]"),
79
+ "migrated file must declare the canonical server; text={text}"
80
+ );
81
+ assert!(
82
+ !text.contains("TEAM_AGENT_ID")
83
+ && !text.contains("TEAM_AGENT_OWNER_TEAM_ID")
84
+ && !text.contains("TEAM_AGENT_AUTH_MODE")
85
+ && text.contains("TEAM_AGENT_WORKSPACE = \"/ws-migrated\""),
86
+ "per-seat keys must not land on the shared toml; workspace env must survive rename; text={text}"
87
+ );
88
+ assert!(
89
+ text.contains("[mcp_servers.keep-me]"),
90
+ "unrelated project MCP servers must survive the rename; text={text}"
91
+ );
92
+ assert!(
93
+ !text.contains("stale-id") && !text.contains("/old/team-agent"),
94
+ "stale identity/command from the old table must not remain; text={text}"
95
+ );
96
+ }
97
+
98
+ #[test]
99
+ fn grok_overlay_clears_existing_per_seat_keys_instead_of_writing_them() {
100
+ // 已废除的行为:旧实现把每席键迁进共享 toml(目录作用域 ⇒ 所有 grok 席互相继承),此断言证明它确实没了。
101
+ let ws = tmp_dir("grok-mcp-clear-per-seat");
102
+ let grok = ws.join(".grok");
103
+ std::fs::create_dir_all(&grok).unwrap();
104
+ std::fs::write(
105
+ grok.join("config.toml"),
106
+ r#"[mcp_servers.team-agent]
107
+ command = "/old/team-agent"
108
+
109
+ [mcp_servers.team-agent.env]
110
+ TEAM_AGENT_ID = "stale-id"
111
+ TEAM_AGENT_OWNER_TEAM_ID = "stale-team"
112
+ TEAM_AGENT_AUTH_MODE = "subscription"
113
+ "#,
114
+ )
115
+ .unwrap();
116
+
117
+ apply_grok_mcp_overlay(&ws, &sample_mcp_config("migrated-seat", "/ws-migrated"))
118
+ .expect("leftover per-seat keys must be cleared, not refuse the overlay");
119
+ let text = std::fs::read_to_string(grok.join("config.toml")).unwrap();
120
+ assert!(
121
+ !text.contains("TEAM_AGENT_ID")
122
+ && !text.contains("TEAM_AGENT_OWNER_TEAM_ID")
123
+ && !text.contains("TEAM_AGENT_AUTH_MODE")
124
+ && !text.contains("stale-id")
125
+ && text.contains("TEAM_AGENT_WORKSPACE = \"/ws-migrated\""),
126
+ "overlay must strip leftover per-seat keys and not write incoming ones; text={text}"
127
+ );
128
+ let events = team_agent::event_log::EventLog::new(&ws)
129
+ .tail(0)
130
+ .expect("events");
131
+ let cleared = events.iter().find(|event| {
132
+ event.get("event").and_then(serde_json::Value::as_str)
133
+ == Some("lifecycle.grok_toml.per_seat_keys_cleared")
134
+ });
135
+ let cleared =
136
+ cleared.unwrap_or_else(|| panic!("cleanup must leave an audit event; events={events:?}"));
137
+ let keys = cleared
138
+ .get("keys")
139
+ .and_then(serde_json::Value::as_array)
140
+ .cloned()
141
+ .unwrap_or_default();
142
+ assert!(
143
+ keys.iter().any(|key| key.as_str() == Some("TEAM_AGENT_ID"))
144
+ && keys
145
+ .iter()
146
+ .any(|key| key.as_str() == Some("TEAM_AGENT_OWNER_TEAM_ID"))
147
+ && keys
148
+ .iter()
149
+ .any(|key| key.as_str() == Some("TEAM_AGENT_AUTH_MODE")),
150
+ "audit must name the cleared keys; event={cleared}"
151
+ );
152
+ let serialized = cleared.to_string();
153
+ assert!(
154
+ !serialized.contains("stale-id") && !serialized.contains("stale-team"),
155
+ "audit must not carry key values; event={cleared}"
156
+ );
157
+ assert_eq!(
158
+ cleared.get("path").and_then(serde_json::Value::as_str),
159
+ Some(grok.join("config.toml").to_string_lossy().as_ref()),
160
+ "audit must name the file; event={cleared}"
161
+ );
162
+ }
163
+
164
+ fn sample_mcp_config(agent_id: &str, workspace: &str) -> McpConfig {
165
+ McpConfig {
166
+ raw: serde_json::json!({
167
+ "team_orchestrator": {
168
+ "command": "/bin/team-agent-test",
169
+ "args": ["mcp-server", "--workspace", workspace],
170
+ "env": {
171
+ "TEAM_AGENT_ID": agent_id,
172
+ "TEAM_AGENT_WORKSPACE": workspace,
173
+ "TEAM_AGENT_OWNER_TEAM_ID": "t1",
174
+ "TEAM_AGENT_AUTH_MODE": "subscription",
175
+ }
176
+ }
177
+ }),
178
+ }
179
+ }
180
+
181
+ #[test]
182
+ #[serial(env)]
183
+ fn two_grok_seats_coexist_when_toml_has_no_per_seat_keys() {
184
+ let ws = tmp_dir("grok-cwd-coexist");
185
+ let home = tmp_dir("grok-cwd-coexist-home");
186
+ seed_grok_home(&home, Some(&ws));
187
+ let _guard = HomeGuard::set(&home);
188
+ let team = write_grok_team_agents(&ws, "grokteam", &["g1", "g2"]);
189
+
190
+ quick_start_with_transport_in_workspace(
191
+ &ws,
192
+ &team,
193
+ None,
194
+ true,
195
+ Some("grokteam"),
196
+ &OfflineTransport::new(),
197
+ )
198
+ .expect("two grok seats must start when the shared toml has no per-seat keys");
199
+
200
+ let text = std::fs::read_to_string(ws.join(".grok/config.toml")).unwrap_or_default();
201
+ assert!(
202
+ !text.contains("TEAM_AGENT_ID")
203
+ && !text.contains("TEAM_AGENT_OWNER_TEAM_ID")
204
+ && !text.contains("TEAM_AGENT_AUTH_MODE"),
205
+ "overlay must not write per-seat keys; text={text}"
206
+ );
207
+ }
208
+
209
+ #[test]
210
+ #[serial(env)]
211
+ fn leftover_per_seat_key_is_cleared_before_start() {
212
+ let ws = tmp_dir("grok-cwd-per-seat");
213
+ let home = tmp_dir("grok-cwd-per-seat-home");
214
+ seed_grok_home(&home, Some(&ws));
215
+ let _guard = HomeGuard::set(&home);
216
+ let grok = ws.join(".grok");
217
+ std::fs::create_dir_all(&grok).unwrap();
218
+ std::fs::write(
219
+ grok.join("config.toml"),
220
+ "[mcp_servers.keep-me.env]\nTEAM_AGENT_FUTURE_SEAT_KEY = \"leaked\"\nGROK_FOLDER_TRUST = \"1\"\n",
221
+ )
222
+ .unwrap();
223
+ let team = write_grok_team_agents(&ws, "grokteam", &["g1", "g2"]);
224
+
225
+ quick_start_with_transport_in_workspace(
226
+ &ws,
227
+ &team,
228
+ None,
229
+ true,
230
+ Some("grokteam"),
231
+ &OfflineTransport::new(),
232
+ )
233
+ .expect("leftover per-seat keys must be cleared, not refuse start");
234
+
235
+ let text = std::fs::read_to_string(ws.join(".grok/config.toml")).unwrap_or_default();
236
+ assert!(
237
+ !text.contains("TEAM_AGENT_FUTURE_SEAT_KEY") && !text.contains("leaked"),
238
+ "upgrade migration must drop the leftover per-seat key; text={text}"
239
+ );
240
+ assert!(
241
+ text.contains("GROK_FOLDER_TRUST"),
242
+ "non-framework keys must stay; text={text}"
243
+ );
244
+ }
245
+
246
+ #[test]
247
+ fn overlay_preserves_unknown_env_keys_on_the_shared_table() {
248
+ let ws = tmp_dir("grok-mcp-keep-unknown");
249
+ let grok = ws.join(".grok");
250
+ std::fs::create_dir_all(&grok).unwrap();
251
+ std::fs::write(
252
+ grok.join("config.toml"),
253
+ r#"[mcp_servers.team_orchestrator]
254
+ command = "/old/team-agent"
255
+
256
+ [mcp_servers.team_orchestrator.env]
257
+ TEAM_AGENT_ID = "stale-id"
258
+ GROK_FOLDER_TRUST = "1"
259
+ USER_EXTRA = "keep-me"
260
+ TEAM_AGENT_WORKSPACE = "/stale-ws"
261
+ "#,
262
+ )
263
+ .unwrap();
264
+
265
+ apply_grok_mcp_overlay(&ws, &sample_mcp_config("keep-seat", "/ws-keep"))
266
+ .expect("overlay must keep unknown keys while dropping per-seat ones");
267
+ let text = std::fs::read_to_string(grok.join("config.toml")).unwrap();
268
+ assert!(
269
+ !text.contains("TEAM_AGENT_ID") && !text.contains("stale-id"),
270
+ "per-seat keys must leave; text={text}"
271
+ );
272
+ assert!(
273
+ text.contains("GROK_FOLDER_TRUST = \"1\"") && text.contains("USER_EXTRA = \"keep-me\""),
274
+ "unknown env keys must survive the stanza rewrite with the same value; text={text}"
275
+ );
276
+ assert!(
277
+ text.contains("TEAM_AGENT_WORKSPACE = \"/ws-keep\""),
278
+ "incoming workspace must win over the stale disk value; text={text}"
279
+ );
280
+ }
281
+
282
+ #[test]
283
+ #[serial(env)]
284
+ fn grok_untrusted_folder_refuses_to_start() {
285
+ let ws = tmp_dir("grok-untrusted");
286
+ let home = tmp_dir("grok-untrusted-home");
287
+ seed_grok_home(&home, None);
288
+ let _guard = HomeGuard::set(&home);
289
+ let team = write_grok_team(&ws, "grokteam", "grok_writer");
290
+ let err = quick_start_with_transport_in_workspace(
291
+ &ws,
292
+ &team,
293
+ None,
294
+ true,
295
+ Some("grokteam"),
296
+ &OfflineTransport::new(),
297
+ )
298
+ .expect_err("untrusted folder must not start a grok seat");
299
+ let text = err.to_string();
300
+ assert!(
301
+ text.contains("not trusted") && text.contains("action:"),
302
+ "untrusted-folder error must be actionable; err={text}"
303
+ );
304
+ assert!(
305
+ text.contains("grok --trust") || text.contains("/hooks-trust"),
306
+ "next step must name grok --trust or /hooks-trust; err={text}"
307
+ );
308
+ }
309
+
310
+ #[test]
311
+ #[serial(env)]
312
+ fn grok_missing_login_refuses_to_start() {
313
+ let ws = tmp_dir("grok-nologin");
314
+ let home = tmp_dir("grok-nologin-home");
315
+ std::fs::create_dir_all(home.join(".grok")).unwrap();
316
+ std::fs::write(
317
+ home.join(".grok").join("trusted_folders.toml"),
318
+ format!("[folders.\"{}\"]\ntrusted = true\n", ws.display()),
319
+ )
320
+ .unwrap();
321
+ let _guard = HomeGuard::set(&home);
322
+ let err = ensure_grok_login_and_folder_trust(&ws).expect_err("missing auth.json");
323
+ match err {
324
+ LifecycleError::RequirementUnmet(text) => {
325
+ assert!(
326
+ text.contains("grok login") && text.contains("action:"),
327
+ "login error must tell the operator to run grok login; err={text}"
328
+ );
329
+ }
330
+ other => panic!("expected RequirementUnmet, got {other:?}"),
331
+ }
332
+ }
333
+
334
+ #[test]
335
+ #[serial(env)]
336
+ fn grok_spawn_writes_resolved_team_agent_into_project_grok_config() {
337
+ let ws = tmp_dir("grok-mcp-overlay");
338
+ let home = tmp_dir("grok-mcp-overlay-home");
339
+ seed_grok_home(&home, Some(&ws));
340
+ let _guard = HomeGuard::set(&home);
341
+ let team = write_grok_team(&ws, "grokteam", "grok_writer");
342
+ let config_path = ws.join(".grok").join("config.toml");
343
+ assert!(
344
+ !config_path.exists(),
345
+ "precondition: project grok config must be absent before spawn; path={}",
346
+ config_path.display()
347
+ );
348
+
349
+ quick_start_with_transport_in_workspace(
350
+ &ws,
351
+ &team,
352
+ None,
353
+ true,
354
+ Some("grokteam"),
355
+ &OfflineTransport::new(),
356
+ )
357
+ .expect("grok quick-start through offline transport should spawn");
358
+
359
+ let text = std::fs::read_to_string(&config_path).unwrap_or_else(|err| {
360
+ panic!(
361
+ "grok spawn must materialize {}; grok CLI only reads this project-scope file: {err}",
362
+ config_path.display()
363
+ )
364
+ });
365
+ let expected_command = current_team_agent_command();
366
+ let workspace = ws.to_string_lossy();
367
+
368
+ assert!(
369
+ text.contains("[mcp_servers.team_orchestrator]"),
370
+ "grok project config must declare [mcp_servers.team_orchestrator]; path={} text={text}",
371
+ config_path.display()
372
+ );
373
+ assert!(
374
+ text.contains(&format!("command = \"{expected_command}\""))
375
+ || text.contains(&format!("command = '{expected_command}'")),
376
+ "command must be the running team-agent binary, not a PATH name; expected={expected_command} text={text}"
377
+ );
378
+ assert!(
379
+ text.contains("\"mcp-server\"") && text.contains("\"--workspace\""),
380
+ "args must launch mcp-server; text={text}"
381
+ );
382
+ assert!(
383
+ text.contains(&format!("\"{workspace}\""))
384
+ || text.contains(&format!("'{workspace}'")),
385
+ "args/--workspace must be the real workspace, not {{workspace}}; workspace={workspace} text={text}"
386
+ );
387
+ assert!(
388
+ !text.contains("{workspace}")
389
+ && !text.contains("{agent_id}")
390
+ && !text.contains("{team_id}"),
391
+ "placeholders must be resolved before grok reads the file; text={text}"
392
+ );
393
+ assert!(
394
+ text.contains("enabled = true"),
395
+ "grok mcp add --scope project writes enabled = true; text={text}"
396
+ );
397
+ assert!(
398
+ text.contains("[mcp_servers.team_orchestrator.env]"),
399
+ "identity env must be under [mcp_servers.team_orchestrator.env]; text={text}"
400
+ );
401
+ assert!(
402
+ !text.contains("mcp_servers.team-agent"),
403
+ "spawned overlay must not leave the misnamed team-agent table; text={text}"
404
+ );
405
+ assert!(
406
+ !text.contains("TEAM_AGENT_ID"),
407
+ "shared grok toml must not carry TEAM_AGENT_ID (pane env is the carrier); text={text}"
408
+ );
409
+ assert!(
410
+ text.contains(&format!("TEAM_AGENT_WORKSPACE = \"{workspace}\""))
411
+ || text.contains(&format!("TEAM_AGENT_WORKSPACE = '{workspace}'")),
412
+ "TEAM_AGENT_WORKSPACE must match spawn cwd; workspace={workspace} text={text}"
413
+ );
414
+ assert!(
415
+ !text.contains("TEAM_AGENT_OWNER_TEAM_ID") && !text.contains("TEAM_AGENT_AUTH_MODE"),
416
+ "shared grok toml must not carry per-seat OWNER/AUTH; text={text}"
417
+ );
418
+ }
419
+
420
+ fn current_team_agent_command() -> String {
421
+ let exe = std::env::current_exe().expect("current_exe");
422
+ match std::fs::canonicalize(&exe) {
423
+ Ok(canon) => canon.to_string_lossy().into_owned(),
424
+ Err(_) => exe.to_string_lossy().into_owned(),
425
+ }
426
+ }
427
+
428
+ fn write_grok_team(ws: &Path, team_key: &str, agent_id: &str) -> PathBuf {
429
+ write_grok_team_agents(ws, team_key, &[agent_id])
430
+ }
431
+
432
+ fn write_grok_team_agents(ws: &Path, team_key: &str, agent_ids: &[&str]) -> PathBuf {
433
+ let team = ws.join(team_key);
434
+ std::fs::create_dir_all(team.join("agents")).unwrap();
435
+ std::fs::write(
436
+ team.join("TEAM.md"),
437
+ format!(
438
+ "---\nname: {team_key}\nobjective: grok MCP overlay contract.\nprovider: grok\ndangerously_skip_permissions: false\n---\n\nTeam.\n"
439
+ ),
440
+ )
441
+ .unwrap();
442
+ for agent_id in agent_ids {
443
+ std::fs::write(
444
+ team.join("agents").join(format!("{agent_id}.md")),
445
+ format!(
446
+ "---\nname: {agent_id}\nrole: Grok Writer\nprovider: grok\nmodel: grok-4\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nWorker.\n"
447
+ ),
448
+ )
449
+ .unwrap();
450
+ }
451
+ team
452
+ }
453
+
454
+ fn seed_grok_home(home: &Path, trusted_cwd: Option<&Path>) {
455
+ let grok = home.join(".grok");
456
+ std::fs::create_dir_all(&grok).unwrap();
457
+ std::fs::write(grok.join("auth.json"), r#"{"test":"ok"}"#).unwrap();
458
+ if let Some(cwd) = trusted_cwd {
459
+ std::fs::write(
460
+ grok.join("trusted_folders.toml"),
461
+ format!("[folders.\"{}\"]\ntrusted = true\n", cwd.display()),
462
+ )
463
+ .unwrap();
464
+ }
465
+ }
466
+
467
+ struct HomeGuard {
468
+ prev: Option<String>,
469
+ }
470
+
471
+ impl HomeGuard {
472
+ fn set(home: &Path) -> Self {
473
+ let prev = std::env::var("HOME").ok();
474
+ std::env::set_var("HOME", home);
475
+ Self { prev }
476
+ }
477
+ }
478
+
479
+ impl Drop for HomeGuard {
480
+ fn drop(&mut self) {
481
+ match &self.prev {
482
+ Some(value) => std::env::set_var("HOME", value),
483
+ None => std::env::remove_var("HOME"),
484
+ }
485
+ }
486
+ }
487
+
488
+ fn tmp_dir(tag: &str) -> PathBuf {
489
+ static N: AtomicU64 = AtomicU64::new(0);
490
+ let dir = std::env::temp_dir().join(format!(
491
+ "ta-rs-grok-mcp-{tag}-{}-{}",
492
+ std::process::id(),
493
+ N.fetch_add(1, Ordering::Relaxed)
494
+ ));
495
+ let _ = std::fs::remove_dir_all(&dir);
496
+ std::fs::create_dir_all(&dir).unwrap();
497
+ std::fs::canonicalize(dir).unwrap()
498
+ }