@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
@@ -0,0 +1,464 @@
1
+ //! purpose: cursor 席位必须把身份写进它实际会读的 mcp.json env
2
+ //! contract: overlay 后 provider-config/<id>/cursor/.cursor/mcp.json 含 team_orchestrator.env.TEAM_AGENT_ID
3
+ //! (不是改 HOME,也不是靠 pane env 继承)
4
+ //! boundary: 只覆盖 cursor launch 产物;不改 claude/codex/copilot/grok 路径
5
+ //!
6
+ //! 生产侧判据,未经血统审计。
7
+
8
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
9
+
10
+ #[path = "../../../tests/support/hermetic.rs"]
11
+ mod hermetic_guard;
12
+ #[allow(dead_code)]
13
+ fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
14
+
15
+ use std::path::{Path, PathBuf};
16
+ use std::sync::atomic::{AtomicU64, Ordering};
17
+
18
+ use serial_test::serial;
19
+ use sha2::{Digest, Sha256};
20
+ use team_agent::lifecycle::quick_start_with_transport_in_workspace;
21
+ use team_agent::lifecycle::{
22
+ apply_cursor_mcp_overlay, apply_cursor_spawn_workspace_pointers,
23
+ apply_cursor_subscription_proxy_env, apply_cursor_workspace_physical_path,
24
+ cursor_mcp_enable_argv, cursor_mcp_json_path, cursor_mcp_project_dir, physical_workspace_path,
25
+ refuse_second_cursor_occupant, LifecycleError,
26
+ };
27
+ use team_agent::provider::McpConfig;
28
+ use team_agent::transport::test_support::OfflineTransport;
29
+
30
+ #[test]
31
+ #[serial(cursor_mcp_isolation)]
32
+ fn cursor_overlay_writes_identity_into_mcp_json_env() {
33
+ let ws = tmp_dir("cursor-mcp-id");
34
+ apply_cursor_mcp_overlay(&ws, &sample_mcp_config("seat-a", &ws.to_string_lossy()))
35
+ .expect("overlay write");
36
+ let path = cursor_mcp_json_path(&ws, "seat-a").unwrap();
37
+ let rendered = path.to_string_lossy();
38
+ assert!(
39
+ rendered.contains("provider-config") && rendered.contains("/cursor/"),
40
+ "identity must land under provider-config/<id>/cursor, not a HOME fork: {rendered}"
41
+ );
42
+ assert!(
43
+ !rendered.contains("/.team/runtime/cursor-mcp/"),
44
+ "must not use the rejected COPILOT_HOME-style cursor-mcp tree: {rendered}"
45
+ );
46
+ let text = std::fs::read_to_string(&path).unwrap();
47
+ assert!(
48
+ text.contains("\"team_orchestrator\""),
49
+ "cursor overlay must use the canonical server name"
50
+ );
51
+ assert!(
52
+ text.contains("\"TEAM_AGENT_ID\"") && text.contains("seat-a"),
53
+ "cursor must write TEAM_AGENT_ID into json env; pane env is not inherited"
54
+ );
55
+ assert!(
56
+ text.contains("\"TEAM_AGENT_OWNER_TEAM_ID\"") && text.contains("t1"),
57
+ "owner team must live in json env"
58
+ );
59
+ assert!(
60
+ text.contains("\"TEAM_AGENT_AUTH_MODE\"") && text.contains("subscription"),
61
+ "auth mode must live in json env"
62
+ );
63
+ assert!(
64
+ !text.contains("{workspace}") && !text.contains("{agent_id}"),
65
+ "placeholders must already be resolved"
66
+ );
67
+ assert!(
68
+ !text.contains("\"team-agent\""),
69
+ "legacy inbound key must not remain as a server name"
70
+ );
71
+ }
72
+
73
+ #[test]
74
+ #[serial(cursor_mcp_isolation)]
75
+ fn cursor_overlay_refuses_missing_identity() {
76
+ let ws = tmp_dir("cursor-mcp-missing-id");
77
+ let cfg = McpConfig {
78
+ raw: serde_json::json!({
79
+ "team_orchestrator": {
80
+ "command": "/bin/team-agent-test",
81
+ "args": ["mcp-server"],
82
+ "env": { "TEAM_AGENT_WORKSPACE": "/ws" }
83
+ }
84
+ }),
85
+ };
86
+ let err = apply_cursor_mcp_overlay(&ws, &cfg).expect_err("missing TEAM_AGENT_ID must fail");
87
+ match err {
88
+ LifecycleError::StatePersist(text) => {
89
+ assert!(
90
+ text.contains("TEAM_AGENT_ID"),
91
+ "refusal must name the missing identity key"
92
+ );
93
+ }
94
+ other => panic!("expected StatePersist, got {other:?}"),
95
+ }
96
+ }
97
+
98
+ #[test]
99
+ #[serial(cursor_mcp_isolation)]
100
+ fn cursor_overlay_keeps_unrelated_servers_and_replaces_orchestrator() {
101
+ let ws = tmp_dir("cursor-mcp-merge");
102
+ let dir = ws.join(".cursor");
103
+ std::fs::create_dir_all(&dir).unwrap();
104
+ std::fs::write(
105
+ dir.join("mcp.json"),
106
+ r#"{
107
+ "mcpServers": {
108
+ "keep-me": { "command": "/bin/keep" },
109
+ "team-agent": { "command": "/old/team-agent" },
110
+ "team_orchestrator": { "command": "/stale" }
111
+ }
112
+ }"#,
113
+ )
114
+ .unwrap();
115
+
116
+ apply_cursor_mcp_overlay(&ws, &sample_mcp_config("seat-b", "/ws-b")).expect("overlay merge");
117
+ let project = std::fs::read_to_string(dir.join("mcp.json")).unwrap();
118
+ assert!(
119
+ project.contains("keep-me"),
120
+ "unrelated project MCP servers must survive in workspace mcp.json"
121
+ );
122
+ assert!(
123
+ !project.contains("/old/team-agent") && !project.contains("/stale"),
124
+ "stale orchestrator/legacy names must be scrubbed from the shared project file"
125
+ );
126
+ let isolated = std::fs::read_to_string(cursor_mcp_json_path(&ws, "seat-b").unwrap()).unwrap();
127
+ assert!(
128
+ isolated.contains("seat-b") && isolated.contains("/ws-b"),
129
+ "new identity must land in the per-seat mcp.json"
130
+ );
131
+ }
132
+
133
+ #[test]
134
+ #[serial(cursor_mcp_isolation)]
135
+ fn cursor_second_seat_keeps_both_identities_when_isolated() {
136
+ let ws = tmp_dir("cursor-mcp-two");
137
+ apply_cursor_mcp_overlay(&ws, &sample_mcp_config("first", "/ws")).expect("first");
138
+ apply_cursor_mcp_overlay(&ws, &sample_mcp_config("second", "/ws")).expect("second");
139
+ let first = std::fs::read_to_string(cursor_mcp_json_path(&ws, "first").unwrap()).unwrap();
140
+ let second = std::fs::read_to_string(cursor_mcp_json_path(&ws, "second").unwrap()).unwrap();
141
+ assert!(
142
+ first.contains("\"first\"") && !first.contains("\"second\""),
143
+ "seat first must keep its TEAM_AGENT_ID"
144
+ );
145
+ assert!(
146
+ second.contains("\"second\"") && !second.contains("\"first\""),
147
+ "seat second must keep its TEAM_AGENT_ID"
148
+ );
149
+ }
150
+
151
+ #[test]
152
+ #[serial(cursor_mcp_isolation)]
153
+ #[serial(env)]
154
+ fn cursor_shared_overlay_last_writer_is_the_destruction_tooth() {
155
+ let previous = std::env::var("TEAM_AGENT_CURSOR_MCP_ISOLATION").ok();
156
+ std::env::set_var("TEAM_AGENT_CURSOR_MCP_ISOLATION", "0");
157
+ let ws = tmp_dir("cursor-mcp-shared-red");
158
+ apply_cursor_mcp_overlay(&ws, &sample_mcp_config("first", "/ws")).expect("first");
159
+ apply_cursor_mcp_overlay(&ws, &sample_mcp_config("second", "/ws")).expect("second");
160
+ let text = std::fs::read_to_string(ws.join(".cursor/mcp.json")).unwrap();
161
+ restore_isolation_env(previous);
162
+ assert!(
163
+ text.contains("second"),
164
+ "destruction: shared mcp.json last writer is second"
165
+ );
166
+ assert!(
167
+ !text.contains("\"first\""),
168
+ "destruction: criterion 2 goes red — first identity is gone"
169
+ );
170
+ }
171
+
172
+ #[test]
173
+ #[serial(cursor_mcp_isolation)]
174
+ fn cursor_enable_argv_is_documented_subcommand_without_workspace_flag() {
175
+ let argv = cursor_mcp_enable_argv();
176
+ assert_eq!(
177
+ argv,
178
+ vec![
179
+ "agent".to_string(),
180
+ "mcp".to_string(),
181
+ "enable".to_string(),
182
+ "team_orchestrator".to_string()
183
+ ]
184
+ );
185
+ }
186
+
187
+ #[test]
188
+ #[serial(cursor_mcp_isolation)]
189
+ fn cursor_enable_failure_preserves_cwd_config_identity_and_redacted_stderr() {
190
+ let ws = tmp_dir("cursor-enable-diagnostic");
191
+ apply_cursor_mcp_overlay(
192
+ &ws,
193
+ &sample_mcp_config("diagnostic-seat", &ws.to_string_lossy()),
194
+ )
195
+ .expect("write fake cursor overlay");
196
+ let cwd = physical_workspace_path(&cursor_mcp_project_dir(&ws, "diagnostic-seat").unwrap());
197
+ let config_path = cwd.join(".cursor/mcp.json");
198
+ let config_body = std::fs::read(&config_path).expect("read fake overlay identity");
199
+ let config_metadata = std::fs::metadata(&config_path).expect("fake overlay metadata");
200
+ let expected_hash = format!("{:x}", Sha256::digest(&config_body));
201
+ let benign = format!("benign first-enable diagnostic {}", "x".repeat(700));
202
+ let secret_value = "must-not-leak-secret-value";
203
+ let stderr =
204
+ format!("Authorization: Bearer sk-{secret_value} token={secret_value}\n{benign}\n");
205
+ let argv = cursor_mcp_enable_argv();
206
+ let error = LifecycleError::cursor_mcp_enable_failure(
207
+ &argv,
208
+ &cwd,
209
+ Some(23),
210
+ b"out",
211
+ stderr.as_bytes(),
212
+ None,
213
+ );
214
+ let LifecycleError::RequirementUnmet(report) = error else {
215
+ panic!("expected RequirementUnmet diagnostic")
216
+ };
217
+
218
+ assert!(report.contains("argv: agent mcp enable team_orchestrator"));
219
+ assert!(report.contains(&format!("cwd: {}", cwd.display())));
220
+ assert!(report.contains(&format!("config_path: {}", config_path.display())));
221
+ assert!(report.contains("config_exists: true"));
222
+ assert!(report.contains(&format!("config_size: {}", config_body.len())));
223
+ assert!(report.contains(&format!("config_sha256: {expected_hash}")));
224
+ assert!(report.contains("stdout_len: 3"));
225
+ assert!(report.contains(&format!("stderr_len: {}", stderr.len())));
226
+ #[cfg(unix)]
227
+ {
228
+ use std::os::unix::fs::PermissionsExt;
229
+ assert!(report.contains(&format!(
230
+ "config_mode: 0o{:o}",
231
+ config_metadata.permissions().mode()
232
+ )));
233
+ }
234
+ #[cfg(not(unix))]
235
+ assert!(report.contains("config_mode: unavailable_non_unix"));
236
+
237
+ let safe = report
238
+ .lines()
239
+ .find_map(|line| line.strip_prefix("stderr_first_safe: "))
240
+ .expect("bounded benign stderr must be retained");
241
+ assert!(safe.starts_with("benign first-enable diagnostic"));
242
+ assert_eq!(safe.len(), 512, "safe stderr must be capped at 512 bytes");
243
+ assert!(!report.contains("Authorization"));
244
+ assert!(!report.contains("Bearer"));
245
+ assert!(!report.contains(secret_value));
246
+ assert!(
247
+ !report.contains("TEAM_AGENT_AUTH_MODE"),
248
+ "diagnostic must not include overlay JSON contents"
249
+ );
250
+ }
251
+
252
+ #[test]
253
+ #[serial(cursor_mcp_isolation)]
254
+ fn cursor_workspace_flag_is_rewritten_to_physical_path() {
255
+ let ws = tmp_dir("cursor-phys");
256
+ let mut argv = vec![
257
+ "agent".to_string(),
258
+ "--workspace".to_string(),
259
+ ws.to_string_lossy().into_owned(),
260
+ ];
261
+ apply_cursor_workspace_physical_path(&mut argv, &ws);
262
+ let physical = physical_workspace_path(&ws);
263
+ assert_eq!(argv[2], physical.to_string_lossy().as_ref());
264
+ }
265
+
266
+ #[test]
267
+ #[serial(cursor_mcp_isolation)]
268
+ fn cursor_spawn_pointers_use_per_seat_workspace_and_add_dir() {
269
+ let ws = tmp_dir("cursor-pointers");
270
+ apply_cursor_mcp_overlay(&ws, &sample_mcp_config("seat-p", &ws.to_string_lossy()))
271
+ .expect("overlay");
272
+ let mut argv = vec![
273
+ "agent".to_string(),
274
+ "--workspace".to_string(),
275
+ ws.to_string_lossy().into_owned(),
276
+ ];
277
+ apply_cursor_spawn_workspace_pointers(&mut argv, &ws, "seat-p").expect("pointers");
278
+ let project = physical_workspace_path(&cursor_mcp_project_dir(&ws, "seat-p").unwrap());
279
+ let team = physical_workspace_path(&ws);
280
+ assert_eq!(argv[2], project.to_string_lossy().as_ref());
281
+ assert!(
282
+ argv.windows(2)
283
+ .any(|pair| pair[0] == "--add-dir" && pair[1] == team.to_string_lossy().as_ref()),
284
+ "true workspace must be added with documented --add-dir: {argv:?}"
285
+ );
286
+ }
287
+
288
+ #[test]
289
+ #[serial(cursor_mcp_isolation)]
290
+ #[serial(env)]
291
+ fn cursor_subscription_proxy_copies_keys_without_requiring_profile() {
292
+ let prev = std::env::var("HTTPS_PROXY").ok();
293
+ std::env::set_var("HTTPS_PROXY", "http://127.0.0.1:9");
294
+ let mut env = std::collections::BTreeMap::new();
295
+ let presence = apply_cursor_subscription_proxy_env(&mut env);
296
+ let has_key = env.contains_key("HTTPS_PROXY");
297
+ let value_len = env.get("HTTPS_PROXY").map(String::len);
298
+ match prev {
299
+ Some(value) => std::env::set_var("HTTPS_PROXY", value),
300
+ None => std::env::remove_var("HTTPS_PROXY"),
301
+ }
302
+ assert!(
303
+ presence.https_proxy,
304
+ "presence must be true when the process has HTTPS_PROXY"
305
+ );
306
+ assert!(has_key, "subscription env must receive the proxy key");
307
+ assert_eq!(
308
+ value_len,
309
+ Some(18),
310
+ "copied value length must match the fixture"
311
+ );
312
+ }
313
+
314
+ #[test]
315
+ #[serial(cursor_mcp_isolation)]
316
+ fn cursor_spawn_writes_identity_into_project_mcp_json() {
317
+ let ws = tmp_dir("cursor-mcp-spawn");
318
+ let team = write_cursor_team(&ws, "cursortm", "cursor_writer");
319
+ let config_path = cursor_mcp_json_path(&ws, "cursor_writer").expect("iso path");
320
+ assert!(
321
+ !config_path.exists(),
322
+ "precondition: project cursor mcp.json must be absent before spawn"
323
+ );
324
+
325
+ quick_start_with_transport_in_workspace(
326
+ &ws,
327
+ &team,
328
+ None,
329
+ true,
330
+ Some("cursortm"),
331
+ &OfflineTransport::new(),
332
+ )
333
+ .expect("cursor quick-start through offline transport should spawn");
334
+
335
+ let text = std::fs::read_to_string(&config_path).unwrap_or_else(|err| {
336
+ panic!(
337
+ "cursor spawn must materialize per-seat {}; cursor reads --workspace/.cursor/mcp.json: {err}",
338
+ config_path.display()
339
+ )
340
+ });
341
+ let workspace = ws.to_string_lossy();
342
+ assert!(
343
+ text.contains("\"team_orchestrator\""),
344
+ "spawned overlay must declare team_orchestrator"
345
+ );
346
+ assert!(
347
+ text.contains("\"mcp-server\"") && text.contains("\"--workspace\""),
348
+ "args must launch mcp-server"
349
+ );
350
+ assert!(
351
+ text.contains(workspace.as_ref()),
352
+ "workspace must be the real path, not a placeholder"
353
+ );
354
+ assert!(
355
+ text.contains("\"TEAM_AGENT_ID\"") && text.contains("cursor_writer"),
356
+ "identity must be in json env"
357
+ );
358
+ assert!(
359
+ !text.contains("{workspace}")
360
+ && !text.contains("{agent_id}")
361
+ && !text.contains("{team_id}"),
362
+ "placeholders must be resolved before cursor reads the file"
363
+ );
364
+ }
365
+
366
+ fn write_cursor_team(ws: &Path, team_key: &str, agent_id: &str) -> PathBuf {
367
+ let team = ws.join(team_key);
368
+ std::fs::create_dir_all(team.join("agents")).unwrap();
369
+ std::fs::write(
370
+ team.join("TEAM.md"),
371
+ format!(
372
+ "---\nname: {team_key}\nobjective: cursor MCP overlay contract.\nprovider: cursor_agent\ndangerously_skip_permissions: true\n---\n\nTeam.\n"
373
+ ),
374
+ )
375
+ .unwrap();
376
+ std::fs::write(
377
+ team.join("agents").join(format!("{agent_id}.md")),
378
+ format!(
379
+ "---\nname: {agent_id}\nrole: Cursor Writer\nprovider: cursor_agent\nmodel: sonnet-4-thinking\nauth_mode: subscription\ndangerously_skip_permissions: true\ntools:\n - mcp_team\n---\n\nWorker.\n"
380
+ ),
381
+ )
382
+ .unwrap();
383
+ team
384
+ }
385
+
386
+ fn sample_mcp_config(agent_id: &str, workspace: &str) -> McpConfig {
387
+ McpConfig {
388
+ raw: serde_json::json!({
389
+ "team_orchestrator": {
390
+ "command": "/bin/team-agent-test",
391
+ "args": ["mcp-server", "--workspace", workspace],
392
+ "env": {
393
+ "TEAM_AGENT_ID": agent_id,
394
+ "TEAM_AGENT_WORKSPACE": workspace,
395
+ "TEAM_AGENT_OWNER_TEAM_ID": "t1",
396
+ "TEAM_AGENT_AUTH_MODE": "subscription",
397
+ }
398
+ }
399
+ }),
400
+ }
401
+ }
402
+
403
+ fn tmp_dir(tag: &str) -> PathBuf {
404
+ static N: AtomicU64 = AtomicU64::new(0);
405
+ // 见 gate_fixtures::scratch_dir:⛔ 不硬编码绝对路径,默认标准临时目录。
406
+ let root = std::env::var_os("TEAM_AGENT_TEST_TMP")
407
+ .map(PathBuf::from)
408
+ .unwrap_or_else(std::env::temp_dir);
409
+ let dir = root.join(format!(
410
+ "ta-rs-cursor-mcp-{tag}-{}-{}",
411
+ std::process::id(),
412
+ N.fetch_add(1, Ordering::Relaxed)
413
+ ));
414
+ let _ = std::fs::remove_dir_all(&dir);
415
+ std::fs::create_dir_all(&dir).unwrap();
416
+ std::fs::canonicalize(dir).unwrap()
417
+ }
418
+
419
+ fn restore_isolation_env(previous: Option<String>) {
420
+ match previous {
421
+ Some(value) => std::env::set_var("TEAM_AGENT_CURSOR_MCP_ISOLATION", value),
422
+ None => std::env::remove_var("TEAM_AGENT_CURSOR_MCP_ISOLATION"),
423
+ }
424
+ }
425
+
426
+ #[test]
427
+ #[serial(cursor_mcp_isolation)]
428
+ fn refuse_second_allows_when_isolation_on() {
429
+ let ws = tmp_dir("cursor-refuse-iso-on");
430
+ let spec = team_agent::model::yaml::loads(
431
+ "agents:\n - id: a\n provider: cursor_agent\n - id: b\n provider: cursor_agent\n",
432
+ )
433
+ .expect("spec");
434
+ refuse_second_cursor_occupant(&ws, "b", Some(&spec)).expect("isolation on allows second");
435
+ }
436
+
437
+ #[test]
438
+ #[serial(cursor_mcp_isolation)]
439
+ #[serial(env)]
440
+ fn refuse_second_still_blocks_when_isolation_disabled() {
441
+ let previous = std::env::var("TEAM_AGENT_CURSOR_MCP_ISOLATION").ok();
442
+ std::env::set_var("TEAM_AGENT_CURSOR_MCP_ISOLATION", "0");
443
+ let ws = tmp_dir("cursor-refuse-iso-off");
444
+ let spec = team_agent::model::yaml::loads(
445
+ "agents:\n - id: a\n provider: cursor_agent\n - id: b\n provider: cursor_agent\n",
446
+ )
447
+ .expect("spec");
448
+ let err = refuse_second_cursor_occupant(&ws, "b", Some(&spec));
449
+ restore_isolation_env(previous);
450
+ let err = err.expect_err("isolation off must fail-closed");
451
+ match err {
452
+ LifecycleError::RequirementUnmet(text) => {
453
+ assert!(
454
+ text.contains("cursor_agent seat already occupies this workspace"),
455
+ "fail-closed text must stay: {text}"
456
+ );
457
+ assert!(
458
+ text.contains("last-writer"),
459
+ "reason must still name last-writer: {text}"
460
+ );
461
+ }
462
+ other => panic!("expected RequirementUnmet, got {other:?}"),
463
+ }
464
+ }