@team-agent/installer 0.5.84 → 0.5.85
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/profile.rs +4 -12
- package/crates/team-agent/src/cli/tests/verb_profile.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +1 -31
- package/crates/team-agent/src/lifecycle/profile_launch.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +179 -8
- package/crates/team-agent/src/messaging/results/collection_tests.rs +719 -0
- package/crates/team-agent/src/messaging/results.rs +104 -27
- package/crates/team-agent/src/provider/adapters/copilot.rs +1 -1
- package/crates/team-agent/src/state/persist.rs +15 -1
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -222,18 +222,10 @@ fn set_owner_only_permissions(path: &Path) -> Result<(), CliError> {
|
|
|
222
222
|
|
|
223
223
|
fn read_profile_env(path: &Path) -> Result<Map<String, Value>, CliError> {
|
|
224
224
|
let raw = std::fs::read_to_string(path)?;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
let Some((key, value)) = trimmed.split_once('=') else {
|
|
232
|
-
continue;
|
|
233
|
-
};
|
|
234
|
-
values.insert(key.to_string(), Value::String(value.to_string()));
|
|
235
|
-
}
|
|
236
|
-
Ok(values)
|
|
225
|
+
Ok(crate::lifecycle::profile_launch::parse_env_text(&raw)
|
|
226
|
+
.into_iter()
|
|
227
|
+
.map(|(key, value)| (key, Value::String(value)))
|
|
228
|
+
.collect())
|
|
237
229
|
}
|
|
238
230
|
|
|
239
231
|
fn redacted_values(values: &Map<String, Value>) -> Value {
|
|
@@ -322,3 +322,69 @@ fn profile_show_routes_and_preserves_redacted_secret_contract() {
|
|
|
322
322
|
);
|
|
323
323
|
let _ = std::fs::remove_dir_all(&ws);
|
|
324
324
|
}
|
|
325
|
+
|
|
326
|
+
#[test]
|
|
327
|
+
fn config_authority_t04_show_doctor_and_runtime_parse_the_same_profile_bytes() {
|
|
328
|
+
let ws = tmp_workspace();
|
|
329
|
+
let runtime_dir = ws.join(".team/runtime");
|
|
330
|
+
std::fs::remove_dir(&runtime_dir).unwrap();
|
|
331
|
+
assert!(
|
|
332
|
+
!runtime_dir.exists(),
|
|
333
|
+
"T04 fixture must start without a runtime directory"
|
|
334
|
+
);
|
|
335
|
+
let dir = profiles_dir(&ws);
|
|
336
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
337
|
+
let body = "# synthetic fixture\nexport AUTH_MODE = 'compatible_api'\nPROXY_MODE=inherit\nexport PROXY_MODE = \"direct\"\nMODEL = 'synthetic-model'\nBASE_URL = \"http://127.0.0.1:9/v1\"\nexport API_KEY = 'synthetic-secret-placeholder'\nPLAIN=plain\nBAD-KEY=ignored\n1BAD=ignored\nMISSING_EQUALS\n";
|
|
338
|
+
std::fs::write(dir.join("syntax.env"), body).unwrap();
|
|
339
|
+
let runtime = crate::lifecycle::profile_launch::load_profile(&ws, "syntax", None).unwrap();
|
|
340
|
+
assert!(
|
|
341
|
+
!runtime_dir.exists(),
|
|
342
|
+
"load_profile must not create the runtime directory"
|
|
343
|
+
);
|
|
344
|
+
for (key, value) in [
|
|
345
|
+
("AUTH_MODE", "compatible_api"),
|
|
346
|
+
("PROXY_MODE", "direct"),
|
|
347
|
+
("MODEL", "synthetic-model"),
|
|
348
|
+
("PLAIN", "plain"),
|
|
349
|
+
] {
|
|
350
|
+
assert_eq!(runtime.values.get(key).map(String::as_str), Some(value));
|
|
351
|
+
}
|
|
352
|
+
assert!(!runtime.values.contains_key("BAD-KEY"));
|
|
353
|
+
assert!(!runtime.values.contains_key("1BAD"));
|
|
354
|
+
for command in ["show", "doctor"] {
|
|
355
|
+
let result = cmd_profile(&ProfileArgs {
|
|
356
|
+
command: command.into(),
|
|
357
|
+
name: "syntax".into(),
|
|
358
|
+
workspace: ws.clone(),
|
|
359
|
+
team: None,
|
|
360
|
+
auth_mode: None,
|
|
361
|
+
proxy_mode: None,
|
|
362
|
+
json: true,
|
|
363
|
+
})
|
|
364
|
+
.unwrap();
|
|
365
|
+
let CmdOutput::Json(output) = result.output else {
|
|
366
|
+
panic!("expected JSON")
|
|
367
|
+
};
|
|
368
|
+
assert!(
|
|
369
|
+
!runtime_dir.exists(),
|
|
370
|
+
"profile {command} must not create the runtime directory"
|
|
371
|
+
);
|
|
372
|
+
assert_eq!(output["auth_mode"], runtime.values["AUTH_MODE"]);
|
|
373
|
+
assert_eq!(output["proxy_mode"], runtime.values["PROXY_MODE"]);
|
|
374
|
+
assert_eq!(output["credential_present"], true);
|
|
375
|
+
assert_eq!(output["secret_values_printed"], false);
|
|
376
|
+
assert!(!output.to_string().contains("synthetic-secret-placeholder"));
|
|
377
|
+
assert!(!output.to_string().contains("BAD-KEY"));
|
|
378
|
+
if command == "show" {
|
|
379
|
+
assert_eq!(output["values"]["MODEL"]["value"], "synthetic-model");
|
|
380
|
+
assert_eq!(output["values"]["API_KEY"]["redacted"], true);
|
|
381
|
+
assert_eq!(output["missing_common"], serde_json::json!([]));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
assert_eq!(
|
|
385
|
+
std::fs::read_to_string(dir.join("syntax.env")).unwrap(),
|
|
386
|
+
body
|
|
387
|
+
);
|
|
388
|
+
assert!(!dir.join("syntax.example.env").exists());
|
|
389
|
+
assert!(!ws.join(".team/runtime").exists());
|
|
390
|
+
}
|
|
@@ -135,7 +135,7 @@ pub(crate) fn write_worker_mcp_config_for_provider(
|
|
|
135
135
|
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", parent.display())))?;
|
|
136
136
|
}
|
|
137
137
|
let raw = if matches!(provider, Some(Provider::Copilot)) {
|
|
138
|
-
|
|
138
|
+
crate::provider::adapters::copilot::copilot_translate_mcp_config(&config.raw)
|
|
139
139
|
} else {
|
|
140
140
|
config.raw.clone()
|
|
141
141
|
};
|
|
@@ -610,36 +610,6 @@ fn upsert_toml_table_prefixes(existing: &str, tables: &[&str], stanza: &str) ->
|
|
|
610
610
|
}
|
|
611
611
|
}
|
|
612
612
|
|
|
613
|
-
/// ---
|
|
614
|
-
/// purpose: 把 MCP 配置里每个 server 的 type 字段名换成 transport
|
|
615
|
-
/// returns: 同结构的新值,其余字段全部保留;非对象原样返回
|
|
616
|
-
/// ---
|
|
617
|
-
/// C-3-4 cr verdict v2 — McpConfig.raw 是 `{name: {type, command, args, env}}` 形;
|
|
618
|
-
/// copilot mcp add schema 取 `transport` 替 `type`(stdio|http|sse 同值)。仅
|
|
619
|
-
/// 字段名变换,其余字段全保留。
|
|
620
|
-
pub(super) fn copilot_translate_mcp_servers(raw: &serde_json::Value) -> serde_json::Value {
|
|
621
|
-
let Some(servers) = raw.as_object() else {
|
|
622
|
-
return raw.clone();
|
|
623
|
-
};
|
|
624
|
-
let mut translated = serde_json::Map::new();
|
|
625
|
-
for (name, server) in servers {
|
|
626
|
-
let Some(obj) = server.as_object() else {
|
|
627
|
-
translated.insert(name.clone(), server.clone());
|
|
628
|
-
continue;
|
|
629
|
-
};
|
|
630
|
-
let mut out = serde_json::Map::new();
|
|
631
|
-
for (key, value) in obj {
|
|
632
|
-
if key == "type" {
|
|
633
|
-
out.insert("transport".to_string(), value.clone());
|
|
634
|
-
} else {
|
|
635
|
-
out.insert(key.clone(), value.clone());
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
translated.insert(name.clone(), serde_json::Value::Object(out));
|
|
639
|
-
}
|
|
640
|
-
serde_json::Value::Object(translated)
|
|
641
|
-
}
|
|
642
|
-
|
|
643
613
|
/// ---
|
|
644
614
|
/// purpose: 把 argv 里 provider 原生的 MCP 配置参数改指到已落盘的文件
|
|
645
615
|
/// params:
|
|
@@ -333,7 +333,7 @@ fn push_unique_path(paths: &mut Vec<PathBuf>, path: PathBuf) {
|
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
fn parse_env_text(text: &str) -> BTreeMap<String, String> {
|
|
336
|
+
pub(crate) fn parse_env_text(text: &str) -> BTreeMap<String, String> {
|
|
337
337
|
let mut values = BTreeMap::new();
|
|
338
338
|
for raw in text.lines() {
|
|
339
339
|
let mut line = raw.trim();
|
|
@@ -343,10 +343,11 @@ pub(super) fn spawn_agent_window(
|
|
|
343
343
|
let mcp_config_path = if provider == crate::provider::Provider::Pi {
|
|
344
344
|
None
|
|
345
345
|
} else {
|
|
346
|
-
Some(crate::lifecycle::launch::
|
|
346
|
+
Some(crate::lifecycle::launch::write_worker_mcp_config_for_provider(
|
|
347
347
|
workspace,
|
|
348
348
|
agent_id.as_str(),
|
|
349
349
|
&mcp_config,
|
|
350
|
+
Some(provider),
|
|
350
351
|
)?)
|
|
351
352
|
};
|
|
352
353
|
let profile_launch =
|
|
@@ -993,6 +993,145 @@ fn copilot_mcp_config_uses_copilot_field_name() {
|
|
|
993
993
|
);
|
|
994
994
|
}
|
|
995
995
|
|
|
996
|
+
fn assert_rules_t02_final_mcp(argv: &[String], ws: &Path, agent_id: &str, team: &str) {
|
|
997
|
+
let argument = flag_value(argv, "--additional-mcp-config").expect("final MCP flag");
|
|
998
|
+
let path = argument
|
|
999
|
+
.strip_prefix('@')
|
|
1000
|
+
.expect("final plan references a file");
|
|
1001
|
+
let config: serde_json::Value =
|
|
1002
|
+
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
|
|
1003
|
+
let server = &config["mcpServers"]["team_orchestrator"];
|
|
1004
|
+
assert_eq!(server["transport"], "stdio", "{config}");
|
|
1005
|
+
assert!(server.get("type").is_none(), "{config}");
|
|
1006
|
+
assert!(Path::new(server["command"].as_str().unwrap()).is_absolute());
|
|
1007
|
+
assert_eq!(
|
|
1008
|
+
server["args"],
|
|
1009
|
+
serde_json::json!(["mcp-server", "--workspace", ws])
|
|
1010
|
+
);
|
|
1011
|
+
assert_eq!(
|
|
1012
|
+
server["env"]["TEAM_AGENT_WORKSPACE"],
|
|
1013
|
+
ws.to_string_lossy().as_ref()
|
|
1014
|
+
);
|
|
1015
|
+
assert_eq!(server["env"]["TEAM_AGENT_ID"], agent_id);
|
|
1016
|
+
assert_eq!(server["env"]["TEAM_AGENT_OWNER_TEAM_ID"], team);
|
|
1017
|
+
eprintln!("T02 final MCP: agent={agent_id} team={team} argv={argv:?} config={config}");
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
#[test]
|
|
1021
|
+
#[serial(env)]
|
|
1022
|
+
fn rules_t02_final_mcp_across_fresh_start_restart_and_add() {
|
|
1023
|
+
let hermetic = enter_hermetic("rules-t02-final-mcp");
|
|
1024
|
+
let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
1025
|
+
let ws = hermetic.workspace("mcp");
|
|
1026
|
+
let team_dir = write_copilot_team(&ws, "cp-resume", &["mcp_team"], false);
|
|
1027
|
+
seed_healthy_coordinator(&ws);
|
|
1028
|
+
let health = team_agent::coordinator::coordinator_health(
|
|
1029
|
+
&team_agent::coordinator::WorkspacePath::new(ws.clone()),
|
|
1030
|
+
);
|
|
1031
|
+
assert!(
|
|
1032
|
+
health.ok,
|
|
1033
|
+
"seeded coordinator must prevent daemon spawn: {health:?}"
|
|
1034
|
+
);
|
|
1035
|
+
let fresh = RecordingTransport::new();
|
|
1036
|
+
quick_start_with_transport_in_workspace(&ws, &team_dir, None, true, Some("cpresume"), &fresh)
|
|
1037
|
+
.expect("fresh entry");
|
|
1038
|
+
let argv = fresh.single_spawn().argv;
|
|
1039
|
+
assert_rules_t02_final_mcp(&argv, &ws, "worker_a", "cpresume");
|
|
1040
|
+
assert!(flag_value(&argv, "--session-id").is_some());
|
|
1041
|
+
assert!(flag_value(&argv, "--resume").is_none());
|
|
1042
|
+
|
|
1043
|
+
seed_running_resumable(&ws, "worker_a");
|
|
1044
|
+
seed_copilot_session_store(hermetic.home(), "99999999-aaaa-4bbb-8ccc-dddddddddddd");
|
|
1045
|
+
for single in [true, false] {
|
|
1046
|
+
// A previous file is an output, never the authority for the next spawn.
|
|
1047
|
+
std::fs::write(ws.join(".team/runtime/mcp/worker_a.json"),
|
|
1048
|
+
r#"{"mcpServers":{"team_orchestrator":{"type":"stdio","env":{"TEAM_AGENT_ID":"foreign"}}}}"#,
|
|
1049
|
+
).unwrap();
|
|
1050
|
+
let transport = RecordingTransport::new();
|
|
1051
|
+
if single {
|
|
1052
|
+
team_agent::lifecycle::start_agent_with_transport(
|
|
1053
|
+
&ws,
|
|
1054
|
+
&team_agent::model::ids::AgentId::new("worker_a"),
|
|
1055
|
+
true,
|
|
1056
|
+
false,
|
|
1057
|
+
false,
|
|
1058
|
+
Some("cpresume"),
|
|
1059
|
+
&transport,
|
|
1060
|
+
)
|
|
1061
|
+
.expect("single start entry");
|
|
1062
|
+
} else {
|
|
1063
|
+
team_agent::lifecycle::restart_with_transport(&ws, true, Some("cpresume"), &transport)
|
|
1064
|
+
.expect("team restart entry");
|
|
1065
|
+
}
|
|
1066
|
+
let argv = transport.single_spawn().argv;
|
|
1067
|
+
assert_rules_t02_final_mcp(&argv, &ws, "worker_a", "cpresume");
|
|
1068
|
+
assert_eq!(
|
|
1069
|
+
flag_value(&argv, "--resume").as_deref(),
|
|
1070
|
+
Some("99999999-aaaa-4bbb-8ccc-dddddddddddd")
|
|
1071
|
+
);
|
|
1072
|
+
assert!(flag_value(&argv, "--session-id").is_none());
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
let role_file = ws.join("worker_b.md");
|
|
1076
|
+
let role = std::fs::read_to_string(team_dir.join("agents/worker_a.md")).unwrap();
|
|
1077
|
+
std::fs::write(&role_file, role.replace("worker_a", "worker_b")).unwrap();
|
|
1078
|
+
let added = RecordingTransport::with_session_present();
|
|
1079
|
+
team_agent::lifecycle::add_agent_with_transport(
|
|
1080
|
+
&team_dir,
|
|
1081
|
+
&team_agent::model::ids::AgentId::new("worker_b"),
|
|
1082
|
+
&role_file,
|
|
1083
|
+
false,
|
|
1084
|
+
Some("cpresume"),
|
|
1085
|
+
&added,
|
|
1086
|
+
)
|
|
1087
|
+
.expect("add entry");
|
|
1088
|
+
assert_rules_t02_final_mcp(&added.single_spawn().argv, &ws, "worker_b", "cpresume");
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
#[test]
|
|
1092
|
+
fn rules_t02_inline_server_map_preserves_unknown_and_non_object_values() {
|
|
1093
|
+
use team_agent::provider::adapters::copilot::copilot_translate_mcp_config;
|
|
1094
|
+
let raw = serde_json::json!({
|
|
1095
|
+
"stdio": {"type": "stdio", "command": "synthetic", "unknown": {"type": "nested"}},
|
|
1096
|
+
"http": {"type": "http", "url": "https://example.invalid"},
|
|
1097
|
+
"opaque": 7,
|
|
1098
|
+
});
|
|
1099
|
+
let translated = copilot_translate_mcp_config(&raw);
|
|
1100
|
+
assert_eq!(
|
|
1101
|
+
translated,
|
|
1102
|
+
serde_json::json!({
|
|
1103
|
+
"stdio": {"transport": "stdio", "command": "synthetic", "unknown": {"type": "nested"}},
|
|
1104
|
+
"http": {"transport": "http", "url": "https://example.invalid"},
|
|
1105
|
+
"opaque": 7,
|
|
1106
|
+
})
|
|
1107
|
+
);
|
|
1108
|
+
assert!(translated.get("mcpServers").is_none());
|
|
1109
|
+
for raw in [serde_json::json!(null), serde_json::json!([1, "opaque"])] {
|
|
1110
|
+
assert_eq!(copilot_translate_mcp_config(&raw), raw);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
#[test]
|
|
1115
|
+
#[serial(env)]
|
|
1116
|
+
fn rules_t02_non_copilot_writer_keeps_canonical_schema() {
|
|
1117
|
+
let hermetic = enter_hermetic("rules-t02-canonical");
|
|
1118
|
+
let ws = hermetic.workspace("canonical");
|
|
1119
|
+
let config = team_agent::provider::get_adapter(Provider::ClaudeCode)
|
|
1120
|
+
.mcp_config(team_agent::provider::AuthMode::Subscription)
|
|
1121
|
+
.unwrap();
|
|
1122
|
+
let path = team_agent::lifecycle::launch::write_worker_mcp_config_for_provider(
|
|
1123
|
+
&ws,
|
|
1124
|
+
"synthetic",
|
|
1125
|
+
&config,
|
|
1126
|
+
Some(Provider::ClaudeCode),
|
|
1127
|
+
)
|
|
1128
|
+
.unwrap();
|
|
1129
|
+
let written: serde_json::Value =
|
|
1130
|
+
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
|
|
1131
|
+
assert_eq!(written["mcpServers"], config.raw);
|
|
1132
|
+
assert_eq!(written["mcpServers"]["team_orchestrator"]["type"], "stdio");
|
|
1133
|
+
}
|
|
1134
|
+
|
|
996
1135
|
/// C-7-3 / RC-24: a resume command line must not carry `--session-id` and `--resume`
|
|
997
1136
|
/// together (conflicting session semantics). Driven through the real restart path so
|
|
998
1137
|
/// the public resume builder is exercised end to end.
|
|
@@ -1436,11 +1575,7 @@ fn write_copilot_team(ws: &Path, name: &str, tools: &[&str], dangerous: bool) ->
|
|
|
1436
1575
|
let team = ws.join(name);
|
|
1437
1576
|
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
1438
1577
|
// 0.5.66 bypass 单源:bypass 由角色字段驱动,不再是 TEAM.md dangerous_auto_approve。
|
|
1439
|
-
let role_bypass = if dangerous {
|
|
1440
|
-
"true"
|
|
1441
|
-
} else {
|
|
1442
|
-
"false"
|
|
1443
|
-
};
|
|
1578
|
+
let role_bypass = if dangerous { "true" } else { "false" };
|
|
1444
1579
|
std::fs::write(
|
|
1445
1580
|
team.join("TEAM.md"),
|
|
1446
1581
|
format!(
|
|
@@ -1637,6 +1772,7 @@ struct RecordedSpawn {
|
|
|
1637
1772
|
struct RecordingTransport {
|
|
1638
1773
|
spawns: Mutex<Vec<RecordedSpawn>>,
|
|
1639
1774
|
session_present: Mutex<bool>,
|
|
1775
|
+
panes: Mutex<Vec<PaneInfo>>,
|
|
1640
1776
|
}
|
|
1641
1777
|
|
|
1642
1778
|
impl RecordingTransport {
|
|
@@ -1681,8 +1817,22 @@ impl Transport for RecordingTransport {
|
|
|
1681
1817
|
window: window.as_str().to_string(),
|
|
1682
1818
|
});
|
|
1683
1819
|
*self.session_present.lock().unwrap() = true;
|
|
1820
|
+
let pane_id = PaneId::new(format!("%{}", spawns.len()));
|
|
1821
|
+
self.panes.lock().unwrap().push(PaneInfo {
|
|
1822
|
+
pane_id: pane_id.clone(),
|
|
1823
|
+
session: session.clone(),
|
|
1824
|
+
window_index: None,
|
|
1825
|
+
window_name: Some(window.clone()),
|
|
1826
|
+
pane_index: None,
|
|
1827
|
+
tty: None,
|
|
1828
|
+
current_command: None,
|
|
1829
|
+
current_path: None,
|
|
1830
|
+
active: false,
|
|
1831
|
+
pane_pid: Some(30_000 + spawns.len() as u32),
|
|
1832
|
+
leader_env: BTreeMap::new(),
|
|
1833
|
+
});
|
|
1684
1834
|
Ok(SpawnResult {
|
|
1685
|
-
pane_id
|
|
1835
|
+
pane_id,
|
|
1686
1836
|
session: session.clone(),
|
|
1687
1837
|
window: window.clone(),
|
|
1688
1838
|
child_pid: Some(30_000 + spawns.len() as u32),
|
|
@@ -1747,7 +1897,7 @@ impl Transport for RecordingTransport {
|
|
|
1747
1897
|
}
|
|
1748
1898
|
|
|
1749
1899
|
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
1750
|
-
Ok(
|
|
1900
|
+
Ok(self.panes.lock().unwrap().clone())
|
|
1751
1901
|
}
|
|
1752
1902
|
|
|
1753
1903
|
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
@@ -1769,10 +1919,20 @@ impl Transport for RecordingTransport {
|
|
|
1769
1919
|
|
|
1770
1920
|
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
1771
1921
|
*self.session_present.lock().unwrap() = false;
|
|
1922
|
+
self.panes
|
|
1923
|
+
.lock()
|
|
1924
|
+
.unwrap()
|
|
1925
|
+
.retain(|pane| &pane.session != _session);
|
|
1772
1926
|
Ok(())
|
|
1773
1927
|
}
|
|
1774
1928
|
|
|
1775
|
-
fn kill_window(&self,
|
|
1929
|
+
fn kill_window(&self, target: &Target) -> Result<(), TransportError> {
|
|
1930
|
+
self.panes.lock().unwrap().retain(|pane| match target {
|
|
1931
|
+
Target::Pane(id) => &pane.pane_id != id,
|
|
1932
|
+
Target::SessionWindow { session, window } => {
|
|
1933
|
+
&pane.session != session || pane.window_name.as_ref() != Some(window)
|
|
1934
|
+
}
|
|
1935
|
+
});
|
|
1776
1936
|
Ok(())
|
|
1777
1937
|
}
|
|
1778
1938
|
|
|
@@ -1796,6 +1956,17 @@ fn copilot_empty_mcp_list_shim(tag: &str) -> TempWorkspace {
|
|
|
1796
1956
|
#[cfg(unix)]
|
|
1797
1957
|
{
|
|
1798
1958
|
use std::os::unix::fs::PermissionsExt;
|
|
1959
|
+
// coordinator health needs the seeded test PID's real non-zombie status.
|
|
1960
|
+
// Keep PATH isolated and refuse any broader process inspection.
|
|
1961
|
+
std::fs::write(
|
|
1962
|
+
bin.join("ps"),
|
|
1963
|
+
format!(
|
|
1964
|
+
"#!/bin/sh\nif [ \"$#\" = 4 ] && [ \"$1\" = -p ] && [ \"$2\" = {} ] && [ \"$3\" = -o ] && [ \"$4\" = stat= ]; then\n exec /bin/ps \"$@\"\nfi\nexit 1\n",
|
|
1965
|
+
std::process::id(),
|
|
1966
|
+
),
|
|
1967
|
+
)
|
|
1968
|
+
.unwrap();
|
|
1969
|
+
std::fs::set_permissions(bin.join("ps"), std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
1799
1970
|
std::fs::set_permissions(bin.join("copilot"), std::fs::Permissions::from_mode(0o755))
|
|
1800
1971
|
.unwrap();
|
|
1801
1972
|
}
|