@team-agent/installer 0.5.77 → 0.5.80
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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +161 -29
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +24 -8
- package/crates/team-agent/src/cli/mod.rs +543 -98
- package/crates/team-agent/src/cli/models.rs +393 -32
- package/crates/team-agent/src/cli/named_address.rs +19 -5
- package/crates/team-agent/src/cli/profile.rs +35 -4
- package/crates/team-agent/src/cli/spec.rs +7 -2
- package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
- package/crates/team-agent/src/cli/status_port/runtime.rs +3 -1
- package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
- package/crates/team-agent/src/cli/status_port/store.rs +1 -1
- package/crates/team-agent/src/cli/status_port/tests.rs +37 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +122 -2
- package/crates/team-agent/src/leader/lease.rs +492 -33
- package/crates/team-agent/src/leader/owner_bind.rs +40 -12
- package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
- package/crates/team-agent/src/leader/rediscover.rs +36 -6
- package/crates/team-agent/src/leader/start.rs +197 -88
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +86 -23
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
- package/crates/team-agent/src/lifecycle/launch.rs +30 -7
- package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
- package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
- package/crates/team-agent/src/lifecycle/restart.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt +9 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +71 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +140 -11
- package/crates/team-agent/src/messaging/delivery.rs +221 -80
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +127 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/provider/session_scan/common/tests.rs +204 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +24 -2
- package/crates/team-agent/src/state/abnormal_watch/tests/mod.rs +248 -0
- package/crates/team-agent/src/state/abnormal_watch.rs +144 -0
- package/crates/team-agent/src/state/mod.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +102 -4
- package/crates/team-agent/src/state/projection.rs +7 -1
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
- package/crates/team-agent/src/tmux_backend/tests.rs +149 -4
- package/crates/team-agent/src/tmux_backend.rs +239 -127
- package/crates/team-agent/src/transport.rs +78 -0
- package/crates/team-agent/src/transport_factory.rs +53 -0
- package/package.json +4 -5
- package/skills/team-agent/SKILL.md +45 -17
- package/skills/team-agent/command-coverage.json +4 -29
- package/npm/check-skill-links.mjs +0 -88
- package/skills/team-agent/references/team-agent-operator.md +0 -672
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//! purpose: cursor 席位必须把身份写进它实际会读的 mcp.json env
|
|
2
|
-
//! contract: overlay
|
|
3
|
-
//!
|
|
2
|
+
//! contract: 隔离默认开时 overlay 写 per-seat 工程根 `.cursor/mcp.json`(`--workspace` /
|
|
3
|
+
//! enable cwd 同一根);关隔离时写 `<workspace>/.cursor/mcp.json`。不是
|
|
4
|
+
//! `.team/runtime/mcp/*.json`,也不是靠 pane env 继承。
|
|
4
5
|
//! boundary: 只覆盖 cursor launch 产物;不改 claude/codex/copilot/grok 路径
|
|
5
6
|
//!
|
|
6
7
|
//! 生产侧判据,未经血统审计。
|
|
@@ -16,34 +17,22 @@ use std::path::{Path, PathBuf};
|
|
|
16
17
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
17
18
|
|
|
18
19
|
use serial_test::serial;
|
|
19
|
-
use sha2::{Digest, Sha256};
|
|
20
20
|
use team_agent::lifecycle::quick_start_with_transport_in_workspace;
|
|
21
21
|
use team_agent::lifecycle::{
|
|
22
22
|
apply_cursor_mcp_overlay, apply_cursor_spawn_workspace_pointers,
|
|
23
23
|
apply_cursor_subscription_proxy_env, apply_cursor_workspace_physical_path,
|
|
24
|
-
cursor_mcp_enable_argv,
|
|
25
|
-
|
|
24
|
+
cursor_mcp_enable_argv, cursor_mcp_enable_working_dir, cursor_mcp_json_path,
|
|
25
|
+
cursor_mcp_project_dir, physical_workspace_path, prepare_cursor_seat_mcp, LifecycleError,
|
|
26
26
|
};
|
|
27
27
|
use team_agent::provider::McpConfig;
|
|
28
28
|
use team_agent::transport::test_support::OfflineTransport;
|
|
29
29
|
|
|
30
30
|
#[test]
|
|
31
|
-
#[serial(cursor_mcp_isolation)]
|
|
32
31
|
fn cursor_overlay_writes_identity_into_mcp_json_env() {
|
|
33
32
|
let ws = tmp_dir("cursor-mcp-id");
|
|
34
33
|
apply_cursor_mcp_overlay(&ws, &sample_mcp_config("seat-a", &ws.to_string_lossy()))
|
|
35
34
|
.expect("overlay write");
|
|
36
|
-
let
|
|
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();
|
|
35
|
+
let text = std::fs::read_to_string(ws.join(".cursor/mcp.json")).unwrap();
|
|
47
36
|
assert!(
|
|
48
37
|
text.contains("\"team_orchestrator\""),
|
|
49
38
|
"cursor overlay must use the canonical server name"
|
|
@@ -71,7 +60,6 @@ fn cursor_overlay_writes_identity_into_mcp_json_env() {
|
|
|
71
60
|
}
|
|
72
61
|
|
|
73
62
|
#[test]
|
|
74
|
-
#[serial(cursor_mcp_isolation)]
|
|
75
63
|
fn cursor_overlay_refuses_missing_identity() {
|
|
76
64
|
let ws = tmp_dir("cursor-mcp-missing-id");
|
|
77
65
|
let cfg = McpConfig {
|
|
@@ -96,7 +84,6 @@ fn cursor_overlay_refuses_missing_identity() {
|
|
|
96
84
|
}
|
|
97
85
|
|
|
98
86
|
#[test]
|
|
99
|
-
#[serial(cursor_mcp_isolation)]
|
|
100
87
|
fn cursor_overlay_keeps_unrelated_servers_and_replaces_orchestrator() {
|
|
101
88
|
let ws = tmp_dir("cursor-mcp-merge");
|
|
102
89
|
let dir = ws.join(".cursor");
|
|
@@ -114,63 +101,38 @@ fn cursor_overlay_keeps_unrelated_servers_and_replaces_orchestrator() {
|
|
|
114
101
|
.unwrap();
|
|
115
102
|
|
|
116
103
|
apply_cursor_mcp_overlay(&ws, &sample_mcp_config("seat-b", "/ws-b")).expect("overlay merge");
|
|
117
|
-
let
|
|
104
|
+
let text = std::fs::read_to_string(dir.join("mcp.json")).unwrap();
|
|
118
105
|
assert!(
|
|
119
|
-
|
|
120
|
-
"unrelated project MCP servers must survive
|
|
106
|
+
text.contains("keep-me"),
|
|
107
|
+
"unrelated project MCP servers must survive"
|
|
121
108
|
);
|
|
122
109
|
assert!(
|
|
123
|
-
!
|
|
124
|
-
"stale orchestrator/legacy names must be
|
|
110
|
+
!text.contains("/old/team-agent") && !text.contains("/stale"),
|
|
111
|
+
"stale orchestrator/legacy names must be replaced"
|
|
125
112
|
);
|
|
126
|
-
let isolated = std::fs::read_to_string(cursor_mcp_json_path(&ws, "seat-b").unwrap()).unwrap();
|
|
127
113
|
assert!(
|
|
128
|
-
|
|
129
|
-
"new identity must land
|
|
114
|
+
text.contains("seat-b") && text.contains("/ws-b"),
|
|
115
|
+
"new identity must land"
|
|
130
116
|
);
|
|
131
117
|
}
|
|
132
118
|
|
|
133
119
|
#[test]
|
|
134
|
-
|
|
135
|
-
fn cursor_second_seat_keeps_both_identities_when_isolated() {
|
|
120
|
+
fn cursor_second_seat_overwrites_json_identity_without_exclusive_gate() {
|
|
136
121
|
let ws = tmp_dir("cursor-mcp-two");
|
|
137
122
|
apply_cursor_mcp_overlay(&ws, &sample_mcp_config("first", "/ws")).expect("first");
|
|
138
123
|
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
124
|
let text = std::fs::read_to_string(ws.join(".cursor/mcp.json")).unwrap();
|
|
161
|
-
restore_isolation_env(previous);
|
|
162
125
|
assert!(
|
|
163
126
|
text.contains("second"),
|
|
164
|
-
"
|
|
127
|
+
"last writer wins on a shared --workspace; no grok-style exclusive gate"
|
|
165
128
|
);
|
|
166
129
|
assert!(
|
|
167
130
|
!text.contains("\"first\""),
|
|
168
|
-
"
|
|
131
|
+
"previous seat identity must not remain in the single json file"
|
|
169
132
|
);
|
|
170
133
|
}
|
|
171
134
|
|
|
172
135
|
#[test]
|
|
173
|
-
#[serial(cursor_mcp_isolation)]
|
|
174
136
|
fn cursor_enable_argv_is_documented_subcommand_without_workspace_flag() {
|
|
175
137
|
let argv = cursor_mcp_enable_argv();
|
|
176
138
|
assert_eq!(
|
|
@@ -185,72 +147,6 @@ fn cursor_enable_argv_is_documented_subcommand_without_workspace_flag() {
|
|
|
185
147
|
}
|
|
186
148
|
|
|
187
149
|
#[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
150
|
fn cursor_workspace_flag_is_rewritten_to_physical_path() {
|
|
255
151
|
let ws = tmp_dir("cursor-phys");
|
|
256
152
|
let mut argv = vec![
|
|
@@ -264,29 +160,6 @@ fn cursor_workspace_flag_is_rewritten_to_physical_path() {
|
|
|
264
160
|
}
|
|
265
161
|
|
|
266
162
|
#[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
163
|
#[serial(env)]
|
|
291
164
|
fn cursor_subscription_proxy_copies_keys_without_requiring_profile() {
|
|
292
165
|
let prev = std::env::var("HTTPS_PROXY").ok();
|
|
@@ -312,14 +185,119 @@ fn cursor_subscription_proxy_copies_keys_without_requiring_profile() {
|
|
|
312
185
|
}
|
|
313
186
|
|
|
314
187
|
#[test]
|
|
315
|
-
|
|
188
|
+
fn cursor_subscription_direct_profile_unsets_only_proxy_url_keys() {
|
|
189
|
+
let ws = tmp_dir("cursor-profile-direct");
|
|
190
|
+
let profiles = ws.join(".team/current/profiles");
|
|
191
|
+
std::fs::create_dir_all(&profiles).unwrap();
|
|
192
|
+
std::fs::write(
|
|
193
|
+
profiles.join("cursor.env"),
|
|
194
|
+
"AUTH_MODE=subscription\nPROFILE_NAME=cursor\nPROXY_MODE=direct\n",
|
|
195
|
+
)
|
|
196
|
+
.unwrap();
|
|
197
|
+
|
|
198
|
+
let launch = crate::lifecycle::profile_launch::prepare_provider_profile_launch_from_json(
|
|
199
|
+
&ws,
|
|
200
|
+
"cursor",
|
|
201
|
+
&serde_json::json!({
|
|
202
|
+
"id": "cursor",
|
|
203
|
+
"provider": "cursor_agent",
|
|
204
|
+
"auth_mode": "subscription",
|
|
205
|
+
"profile": "cursor"
|
|
206
|
+
}),
|
|
207
|
+
None,
|
|
208
|
+
)
|
|
209
|
+
.expect("direct Cursor profile should prepare");
|
|
210
|
+
for key in [
|
|
211
|
+
"HTTPS_PROXY",
|
|
212
|
+
"HTTP_PROXY",
|
|
213
|
+
"ALL_PROXY",
|
|
214
|
+
"https_proxy",
|
|
215
|
+
"http_proxy",
|
|
216
|
+
"all_proxy",
|
|
217
|
+
"GLOBAL_AGENT_HTTPS_PROXY",
|
|
218
|
+
"GLOBAL_AGENT_HTTP_PROXY",
|
|
219
|
+
] {
|
|
220
|
+
assert!(launch.env_unset.contains(key), "direct must unset {key}");
|
|
221
|
+
}
|
|
222
|
+
assert!(!launch.env_unset.contains("NO_PROXY"));
|
|
223
|
+
assert!(!launch.env_unset.contains("no_proxy"));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[test]
|
|
227
|
+
fn cursor_mcp_enable_child_env_applies_direct_profile_unsets() {
|
|
228
|
+
let ws = tmp_dir("cursor-profile-enable-env");
|
|
229
|
+
let profiles = ws.join(".team/current/profiles");
|
|
230
|
+
std::fs::create_dir_all(&profiles).unwrap();
|
|
231
|
+
std::fs::write(
|
|
232
|
+
profiles.join("cursor.env"),
|
|
233
|
+
"AUTH_MODE=subscription\nPROFILE_NAME=cursor\nPROXY_MODE=direct\n",
|
|
234
|
+
)
|
|
235
|
+
.unwrap();
|
|
236
|
+
let launch = crate::lifecycle::profile_launch::prepare_provider_profile_launch_from_json(
|
|
237
|
+
&ws,
|
|
238
|
+
"cursor",
|
|
239
|
+
&serde_json::json!({
|
|
240
|
+
"id": "cursor",
|
|
241
|
+
"provider": "cursor_agent",
|
|
242
|
+
"auth_mode": "subscription",
|
|
243
|
+
"profile": "cursor"
|
|
244
|
+
}),
|
|
245
|
+
None,
|
|
246
|
+
)
|
|
247
|
+
.expect("direct Cursor profile should prepare");
|
|
248
|
+
|
|
249
|
+
let mut command = std::process::Command::new("/usr/bin/env");
|
|
250
|
+
command.env_clear();
|
|
251
|
+
for key in [
|
|
252
|
+
"HTTPS_PROXY",
|
|
253
|
+
"HTTP_PROXY",
|
|
254
|
+
"ALL_PROXY",
|
|
255
|
+
"https_proxy",
|
|
256
|
+
"http_proxy",
|
|
257
|
+
"all_proxy",
|
|
258
|
+
"GLOBAL_AGENT_HTTPS_PROXY",
|
|
259
|
+
"GLOBAL_AGENT_HTTP_PROXY",
|
|
260
|
+
] {
|
|
261
|
+
command.env(key, "fixture");
|
|
262
|
+
}
|
|
263
|
+
command.env("NO_PROXY", "fixture");
|
|
264
|
+
command.env("no_proxy", "fixture");
|
|
265
|
+
crate::lifecycle::launch::apply_cursor_mcp_enable_profile_env(
|
|
266
|
+
&mut command,
|
|
267
|
+
Some(&launch),
|
|
268
|
+
);
|
|
269
|
+
let output = command.output().expect("env child should run");
|
|
270
|
+
assert!(output.status.success());
|
|
271
|
+
let text = String::from_utf8(output.stdout).unwrap();
|
|
272
|
+
for key in [
|
|
273
|
+
"HTTPS_PROXY",
|
|
274
|
+
"HTTP_PROXY",
|
|
275
|
+
"ALL_PROXY",
|
|
276
|
+
"https_proxy",
|
|
277
|
+
"http_proxy",
|
|
278
|
+
"all_proxy",
|
|
279
|
+
"GLOBAL_AGENT_HTTPS_PROXY",
|
|
280
|
+
"GLOBAL_AGENT_HTTP_PROXY",
|
|
281
|
+
] {
|
|
282
|
+
assert!(
|
|
283
|
+
!text.lines().any(|line| line.starts_with(&format!("{key}="))),
|
|
284
|
+
"direct MCP child must omit {key}"
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
assert!(text.lines().any(|line| line == "NO_PROXY=fixture"));
|
|
288
|
+
assert!(text.lines().any(|line| line == "no_proxy=fixture"));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#[test]
|
|
292
|
+
#[serial(env)]
|
|
316
293
|
fn cursor_spawn_writes_identity_into_project_mcp_json() {
|
|
294
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter("cursor-mcp-spawn");
|
|
317
295
|
let ws = tmp_dir("cursor-mcp-spawn");
|
|
318
296
|
let team = write_cursor_team(&ws, "cursortm", "cursor_writer");
|
|
319
|
-
let
|
|
297
|
+
let selected = cursor_mcp_json_path(&ws, "cursor_writer").unwrap();
|
|
320
298
|
assert!(
|
|
321
|
-
!
|
|
322
|
-
"precondition: project
|
|
299
|
+
!selected.exists() && !ws.join(".cursor/mcp.json").exists(),
|
|
300
|
+
"precondition: selected project and workspace overlay must be absent before spawn"
|
|
323
301
|
);
|
|
324
302
|
|
|
325
303
|
quick_start_with_transport_in_workspace(
|
|
@@ -332,13 +310,66 @@ fn cursor_spawn_writes_identity_into_project_mcp_json() {
|
|
|
332
310
|
)
|
|
333
311
|
.expect("cursor quick-start through offline transport should spawn");
|
|
334
312
|
|
|
313
|
+
let text = std::fs::read_to_string(&selected).unwrap_or_else(|err| {
|
|
314
|
+
panic!(
|
|
315
|
+
"cursor spawn must materialize {}; isolation default reads this per-seat file: {err}",
|
|
316
|
+
selected.display()
|
|
317
|
+
)
|
|
318
|
+
});
|
|
319
|
+
assert_cursor_identity_overlay(&text, "cursor_writer", &ws);
|
|
320
|
+
assert!(
|
|
321
|
+
!ws.join(".cursor/mcp.json").exists(),
|
|
322
|
+
"default isolation must not dual-write the team workspace overlay"
|
|
323
|
+
);
|
|
324
|
+
let project = cursor_mcp_project_dir(&ws, "cursor_writer").unwrap();
|
|
325
|
+
let enable_cwd = cursor_mcp_enable_working_dir(&ws, Some(&project));
|
|
326
|
+
assert_eq!(enable_cwd, physical_workspace_path(&project));
|
|
327
|
+
let argv_workspace = spawn_argv_workspace_flag(&ws).expect("spawn argv --workspace");
|
|
328
|
+
assert_eq!(argv_workspace, physical_workspace_path(&project).to_string_lossy());
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#[test]
|
|
332
|
+
#[serial(env)]
|
|
333
|
+
fn cursor_spawn_isolation_off_writes_workspace_mcp_json() {
|
|
334
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter("cursor-mcp-spawn-legacy");
|
|
335
|
+
let key = "TEAM_AGENT_CURSOR_MCP_ISOLATION";
|
|
336
|
+
let prev = std::env::var(key).ok();
|
|
337
|
+
std::env::set_var(key, "0");
|
|
338
|
+
let ws = tmp_dir("cursor-mcp-spawn-legacy");
|
|
339
|
+
let team = write_cursor_team(&ws, "cursortm", "cursor_writer");
|
|
340
|
+
let result = quick_start_with_transport_in_workspace(
|
|
341
|
+
&ws,
|
|
342
|
+
&team,
|
|
343
|
+
None,
|
|
344
|
+
true,
|
|
345
|
+
Some("cursortm"),
|
|
346
|
+
&OfflineTransport::new(),
|
|
347
|
+
);
|
|
348
|
+
match prev {
|
|
349
|
+
Some(value) => std::env::set_var(key, value),
|
|
350
|
+
None => std::env::remove_var(key),
|
|
351
|
+
}
|
|
352
|
+
result.expect("legacy isolation-off cursor spawn");
|
|
353
|
+
let config_path = ws.join(".cursor").join("mcp.json");
|
|
335
354
|
let text = std::fs::read_to_string(&config_path).unwrap_or_else(|err| {
|
|
336
355
|
panic!(
|
|
337
|
-
"
|
|
356
|
+
"isolation-off spawn must write {}; cursor --workspace is the team root: {err}",
|
|
338
357
|
config_path.display()
|
|
339
358
|
)
|
|
340
359
|
});
|
|
341
|
-
|
|
360
|
+
assert_cursor_identity_overlay(&text, "cursor_writer", &ws);
|
|
361
|
+
assert!(
|
|
362
|
+
!cursor_mcp_json_path(&ws, "cursor_writer")
|
|
363
|
+
.unwrap()
|
|
364
|
+
.exists(),
|
|
365
|
+
"isolation-off must not materialize the per-seat project overlay"
|
|
366
|
+
);
|
|
367
|
+
let argv_workspace = spawn_argv_workspace_flag(&ws).expect("spawn argv --workspace");
|
|
368
|
+
assert_eq!(argv_workspace, physical_workspace_path(&ws).to_string_lossy());
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
fn assert_cursor_identity_overlay(text: &str, agent_id: &str, workspace: &Path) {
|
|
372
|
+
let workspace = workspace.to_string_lossy();
|
|
342
373
|
assert!(
|
|
343
374
|
text.contains("\"team_orchestrator\""),
|
|
344
375
|
"spawned overlay must declare team_orchestrator"
|
|
@@ -349,10 +380,10 @@ fn cursor_spawn_writes_identity_into_project_mcp_json() {
|
|
|
349
380
|
);
|
|
350
381
|
assert!(
|
|
351
382
|
text.contains(workspace.as_ref()),
|
|
352
|
-
"
|
|
383
|
+
"TEAM_AGENT_WORKSPACE / mcp args must be the real team path, not a placeholder"
|
|
353
384
|
);
|
|
354
385
|
assert!(
|
|
355
|
-
text.contains("\"TEAM_AGENT_ID\"") && text.contains(
|
|
386
|
+
text.contains("\"TEAM_AGENT_ID\"") && text.contains(agent_id),
|
|
356
387
|
"identity must be in json env"
|
|
357
388
|
);
|
|
358
389
|
assert!(
|
|
@@ -363,6 +394,27 @@ fn cursor_spawn_writes_identity_into_project_mcp_json() {
|
|
|
363
394
|
);
|
|
364
395
|
}
|
|
365
396
|
|
|
397
|
+
fn spawn_argv_workspace_flag(workspace: &Path) -> Option<String> {
|
|
398
|
+
let log = workspace.join(".team").join("logs").join("events.jsonl");
|
|
399
|
+
let text = std::fs::read_to_string(log).ok()?;
|
|
400
|
+
for line in text.lines().rev() {
|
|
401
|
+
let value: serde_json::Value = serde_json::from_str(line).ok()?;
|
|
402
|
+
if value.get("event").and_then(serde_json::Value::as_str)
|
|
403
|
+
!= Some("provider.worker.spawn_argv")
|
|
404
|
+
{
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
let argv = value.get("argv")?.as_array()?;
|
|
408
|
+
let mut items = argv.iter().filter_map(serde_json::Value::as_str);
|
|
409
|
+
while let Some(item) = items.next() {
|
|
410
|
+
if item == "--workspace" {
|
|
411
|
+
return items.next().map(str::to_string);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
None
|
|
416
|
+
}
|
|
417
|
+
|
|
366
418
|
fn write_cursor_team(ws: &Path, team_key: &str, agent_id: &str) -> PathBuf {
|
|
367
419
|
let team = ws.join(team_key);
|
|
368
420
|
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
@@ -400,6 +452,68 @@ fn sample_mcp_config(agent_id: &str, workspace: &str) -> McpConfig {
|
|
|
400
452
|
}
|
|
401
453
|
}
|
|
402
454
|
|
|
455
|
+
#[test]
|
|
456
|
+
#[serial(env)]
|
|
457
|
+
fn isolated_cursor_prepare_materializes_per_seat_project_not_workspace_overlay() {
|
|
458
|
+
let ws = tmp_dir("cursor-iso-clean");
|
|
459
|
+
let project = prepare_cursor_seat_mcp(&ws, "seat-iso", &sample_mcp_config("seat-iso", &ws.to_string_lossy()))
|
|
460
|
+
.expect("prepare isolated cursor")
|
|
461
|
+
.expect("isolation defaults on");
|
|
462
|
+
assert!(
|
|
463
|
+
project.ends_with("provider-config/seat-iso/cursor"),
|
|
464
|
+
"per-seat project must be materialized: {}",
|
|
465
|
+
project.display()
|
|
466
|
+
);
|
|
467
|
+
assert!(project.join(".cursor").is_dir());
|
|
468
|
+
let overlay = std::fs::read_to_string(cursor_mcp_json_path(&ws, "seat-iso").unwrap()).unwrap();
|
|
469
|
+
assert!(
|
|
470
|
+
overlay.contains("seat-iso") && overlay.contains("team_orchestrator"),
|
|
471
|
+
"overlay must land in the per-seat mcp.json"
|
|
472
|
+
);
|
|
473
|
+
assert!(
|
|
474
|
+
!ws.join(".cursor/mcp.json").exists(),
|
|
475
|
+
"clean workspace overlay must not be written when isolation is on"
|
|
476
|
+
);
|
|
477
|
+
let enable_cwd = cursor_mcp_enable_working_dir(&ws, Some(&project));
|
|
478
|
+
assert_eq!(enable_cwd, physical_workspace_path(&project));
|
|
479
|
+
let mut argv = vec![
|
|
480
|
+
"agent".to_string(),
|
|
481
|
+
"--workspace".to_string(),
|
|
482
|
+
ws.to_string_lossy().into_owned(),
|
|
483
|
+
];
|
|
484
|
+
apply_cursor_spawn_workspace_pointers(&mut argv, &ws, "seat-iso").unwrap();
|
|
485
|
+
let workspace_flag = argv
|
|
486
|
+
.windows(2)
|
|
487
|
+
.find(|pair| pair[0] == "--workspace")
|
|
488
|
+
.map(|pair| pair[1].as_str())
|
|
489
|
+
.unwrap();
|
|
490
|
+
assert_eq!(workspace_flag, physical_workspace_path(&project).to_string_lossy());
|
|
491
|
+
assert_eq!(
|
|
492
|
+
include_str!("../launch/spawn.rs").contains("prepare_cursor_seat_mcp("),
|
|
493
|
+
true,
|
|
494
|
+
"fresh spawn must call the shared materializer, not only compute the path"
|
|
495
|
+
);
|
|
496
|
+
assert!(
|
|
497
|
+
include_str!("../restart/common.rs").contains("prepare_cursor_seat_mcp("),
|
|
498
|
+
"restart spawn_agent_window must use the same materializer"
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
#[test]
|
|
503
|
+
fn cursor_enable_working_dir_does_not_depend_on_cfg_test_skip() {
|
|
504
|
+
let ws = tmp_dir("cursor-enable-cwd");
|
|
505
|
+
let project = ws.join(".team/runtime/provider-config/seat/cursor");
|
|
506
|
+
std::fs::create_dir_all(&project).unwrap();
|
|
507
|
+
assert_eq!(
|
|
508
|
+
cursor_mcp_enable_working_dir(&ws, Some(&project)),
|
|
509
|
+
physical_workspace_path(&project)
|
|
510
|
+
);
|
|
511
|
+
assert_eq!(
|
|
512
|
+
cursor_mcp_enable_working_dir(&ws, None),
|
|
513
|
+
physical_workspace_path(&ws)
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
403
517
|
fn tmp_dir(tag: &str) -> PathBuf {
|
|
404
518
|
static N: AtomicU64 = AtomicU64::new(0);
|
|
405
519
|
// 见 gate_fixtures::scratch_dir:⛔ 不硬编码绝对路径,默认标准临时目录。
|
|
@@ -415,50 +529,3 @@ fn tmp_dir(tag: &str) -> PathBuf {
|
|
|
415
529
|
std::fs::create_dir_all(&dir).unwrap();
|
|
416
530
|
std::fs::canonicalize(dir).unwrap()
|
|
417
531
|
}
|
|
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
|
-
}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
//! what: 写了 model 则该值进入 `--model <值>`
|
|
9
9
|
//! - name: cursor-effort-refuses-compile
|
|
10
10
|
//! what: 角色写 effort 则失败,不得丢掉后仍起席
|
|
11
|
-
//! - name: cursor-second-seat-
|
|
12
|
-
//! what:
|
|
11
|
+
//! - name: cursor-second-seat-refuses
|
|
12
|
+
//! what: 同 workspace 第二 CursorAgent 拒绝,文案含 TEAM_AGENT_ID/mcp.json
|
|
13
13
|
//! boundary:
|
|
14
14
|
//! - 只钉 Provider::CursorAgent;不改 grok/claude 路径
|
|
15
15
|
//! maturity: wired
|
|
@@ -26,7 +26,6 @@ use std::path::{Path, PathBuf};
|
|
|
26
26
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
27
27
|
|
|
28
28
|
use serial_test::serial;
|
|
29
|
-
use team_agent::lifecycle::cursor_mcp_json_path;
|
|
30
29
|
use team_agent::lifecycle::quick_start_with_transport_in_workspace;
|
|
31
30
|
use team_agent::transport::test_support::OfflineTransport;
|
|
32
31
|
|
|
@@ -67,6 +66,7 @@ fn cursor_role_missing_model_refuses_to_start() {
|
|
|
67
66
|
#[test]
|
|
68
67
|
#[serial(env)]
|
|
69
68
|
fn cursor_role_explicit_model_reaches_argv() {
|
|
69
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter("cursor-with-model");
|
|
70
70
|
let ws = tmp_dir("cursor-with-model");
|
|
71
71
|
let team = write_role_team(
|
|
72
72
|
&ws,
|
|
@@ -127,9 +127,8 @@ fn cursor_role_with_effort_refuses_to_start() {
|
|
|
127
127
|
|
|
128
128
|
#[test]
|
|
129
129
|
#[serial(env)]
|
|
130
|
-
fn
|
|
131
|
-
let
|
|
132
|
-
std::env::remove_var("TEAM_AGENT_CURSOR_MCP_ISOLATION");
|
|
130
|
+
fn cursor_second_seat_in_same_workspace_is_refused() {
|
|
131
|
+
let _hermetic = hermetic_guard::HermeticTestEnv::enter("cursor-two");
|
|
133
132
|
let ws = tmp_dir("cursor-two");
|
|
134
133
|
let team = ws.join("cursortm");
|
|
135
134
|
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
@@ -156,36 +155,21 @@ fn cursor_second_seat_in_same_workspace_uses_isolated_identity() {
|
|
|
156
155
|
Some("cursortm"),
|
|
157
156
|
&transport,
|
|
158
157
|
);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
records.len(),
|
|
164
|
-
2,
|
|
165
|
-
"both Cursor seats must spawn; records={records:?}"
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
let first = std::fs::read_to_string(cursor_mcp_json_path(&ws, "cursor_a").unwrap())
|
|
169
|
-
.expect("first seat MCP config");
|
|
170
|
-
let second = std::fs::read_to_string(cursor_mcp_json_path(&ws, "cursor_b").unwrap())
|
|
171
|
-
.expect("second seat MCP config");
|
|
158
|
+
let err = match result {
|
|
159
|
+
Ok(report) => panic!("second cursor seat must refuse; report={report:?}"),
|
|
160
|
+
Err(error) => error.to_string(),
|
|
161
|
+
};
|
|
172
162
|
assert!(
|
|
173
|
-
|
|
174
|
-
"
|
|
163
|
+
err.contains("TEAM_AGENT_ID") && err.contains("mcp.json"),
|
|
164
|
+
"error must name the last-writer identity file; err={err}"
|
|
175
165
|
);
|
|
176
166
|
assert!(
|
|
177
|
-
|
|
178
|
-
"
|
|
167
|
+
transport.spawn_records().is_empty(),
|
|
168
|
+
"must refuse before any cursor spawn; records={:?}",
|
|
169
|
+
transport.spawn_records()
|
|
179
170
|
);
|
|
180
171
|
}
|
|
181
172
|
|
|
182
|
-
fn restore_isolation_env(previous: Option<String>) {
|
|
183
|
-
match previous {
|
|
184
|
-
Some(value) => std::env::set_var("TEAM_AGENT_CURSOR_MCP_ISOLATION", value),
|
|
185
|
-
None => std::env::remove_var("TEAM_AGENT_CURSOR_MCP_ISOLATION"),
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
173
|
fn first_spawn_argv(transport: &OfflineTransport) -> Vec<String> {
|
|
190
174
|
let records = transport.spawn_records();
|
|
191
175
|
assert!(
|