@team-agent/installer 0.5.49 → 0.5.51
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 +6 -4
- package/crates/team-agent/src/cli/emit.rs +91 -39
- package/crates/team-agent/src/cli/mod.rs +33 -15
- package/crates/team-agent/src/cli/named_address.rs +82 -53
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +103 -1308
- package/crates/team-agent/src/cli/spec.rs +2 -2
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -1
- package/crates/team-agent/src/cli/tests/named_address.rs +9 -7
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +17 -33
- package/crates/team-agent/src/cli/types.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
- package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +2 -2
- package/crates/team-agent/src/mcp_server/tests/send.rs +22 -15
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -0
- package/crates/team-agent/src/mcp_server/tools.rs +26 -15
- package/crates/team-agent/src/mcp_server/wire.rs +2 -18
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +165 -39
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +11 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +133 -58
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +2 -1
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +59 -16
- package/package.json +4 -4
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use std::process::Command;
|
|
4
|
+
|
|
5
|
+
use crate::lifecycle::*;
|
|
6
|
+
use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
|
|
7
|
+
use crate::model::ids::AgentId;
|
|
8
|
+
use crate::model::permissions::{self, AgentPermissionInput};
|
|
9
|
+
use crate::model::yaml::{self, Value};
|
|
10
|
+
use crate::state::persist::load_runtime_state;
|
|
11
|
+
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
12
|
+
|
|
13
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
14
|
+
|
|
15
|
+
use super::*;
|
|
16
|
+
|
|
17
|
+
pub(super) fn agent_is_paused(agent: &Value) -> bool {
|
|
18
|
+
matches!(agent.get("paused"), Some(Value::Bool(true)))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub(super) fn spawn_timestamp() -> String {
|
|
22
|
+
match std::env::var("TEAM_AGENT_TEST_FIXED_SPAWNED_AT") {
|
|
23
|
+
Ok(value) => value,
|
|
24
|
+
Err(_) => chrono::Utc::now()
|
|
25
|
+
.format("%Y-%m-%dT%H:%M:%S%.6f+00:00")
|
|
26
|
+
.to_string(),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub(super) fn spawn_timestamp_for_agent(offset_micros: u32) -> String {
|
|
31
|
+
if offset_micros == 0 {
|
|
32
|
+
return spawn_timestamp();
|
|
33
|
+
}
|
|
34
|
+
match std::env::var("TEAM_AGENT_TEST_FIXED_SPAWNED_AT") {
|
|
35
|
+
Ok(value) => chrono::DateTime::parse_from_rfc3339(&value)
|
|
36
|
+
.map(|dt| {
|
|
37
|
+
(dt.with_timezone(&chrono::Utc)
|
|
38
|
+
+ chrono::Duration::microseconds(i64::from(offset_micros)))
|
|
39
|
+
.format("%Y-%m-%dT%H:%M:%S%.6f+00:00")
|
|
40
|
+
.to_string()
|
|
41
|
+
})
|
|
42
|
+
.unwrap_or(value),
|
|
43
|
+
Err(_) => spawn_timestamp(),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub(crate) fn fill_spawn_placeholders(argv: &mut [String], workspace: &Path, agent_id: &str) {
|
|
48
|
+
fill_spawn_placeholders_full(argv, workspace, agent_id, None);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// #229 B-layer worker env contract (`worker_spawn_inherits_parent_process_env_for_proxy_and_ca`):
|
|
52
|
+
/// every worker `transport.spawn_first/into` MUST receive an env map that is the **complete**
|
|
53
|
+
/// `team-agent` process environ (so the child sees the user's PATH ordering, HTTP_PROXY /
|
|
54
|
+
/// HTTPS_PROXY / ALL_PROXY / NO_PROXY, NODE_EXTRA_CA_CERTS / SSL_CERT_FILE / CURL_CA_BUNDLE /
|
|
55
|
+
/// REQUESTS_CA_BUNDLE / GIT_SSL_CAINFO, plus any wrapper-sourced vars), **then** overlay the
|
|
56
|
+
/// Team Agent identity three-tuple. This equals POSIX "child inherits parent environ" — the same
|
|
57
|
+
/// behavior the user gets when typing `codex` from their own shell. Zero hardcoded paths, zero
|
|
58
|
+
/// wrapper-name assumptions, generic across providers.
|
|
59
|
+
///
|
|
60
|
+
/// `TMUX` / `TMUX_PANE` are stripped because they bind the inherited shell to the **launching**
|
|
61
|
+
/// tmux pane; leaving them in would point worker-side tmux integrations at the wrong pane.
|
|
62
|
+
pub(crate) fn inherited_env_with_team_overrides(
|
|
63
|
+
workspace: &Path,
|
|
64
|
+
agent_id: &str,
|
|
65
|
+
team_id: Option<&str>,
|
|
66
|
+
) -> BTreeMap<String, String> {
|
|
67
|
+
// 0.3.28 Step 3: delegate to `layout::worker_env::worker_spawn_env` which
|
|
68
|
+
// implements Python's whitelist semantics (`providers.py:130-145`). The
|
|
69
|
+
// whitelist:
|
|
70
|
+
// * Keeps PATH-like vars, locale, provider creds (CLAUDE_*/OPENAI_*/
|
|
71
|
+
// COPILOT_*/GH_*/GEMINI_*/GOOGLE_*) + posix identifiers only.
|
|
72
|
+
// * Strips ALL `TEAM_AGENT_LEADER_*` and
|
|
73
|
+
// `TEAM_AGENT_MACHINE_FINGERPRINT` (leader identity contamination,
|
|
74
|
+
// E60 root).
|
|
75
|
+
// * Strips `TEAM_AGENT_TEAM_ID` (the leader's team_id — re-injected
|
|
76
|
+
// as `TEAM_AGENT_OWNER_TEAM_ID` for the worker).
|
|
77
|
+
// * Strips `COPILOT_DISABLE_TERMINAL_TITLE` (re-injected per-agent by
|
|
78
|
+
// `apply_copilot_instructions_overlay` based on the WORKER's provider).
|
|
79
|
+
// * Strips `TMUX` / `TMUX_PANE` (would attach worker to leader's pane).
|
|
80
|
+
crate::layout::worker_env::worker_spawn_env(std::env::vars(), workspace, agent_id, team_id)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pub(crate) fn apply_mcp_auto_approval_env(
|
|
84
|
+
env: &mut BTreeMap<String, String>,
|
|
85
|
+
safety: &DangerousApproval,
|
|
86
|
+
) {
|
|
87
|
+
for key in [
|
|
88
|
+
"TEAM_AGENT_LEADER_BYPASS",
|
|
89
|
+
"TEAM_AGENT_LEADER_BYPASS_SOURCE",
|
|
90
|
+
"TEAM_AGENT_LEADER_BYPASS_PROVIDER",
|
|
91
|
+
"TEAM_AGENT_LEADER_BYPASS_FLAG",
|
|
92
|
+
"TEAM_AGENT_MCP_AUTO_APPROVE",
|
|
93
|
+
"TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE",
|
|
94
|
+
] {
|
|
95
|
+
env.remove(key);
|
|
96
|
+
}
|
|
97
|
+
if safety.enabled
|
|
98
|
+
&& matches!(safety.source, DangerousApprovalSource::LeaderProcess)
|
|
99
|
+
&& safety.inherited
|
|
100
|
+
{
|
|
101
|
+
env.insert("TEAM_AGENT_LEADER_BYPASS".to_string(), "1".to_string());
|
|
102
|
+
env.insert(
|
|
103
|
+
"TEAM_AGENT_LEADER_BYPASS_SOURCE".to_string(),
|
|
104
|
+
"leader_process".to_string(),
|
|
105
|
+
);
|
|
106
|
+
if let Some(provider) = safety.provider.as_deref() {
|
|
107
|
+
env.insert(
|
|
108
|
+
"TEAM_AGENT_LEADER_BYPASS_PROVIDER".to_string(),
|
|
109
|
+
provider.to_string(),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if let Some(flag) = safety.flag.as_deref() {
|
|
113
|
+
env.insert(
|
|
114
|
+
"TEAM_AGENT_LEADER_BYPASS_FLAG".to_string(),
|
|
115
|
+
flag.to_string(),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
env.insert(
|
|
119
|
+
"TEAM_AGENT_MCP_AUTO_APPROVE".to_string(),
|
|
120
|
+
"team_orchestrator".to_string(),
|
|
121
|
+
);
|
|
122
|
+
env.insert(
|
|
123
|
+
"TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE".to_string(),
|
|
124
|
+
"leader_bypass".to_string(),
|
|
125
|
+
);
|
|
126
|
+
} else {
|
|
127
|
+
env.insert("TEAM_AGENT_LEADER_BYPASS".to_string(), "0".to_string());
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// BUG / B2 灵魂件 + C-1-2 + C-6-1 cr verdict — Copilot per-worker AGENTS.md
|
|
132
|
+
/// 写入 + `COPILOT_CUSTOM_INSTRUCTIONS_DIRS` 注入。
|
|
133
|
+
///
|
|
134
|
+
/// 目录布局:`<workspace>/.team/runtime/copilot-instructions/<agent_id>/AGENTS.md`
|
|
135
|
+
/// * 含 `<agent_id>` segment(C-6-2 per-agent isolation,N18 精神)
|
|
136
|
+
/// * 文件内容 ≡ `compile_worker_system_prompt` 输出(B2 ps/文件双验法)
|
|
137
|
+
/// * **禁** silent 写全局 `~/.copilot/AGENTS.md`(C-1-2 grep guard)
|
|
138
|
+
///
|
|
139
|
+
/// 失败回 `LifecycleError::StatePersist` 以与既有 state 持久化错误同源,
|
|
140
|
+
/// 不 silent 吞(MUST-NOT-13 诚实)。
|
|
141
|
+
pub(crate) fn apply_copilot_instructions_overlay(
|
|
142
|
+
workspace: &Path,
|
|
143
|
+
agent_id: &str,
|
|
144
|
+
system_prompt: &str,
|
|
145
|
+
env: &mut BTreeMap<String, String>,
|
|
146
|
+
) -> Result<(), LifecycleError> {
|
|
147
|
+
let dir = workspace
|
|
148
|
+
.join(".team")
|
|
149
|
+
.join("runtime")
|
|
150
|
+
.join("copilot-instructions")
|
|
151
|
+
.join(agent_id);
|
|
152
|
+
std::fs::create_dir_all(&dir)
|
|
153
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", dir.display())))?;
|
|
154
|
+
let agents_md = dir.join("AGENTS.md");
|
|
155
|
+
std::fs::write(&agents_md, system_prompt.as_bytes())
|
|
156
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", agents_md.display())))?;
|
|
157
|
+
env.insert(
|
|
158
|
+
"COPILOT_CUSTOM_INSTRUCTIONS_DIRS".to_string(),
|
|
159
|
+
dir.to_string_lossy().to_string(),
|
|
160
|
+
);
|
|
161
|
+
// ★ C-4 P0(N39 红线 / MUST-12) — copilot config 默认 `updateTerminalTitle=true`
|
|
162
|
+
// 会改 tmux window 名(help-config 原文)。tmux window 名是框架定位 agent 的
|
|
163
|
+
// anchor(window==agent_id);copilot 静默改写 → 寻址 / kill / 保护集 三处同源
|
|
164
|
+
// 派生漂移 → B5 protected_set 误判、MUST-12 pane 身份失锚、N39 同源派生破。
|
|
165
|
+
// 漏关后果定级为【B5 leader 误杀同级 incident】,绝不允许 silent 跳过。
|
|
166
|
+
// 主案:env `COPILOT_DISABLE_TERMINAL_TITLE=1`(help-config 原文 "Can also be
|
|
167
|
+
// disabled via the COPILOT_DISABLE_TERMINAL_TITLE environment variable")。
|
|
168
|
+
env.insert(
|
|
169
|
+
"COPILOT_DISABLE_TERMINAL_TITLE".to_string(),
|
|
170
|
+
"1".to_string(),
|
|
171
|
+
);
|
|
172
|
+
Ok(())
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// C-3-2/C-3-3 cr verdict v2 — Copilot spawn 前调 `copilot mcp list` 扫用户全局
|
|
176
|
+
/// `~/.copilot/mcp-config.json` 与 workspace `.mcp.json` 的 MCP 残留;对每个非
|
|
177
|
+
/// `team_orchestrator` server 追加 `--disable-mcp-server <name>`(main-help:72-73)
|
|
178
|
+
/// 并落 `<log_dir>/mcp-residual.txt` + emit `provider.copilot.mcp_residual_detected`
|
|
179
|
+
/// event(MUST-NOT-13 诚实记录,非 silent)。
|
|
180
|
+
///
|
|
181
|
+
/// 失败回 `LifecycleError::StatePersist`,不 silent 吞;`copilot mcp list` 自身
|
|
182
|
+
/// 无法运行(命令缺失 / 退出码非零)时,仅记 `mcp-residual.txt` 的 unavailable
|
|
183
|
+
/// 行,不阻断 spawn(provider 一期 subscription-only,工具链可能未完全就绪)。
|
|
184
|
+
pub(super) fn apply_copilot_mcp_residual_disables(
|
|
185
|
+
workspace: &Path,
|
|
186
|
+
agent_id: &str,
|
|
187
|
+
argv: &mut Vec<String>,
|
|
188
|
+
log_dir: &Path,
|
|
189
|
+
) -> Result<(), LifecycleError> {
|
|
190
|
+
let listing = std::process::Command::new("copilot")
|
|
191
|
+
.arg("mcp")
|
|
192
|
+
.arg("list")
|
|
193
|
+
.output();
|
|
194
|
+
let residual_path = log_dir.join("mcp-residual.txt");
|
|
195
|
+
match listing {
|
|
196
|
+
Ok(out) if out.status.success() => {
|
|
197
|
+
let text = String::from_utf8_lossy(&out.stdout).to_string();
|
|
198
|
+
std::fs::write(&residual_path, &text).map_err(|e| {
|
|
199
|
+
LifecycleError::StatePersist(format!("{}: {e}", residual_path.display()))
|
|
200
|
+
})?;
|
|
201
|
+
let residual_servers = parse_copilot_mcp_list_server_names(&text);
|
|
202
|
+
let non_orchestrator: Vec<String> = residual_servers
|
|
203
|
+
.iter()
|
|
204
|
+
.filter(|name| name.as_str() != "team_orchestrator")
|
|
205
|
+
.cloned()
|
|
206
|
+
.collect();
|
|
207
|
+
for name in &non_orchestrator {
|
|
208
|
+
argv.push("--disable-mcp-server".to_string());
|
|
209
|
+
argv.push(name.clone());
|
|
210
|
+
}
|
|
211
|
+
if !non_orchestrator.is_empty() {
|
|
212
|
+
let event_log = crate::event_log::EventLog::new(workspace);
|
|
213
|
+
let _ = event_log.write(
|
|
214
|
+
"provider.copilot.mcp_residual_detected",
|
|
215
|
+
serde_json::json!({
|
|
216
|
+
"agent_id": agent_id,
|
|
217
|
+
"residual_servers": non_orchestrator,
|
|
218
|
+
"log_path": residual_path.to_string_lossy(),
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
Ok(out) => {
|
|
224
|
+
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
|
225
|
+
std::fs::write(
|
|
226
|
+
&residual_path,
|
|
227
|
+
format!(
|
|
228
|
+
"copilot mcp list exit={:?} stderr={stderr}\n",
|
|
229
|
+
out.status.code()
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
.map_err(|e| {
|
|
233
|
+
LifecycleError::StatePersist(format!("{}: {e}", residual_path.display()))
|
|
234
|
+
})?;
|
|
235
|
+
}
|
|
236
|
+
Err(e) => {
|
|
237
|
+
std::fs::write(
|
|
238
|
+
&residual_path,
|
|
239
|
+
format!("copilot mcp list unavailable: {e}\n"),
|
|
240
|
+
)
|
|
241
|
+
.map_err(|e| {
|
|
242
|
+
LifecycleError::StatePersist(format!("{}: {e}", residual_path.display()))
|
|
243
|
+
})?;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
Ok(())
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// 解析 `copilot mcp list` 输出取 server 名集合(te 真机实证 v2,1.0.59 形态):
|
|
250
|
+
/// ```text
|
|
251
|
+
/// User servers:
|
|
252
|
+
/// foo (local)
|
|
253
|
+
/// bar (http)
|
|
254
|
+
/// Builtin servers:
|
|
255
|
+
/// github-mcp-server (local)
|
|
256
|
+
/// ```
|
|
257
|
+
/// 或空集形态(te 真机实证 fake HOME 无 mcp-config.json):
|
|
258
|
+
/// ```text
|
|
259
|
+
/// No MCP servers configured.
|
|
260
|
+
///
|
|
261
|
+
/// Add a server with:
|
|
262
|
+
/// copilot mcp add <name> -- <command> [args...]
|
|
263
|
+
/// copilot mcp add --transport http <name> <url>
|
|
264
|
+
/// ```
|
|
265
|
+
///
|
|
266
|
+
/// 规则:
|
|
267
|
+
/// 1. 首行含 "No MCP servers configured" → 立即返空(避免把 "Add a server with"
|
|
268
|
+
/// 段下的 help 行误识为 server)
|
|
269
|
+
/// 2. 段标题行(非缩进、以 `:` 结尾):只有 *servers:* 后缀的段(User/Builtin/
|
|
270
|
+
/// Workspace servers:)才进 server-listing 模式;其余段(如 "Add a server with:")
|
|
271
|
+
/// 进 skip 模式直到下个 servers: 段或文档结束
|
|
272
|
+
/// 3. servers: 段内的缩进行取首段 token,剥 ` (local)`/` (http)`/` (sse)` 后缀
|
|
273
|
+
/// 4. 空行 / 不识别行容忍跳过(诚实降级:漏识 = silent 残留,在 mcp-residual.txt
|
|
274
|
+
/// 全量落盘留证)
|
|
275
|
+
pub(super) fn parse_copilot_mcp_list_server_names(text: &str) -> Vec<String> {
|
|
276
|
+
let mut out: Vec<String> = Vec::new();
|
|
277
|
+
let mut in_servers_section = false;
|
|
278
|
+
for line in text.lines() {
|
|
279
|
+
let trimmed_end = line.trim_end();
|
|
280
|
+
if trimmed_end.is_empty() {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
// C-3-2 fix(te 真机实证):空集 sentinel 立即返空。
|
|
284
|
+
if trimmed_end
|
|
285
|
+
.trim_start()
|
|
286
|
+
.starts_with("No MCP servers configured")
|
|
287
|
+
{
|
|
288
|
+
return Vec::new();
|
|
289
|
+
}
|
|
290
|
+
// 段标题行(非缩进):决定后续缩进行是否取 server 名。"*servers:" 是
|
|
291
|
+
// listing 段(User/Builtin/Workspace),其它段都 skip(如 "Add a server with:"
|
|
292
|
+
// 下面的 help 命令缩进行)。
|
|
293
|
+
if !(line.starts_with(' ') || line.starts_with('\t')) {
|
|
294
|
+
let lower = trimmed_end.to_ascii_lowercase();
|
|
295
|
+
in_servers_section = lower.trim_end_matches(':').ends_with("servers");
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if !in_servers_section {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
let trimmed = trimmed_end.trim_start();
|
|
302
|
+
if trimmed.is_empty() {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
let mut token = trimmed.split_whitespace().next().unwrap_or("").to_string();
|
|
306
|
+
// 剥常见装饰后缀(实测形如 "(local)"/"(http)"/"(sse)" 是独立 whitespace
|
|
307
|
+
// 分隔的 token,首段 token 通常不带括号;若实际 copilot 把括号粘连首段
|
|
308
|
+
// token,这里多做一次后缀剥离守护)。
|
|
309
|
+
if let Some(idx) = token.find('(') {
|
|
310
|
+
token.truncate(idx);
|
|
311
|
+
}
|
|
312
|
+
token = token.trim_end_matches(':').trim().to_string();
|
|
313
|
+
if token.is_empty() {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
out.push(token);
|
|
317
|
+
}
|
|
318
|
+
out
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
pub(crate) fn apply_profile_launch_env(
|
|
322
|
+
env: &mut BTreeMap<String, String>,
|
|
323
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
324
|
+
) {
|
|
325
|
+
for key in &profile_launch.env_unset {
|
|
326
|
+
env.remove(key);
|
|
327
|
+
}
|
|
328
|
+
env.extend(profile_launch.env_overlay.clone());
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
pub(super) fn persist_started_agent_plan_state(
|
|
332
|
+
state: &mut serde_json::Map<String, serde_json::Value>,
|
|
333
|
+
started_agent: &StartedAgent,
|
|
334
|
+
) {
|
|
335
|
+
if let Some(session_id) = started_agent.pending_session_id.as_ref() {
|
|
336
|
+
state.insert(
|
|
337
|
+
"_pending_session_id".to_string(),
|
|
338
|
+
serde_json::json!(session_id.as_str()),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if let Some(root) = started_agent.provider_projects_root.as_ref() {
|
|
342
|
+
state.insert(
|
|
343
|
+
"claude_projects_root".to_string(),
|
|
344
|
+
serde_json::json!(root.to_string_lossy().to_string()),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
if started_agent.managed_mcp_config {
|
|
348
|
+
state.insert("managed_mcp_config".to_string(), serde_json::json!(true));
|
|
349
|
+
}
|
|
350
|
+
if started_agent.managed_mcp_config
|
|
351
|
+
|| started_agent.claude_config_dir.is_some()
|
|
352
|
+
|| started_agent.provider_projects_root.is_some()
|
|
353
|
+
{
|
|
354
|
+
state.insert(
|
|
355
|
+
"profile_launch".to_string(),
|
|
356
|
+
serde_json::json!({
|
|
357
|
+
"managed_mcp_config": started_agent.managed_mcp_config,
|
|
358
|
+
"claude_config_dir": started_agent.claude_config_dir.as_ref().map(|path| path.to_string_lossy().to_string()),
|
|
359
|
+
"claude_projects_root": started_agent.provider_projects_root.as_ref().map(|path| path.to_string_lossy().to_string()),
|
|
360
|
+
}),
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
pub(crate) fn persist_command_plan_state(
|
|
366
|
+
state: &mut serde_json::Map<String, serde_json::Value>,
|
|
367
|
+
plan: &crate::provider::CommandPlan,
|
|
368
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
369
|
+
) {
|
|
370
|
+
if let Some(session_id) = plan.expected_session_id.as_ref() {
|
|
371
|
+
state.insert(
|
|
372
|
+
"_pending_session_id".to_string(),
|
|
373
|
+
serde_json::json!(session_id.as_str()),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
let projects_root = plan
|
|
377
|
+
.provider_projects_root
|
|
378
|
+
.as_ref()
|
|
379
|
+
.or(profile_launch.claude_projects_root.as_ref());
|
|
380
|
+
if let Some(root) = projects_root {
|
|
381
|
+
state.insert(
|
|
382
|
+
"claude_projects_root".to_string(),
|
|
383
|
+
serde_json::json!(root.to_string_lossy().to_string()),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
let managed_mcp_config = plan.managed_mcp_config || profile_launch.managed_mcp_config;
|
|
387
|
+
if managed_mcp_config {
|
|
388
|
+
state.insert("managed_mcp_config".to_string(), serde_json::json!(true));
|
|
389
|
+
}
|
|
390
|
+
if managed_mcp_config || profile_launch.claude_config_dir.is_some() || projects_root.is_some() {
|
|
391
|
+
state.insert(
|
|
392
|
+
"profile_launch".to_string(),
|
|
393
|
+
serde_json::json!({
|
|
394
|
+
"managed_mcp_config": managed_mcp_config,
|
|
395
|
+
"claude_config_dir": profile_launch.claude_config_dir.as_ref().map(|path| path.to_string_lossy().to_string()),
|
|
396
|
+
"claude_projects_root": projects_root.map(|path| path.to_string_lossy().to_string()),
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
pub(super) fn is_posix_shell_identifier(name: &str) -> bool {
|
|
403
|
+
let mut chars = name.chars();
|
|
404
|
+
match chars.next() {
|
|
405
|
+
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
|
|
406
|
+
_ => return false,
|
|
407
|
+
}
|
|
408
|
+
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/// Same as [`fill_spawn_placeholders`] plus `{team_id}` substitution everywhere it
|
|
412
|
+
/// appears as a SUBSTRING (the MCP config encodes it as `mcp_servers.team_orchestrator
|
|
413
|
+
/// .env.TEAM_AGENT_OWNER_TEAM_ID="{team_id}"`, embedded inside `-c key=value` strings,
|
|
414
|
+
/// so a token-equality replace would miss it).
|
|
415
|
+
pub(crate) fn fill_spawn_placeholders_full(
|
|
416
|
+
argv: &mut [String],
|
|
417
|
+
workspace: &Path,
|
|
418
|
+
agent_id: &str,
|
|
419
|
+
team_id: Option<&str>,
|
|
420
|
+
) {
|
|
421
|
+
let workspace_text = workspace.to_string_lossy().to_string();
|
|
422
|
+
let team_text = team_id.unwrap_or("").to_string();
|
|
423
|
+
for arg in argv {
|
|
424
|
+
if arg == "{workspace}" {
|
|
425
|
+
*arg = workspace_text.clone();
|
|
426
|
+
} else if arg == "{agent_id}" {
|
|
427
|
+
*arg = agent_id.to_string();
|
|
428
|
+
} else if arg.contains("{workspace}")
|
|
429
|
+
|| arg.contains("{agent_id}")
|
|
430
|
+
|| arg.contains("{team_id}")
|
|
431
|
+
{
|
|
432
|
+
*arg = arg
|
|
433
|
+
.replace("{workspace}", &workspace_text)
|
|
434
|
+
.replace("{agent_id}", agent_id)
|
|
435
|
+
.replace("{team_id}", &team_text);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|