@team-agent/installer 0.5.106 → 0.5.107
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/tests/base.rs +112 -225
- package/crates/team-agent/src/compiler/tests.rs +46 -59
- package/crates/team-agent/src/leader/tests/byte_findings.rs +1 -14
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +57 -173
- package/crates/team-agent/src/lifecycle/launch/pi_mcp_extension.js +226 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +0 -37
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +0 -9
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_native_mcp_host.mjs +71 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -66
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +0 -392
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +0 -5
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -361
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -16
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +20 -112
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1 -12
- package/crates/team-agent/src/lifecycle/tests/pi_native_mcp_capability_red.rs +546 -0
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +11 -54
- package/crates/team-agent/src/messaging/tests/wave2.rs +2 -302
- package/crates/team-agent/src/model/name_similarity.rs +52 -67
- package/crates/team-agent/src/model/task_graph.rs +37 -124
- package/crates/team-agent/src/model/yaml/tests.rs +46 -165
- package/crates/team-agent/src/provider/tests/adapter.rs +0 -20
- package/crates/team-agent/src/provider/tests/classify.rs +106 -198
- package/crates/team-agent/src/tmux_backend/tests.rs +178 -501
- package/crates/team-agent/src/transport/tests/wire.rs +0 -55
- package/package.json +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//! ---
|
|
2
|
-
//! purpose: Pi executable/catalog
|
|
2
|
+
//! purpose: Pi executable/catalog 验证、原生/公共 MCP 能力注册与共享启动计划物化
|
|
3
3
|
//! contract:
|
|
4
4
|
//! provides:
|
|
5
5
|
//! - name: materialize_pi_plan
|
|
@@ -29,8 +29,6 @@ use crate::model::enums::{Provider, ProviderEffort};
|
|
|
29
29
|
use crate::provider::adapters::pi::{build_pi_command_argv, PiCommandRequest, PiSessionSelector};
|
|
30
30
|
use crate::provider::{CommandPlan, McpConfig, ProviderError, SessionId};
|
|
31
31
|
|
|
32
|
-
const ADAPTER_NAME: &str = "pi-mcp-adapter";
|
|
33
|
-
|
|
34
32
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
35
33
|
pub(crate) enum PiExecutableFileType {
|
|
36
34
|
Wrapper,
|
|
@@ -38,17 +36,6 @@ pub(crate) enum PiExecutableFileType {
|
|
|
38
36
|
Binary,
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
42
|
-
pub(crate) struct PiAdapterIdentity {
|
|
43
|
-
pub package_name: String,
|
|
44
|
-
pub version: String,
|
|
45
|
-
pub extension_entry: String,
|
|
46
|
-
pub package_json: PathBuf,
|
|
47
|
-
pub index_ts: PathBuf,
|
|
48
|
-
pub package_json_sha256: String,
|
|
49
|
-
pub index_ts_sha256: String,
|
|
50
|
-
}
|
|
51
|
-
|
|
52
39
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
53
40
|
pub(crate) struct PiExecutableChain {
|
|
54
41
|
pub path_entry: PathBuf,
|
|
@@ -57,7 +44,6 @@ pub(crate) struct PiExecutableChain {
|
|
|
57
44
|
pub real_binary: PathBuf,
|
|
58
45
|
pub pi_version: String,
|
|
59
46
|
pub catalog_sha256: String,
|
|
60
|
-
pub adapter: PiAdapterIdentity,
|
|
61
47
|
}
|
|
62
48
|
|
|
63
49
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
@@ -183,54 +169,9 @@ pub(crate) fn verify_started_pi_model(selected: &str, observed: &str) -> Result<
|
|
|
183
169
|
}
|
|
184
170
|
|
|
185
171
|
/// ---
|
|
186
|
-
/// purpose: 校验
|
|
187
|
-
/// returns:
|
|
188
|
-
/// errors:
|
|
189
|
-
/// ---
|
|
190
|
-
pub(crate) fn validate_pi_adapter_identity(
|
|
191
|
-
identity: &PiAdapterIdentity,
|
|
192
|
-
) -> Result<(), ProviderError> {
|
|
193
|
-
if identity.package_name != ADAPTER_NAME
|
|
194
|
-
|| identity.extension_entry.is_empty()
|
|
195
|
-
|| !identity.package_json.is_absolute()
|
|
196
|
-
|| !identity.index_ts.is_absolute()
|
|
197
|
-
{
|
|
198
|
-
return Err(ProviderError::Command(
|
|
199
|
-
"Pi MCP adapter package or extension entry is not resolvable".to_string(),
|
|
200
|
-
));
|
|
201
|
-
}
|
|
202
|
-
let package_root = identity.package_json.parent().ok_or_else(|| {
|
|
203
|
-
ProviderError::Command("Pi MCP adapter package root is not resolvable".to_string())
|
|
204
|
-
})?;
|
|
205
|
-
let extension_entry = Path::new(&identity.extension_entry);
|
|
206
|
-
if extension_entry.is_absolute() {
|
|
207
|
-
return Err(ProviderError::Command(
|
|
208
|
-
"Pi MCP adapter extension entry must be package-relative".to_string(),
|
|
209
|
-
));
|
|
210
|
-
}
|
|
211
|
-
let declared_entry =
|
|
212
|
-
std::fs::canonicalize(package_root.join(extension_entry)).map_err(|error| {
|
|
213
|
-
ProviderError::Io(format!(
|
|
214
|
-
"Pi MCP adapter extension entry {}: {error}",
|
|
215
|
-
identity.index_ts.display()
|
|
216
|
-
))
|
|
217
|
-
})?;
|
|
218
|
-
let loaded_entry = std::fs::canonicalize(&identity.index_ts)
|
|
219
|
-
.map_err(|error| ProviderError::Io(format!("{}: {error}", identity.index_ts.display())))?;
|
|
220
|
-
if declared_entry != loaded_entry || !identity.package_json.is_file() {
|
|
221
|
-
return Err(ProviderError::Command(
|
|
222
|
-
"Pi MCP adapter package or extension entry is not loadable".to_string(),
|
|
223
|
-
));
|
|
224
|
-
}
|
|
225
|
-
std::fs::read(&loaded_entry)
|
|
226
|
-
.map_err(|error| ProviderError::Io(format!("{}: {error}", loaded_entry.display())))?;
|
|
227
|
-
Ok(())
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/// ---
|
|
231
|
-
/// purpose: 校验 Pi launch/real path、catalog 与 adapter 公共能力
|
|
232
|
-
/// returns: executable chain 与 capability receipts 完整时成功
|
|
233
|
-
/// errors: 缺路径、catalog receipt 或 adapter capability 时返回 ProviderError
|
|
172
|
+
/// purpose: 校验 Pi launch/real path 与 live catalog;MCP 能力在实际 Pi context 验证
|
|
173
|
+
/// returns: executable chain 与 catalog receipt 完整时成功
|
|
174
|
+
/// errors: 缺路径或 catalog receipt 时返回 ProviderError
|
|
234
175
|
/// ---
|
|
235
176
|
pub(crate) fn validate_pi_executable_chain(chain: &PiExecutableChain) -> Result<(), ProviderError> {
|
|
236
177
|
if !chain.path_entry.is_absolute()
|
|
@@ -253,12 +194,11 @@ pub(crate) fn validate_pi_executable_chain(chain: &PiExecutableChain) -> Result<
|
|
|
253
194
|
"Pi executable chain does not provide the required public protocol".to_string(),
|
|
254
195
|
));
|
|
255
196
|
}
|
|
256
|
-
|
|
197
|
+
Ok(())
|
|
257
198
|
}
|
|
258
199
|
|
|
259
200
|
pub(crate) struct PiWrapperRequest<'a> {
|
|
260
201
|
pub destination: &'a Path,
|
|
261
|
-
pub adapter: &'a PiAdapterIdentity,
|
|
262
202
|
pub candidate_executable: &'a Path,
|
|
263
203
|
pub mcp_config: &'a McpConfig,
|
|
264
204
|
pub team_id: &'a str,
|
|
@@ -301,7 +241,17 @@ fn candidate_from_mcp_config(config: &McpConfig) -> Result<PathBuf, ProviderErro
|
|
|
301
241
|
}
|
|
302
242
|
|
|
303
243
|
fn render_pi_wrapper(request: &PiWrapperRequest<'_>) -> Result<String, ProviderError> {
|
|
304
|
-
|
|
244
|
+
let allowed = request.include_tools.iter().copied().collect::<BTreeSet<_>>();
|
|
245
|
+
if allowed.is_empty()
|
|
246
|
+
|| allowed.len() != request.include_tools.len()
|
|
247
|
+
|| allowed
|
|
248
|
+
.iter()
|
|
249
|
+
.any(|tool| !["send_message", "report_result", "get_team_status"].contains(tool))
|
|
250
|
+
{
|
|
251
|
+
return Err(ProviderError::Command(
|
|
252
|
+
"Pi MCP tools must be a non-empty subset of the three Team tools".to_string(),
|
|
253
|
+
));
|
|
254
|
+
}
|
|
305
255
|
let configured_candidate = candidate_from_mcp_config(request.mcp_config)?;
|
|
306
256
|
let expected = std::fs::canonicalize(request.candidate_executable)
|
|
307
257
|
.unwrap_or_else(|_| request.candidate_executable.to_path_buf());
|
|
@@ -349,63 +299,55 @@ fn render_pi_wrapper(request: &PiWrapperRequest<'_>) -> Result<String, ProviderE
|
|
|
349
299
|
"TEAM_AGENT_OWNER_TEAM_ID".to_string(),
|
|
350
300
|
serde_json::Value::String(request.team_id.to_string()),
|
|
351
301
|
);
|
|
352
|
-
let definition = serde_json::
|
|
302
|
+
let definition = serde_json::to_string(&serde_json::Value::Object(server))
|
|
353
303
|
.map_err(|error| ProviderError::Command(format!("serialize Pi MCP server: {error}")))?;
|
|
304
|
+
let receipt_root = request
|
|
305
|
+
.destination
|
|
306
|
+
.parent()
|
|
307
|
+
.filter(|path| path.is_absolute())
|
|
308
|
+
.ok_or_else(|| {
|
|
309
|
+
ProviderError::Command("Pi MCP wrapper requires an absolute seat directory".to_string())
|
|
310
|
+
})?;
|
|
311
|
+
let receipt_root = serde_json::to_string(&receipt_root.to_string_lossy())
|
|
312
|
+
.map_err(|error| ProviderError::Command(format!("serialize Pi MCP seat directory: {error}")))?;
|
|
354
313
|
Ok(format!(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
export default function teamAgentMcp(pi: any) {{
|
|
358
|
-
let registration: {{ dispose(): Promise<void> }} | undefined;
|
|
359
|
-
|
|
360
|
-
pi.on("session_start", () => {{
|
|
361
|
-
if (registration) return;
|
|
362
|
-
const request: any = {{
|
|
363
|
-
version: 1,
|
|
364
|
-
name: "team_orchestrator",
|
|
365
|
-
definition: {definition},
|
|
366
|
-
}};
|
|
367
|
-
pi.events.emit(MCP_RUNTIME_REGISTER_EVENT, request);
|
|
368
|
-
if (!request.result) throw new Error("pi-mcp-adapter is not enabled");
|
|
369
|
-
if (!request.result.ok) throw request.result.error;
|
|
370
|
-
registration = request.result.registration;
|
|
371
|
-
}});
|
|
372
|
-
|
|
373
|
-
pi.on("session_shutdown", async () => {{
|
|
374
|
-
const current = registration;
|
|
375
|
-
registration = undefined;
|
|
376
|
-
await current?.dispose();
|
|
377
|
-
}});
|
|
378
|
-
}}
|
|
379
|
-
"#
|
|
314
|
+
"const teamMcpDefinition = {definition};\nconst teamMcpReceiptRoot = {receipt_root};\n\n{}",
|
|
315
|
+
include_str!("pi_mcp_extension.js")
|
|
380
316
|
))
|
|
381
317
|
}
|
|
382
318
|
|
|
383
319
|
/// ---
|
|
384
|
-
/// purpose: 静态核对 wrapper
|
|
385
|
-
/// returns:
|
|
386
|
-
/// errors:
|
|
320
|
+
/// purpose: 静态核对 wrapper 使用原生/公共 MCP 能力模板与指定 candidate
|
|
321
|
+
/// returns: 模板完整且 candidate、seat 目录正确时成功
|
|
322
|
+
/// errors: 模板、candidate 或 seat 目录不匹配时返回 ProviderError
|
|
387
323
|
/// ---
|
|
388
324
|
pub(crate) fn validate_pi_wrapper_source(
|
|
389
325
|
source: &str,
|
|
390
|
-
adapter: &PiAdapterIdentity,
|
|
391
326
|
candidate_executable: &Path,
|
|
392
327
|
) -> Result<(), ProviderError> {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
)
|
|
328
|
+
let invalid = || {
|
|
329
|
+
ProviderError::Command(
|
|
330
|
+
"Pi wrapper source is not the owned MCP capability template".to_string(),
|
|
331
|
+
)
|
|
332
|
+
};
|
|
333
|
+
let prefix = source
|
|
334
|
+
.strip_suffix(include_str!("pi_mcp_extension.js"))
|
|
335
|
+
.ok_or_else(invalid)?;
|
|
336
|
+
let (definition, root) = prefix
|
|
337
|
+
.strip_prefix("const teamMcpDefinition = ")
|
|
338
|
+
.and_then(|value| value.split_once(";\nconst teamMcpReceiptRoot = "))
|
|
339
|
+
.ok_or_else(invalid)?;
|
|
340
|
+
let definition: serde_json::Value = serde_json::from_str(definition).map_err(|_| invalid())?;
|
|
341
|
+
let command = definition
|
|
342
|
+
.get("command")
|
|
343
|
+
.and_then(serde_json::Value::as_str)
|
|
344
|
+
.ok_or_else(invalid)?;
|
|
345
|
+
let root: String = serde_json::from_str(root.strip_suffix(";\n\n").ok_or_else(invalid)?)
|
|
346
|
+
.map_err(|_| invalid())?;
|
|
347
|
+
let expected = std::fs::canonicalize(candidate_executable)
|
|
348
|
+
.unwrap_or_else(|_| candidate_executable.to_path_buf());
|
|
349
|
+
if Path::new(command) != expected || !Path::new(&root).is_absolute() {
|
|
350
|
+
return Err(invalid());
|
|
409
351
|
}
|
|
410
352
|
Ok(())
|
|
411
353
|
}
|
|
@@ -424,7 +366,7 @@ pub(crate) fn write_pi_wrapper_with_publish(
|
|
|
424
366
|
publish: impl FnOnce(&Path, &Path) -> std::io::Result<()>,
|
|
425
367
|
) -> Result<PathBuf, ProviderError> {
|
|
426
368
|
let source = render_pi_wrapper(&request)?;
|
|
427
|
-
validate_pi_wrapper_source(&source, request.
|
|
369
|
+
validate_pi_wrapper_source(&source, request.candidate_executable)?;
|
|
428
370
|
let parent = request.destination.parent().ok_or_else(|| {
|
|
429
371
|
ProviderError::Io("Pi wrapper destination has no parent directory".to_string())
|
|
430
372
|
})?;
|
|
@@ -726,63 +668,8 @@ fn command_stdout(executable: &Path, args: &[&str]) -> Result<Vec<u8>, ProviderE
|
|
|
726
668
|
Ok(output.stdout)
|
|
727
669
|
}
|
|
728
670
|
|
|
729
|
-
fn discover_pi_adapter(launch_executable: &Path) -> Result<PiAdapterIdentity, ProviderError> {
|
|
730
|
-
let listing = command_stdout(launch_executable, &["list"])?;
|
|
731
|
-
let listing = std::str::from_utf8(&listing).map_err(|error| {
|
|
732
|
-
ProviderError::Command(format!("Pi package list is not UTF-8: {error}"))
|
|
733
|
-
})?;
|
|
734
|
-
let mut lines = listing.lines();
|
|
735
|
-
let mut package_root = None;
|
|
736
|
-
while let Some(line) = lines.next() {
|
|
737
|
-
if line.trim() == "npm:pi-mcp-adapter" {
|
|
738
|
-
package_root = lines
|
|
739
|
-
.next()
|
|
740
|
-
.map(str::trim)
|
|
741
|
-
.filter(|path| Path::new(path).is_absolute())
|
|
742
|
-
.map(PathBuf::from);
|
|
743
|
-
break;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
let package_root = package_root.ok_or_else(|| {
|
|
747
|
-
ProviderError::Command("Pi package list does not resolve npm:pi-mcp-adapter".to_string())
|
|
748
|
-
})?;
|
|
749
|
-
let package_json = package_root.join("package.json");
|
|
750
|
-
let package_bytes = std::fs::read(&package_json)
|
|
751
|
-
.map_err(|error| ProviderError::Io(format!("{}: {error}", package_json.display())))?;
|
|
752
|
-
let package: serde_json::Value = serde_json::from_slice(&package_bytes)
|
|
753
|
-
.map_err(|error| ProviderError::Command(format!("{}: {error}", package_json.display())))?;
|
|
754
|
-
let extension_entry = package
|
|
755
|
-
.pointer("/pi/extensions")
|
|
756
|
-
.and_then(serde_json::Value::as_array)
|
|
757
|
-
.and_then(|entries| entries.iter().find_map(serde_json::Value::as_str))
|
|
758
|
-
.unwrap_or("")
|
|
759
|
-
.to_string();
|
|
760
|
-
let index_ts = package_root.join(extension_entry.trim_start_matches("./"));
|
|
761
|
-
let index_bytes = std::fs::read(&index_ts)
|
|
762
|
-
.map_err(|error| ProviderError::Io(format!("{}: {error}", index_ts.display())))?;
|
|
763
|
-
let identity = PiAdapterIdentity {
|
|
764
|
-
package_name: package
|
|
765
|
-
.get("name")
|
|
766
|
-
.and_then(serde_json::Value::as_str)
|
|
767
|
-
.unwrap_or("")
|
|
768
|
-
.to_string(),
|
|
769
|
-
version: package
|
|
770
|
-
.get("version")
|
|
771
|
-
.and_then(serde_json::Value::as_str)
|
|
772
|
-
.unwrap_or("")
|
|
773
|
-
.to_string(),
|
|
774
|
-
extension_entry,
|
|
775
|
-
package_json,
|
|
776
|
-
index_ts,
|
|
777
|
-
package_json_sha256: sha256(&package_bytes),
|
|
778
|
-
index_ts_sha256: sha256(&index_bytes),
|
|
779
|
-
};
|
|
780
|
-
validate_pi_adapter_identity(&identity)?;
|
|
781
|
-
Ok(identity)
|
|
782
|
-
}
|
|
783
|
-
|
|
784
671
|
/// ---
|
|
785
|
-
/// purpose: 从当前 PATH 环境测量 Pi launch/real executable
|
|
672
|
+
/// purpose: 从当前 PATH 环境测量 Pi launch/real executable 与 live catalog,不检查 Pi packages
|
|
786
673
|
/// returns: 已验证的 chain 与同次 live catalog exact ids
|
|
787
674
|
/// errors: 缺失、命令失败或任一公共 protocol capability 缺失时返回 ProviderError
|
|
788
675
|
/// ---
|
|
@@ -829,7 +716,6 @@ pub(crate) fn resolve_pi_executable_chain(
|
|
|
829
716
|
})?;
|
|
830
717
|
let catalog = command_stdout(&path_entry, &["--list-models"])?;
|
|
831
718
|
let models = parse_pi_list_models_table(&catalog)?;
|
|
832
|
-
let adapter = discover_pi_adapter(&path_entry)?;
|
|
833
719
|
let chain = PiExecutableChain {
|
|
834
720
|
path_entry: path_entry.clone(),
|
|
835
721
|
path_entry_type,
|
|
@@ -837,7 +723,6 @@ pub(crate) fn resolve_pi_executable_chain(
|
|
|
837
723
|
real_binary,
|
|
838
724
|
pi_version: version,
|
|
839
725
|
catalog_sha256: sha256(&catalog),
|
|
840
|
-
adapter,
|
|
841
726
|
};
|
|
842
727
|
validate_pi_executable_chain(&chain)?;
|
|
843
728
|
Ok((chain, models))
|
|
@@ -906,7 +791,7 @@ fn materialize_pi_plan_with_session(
|
|
|
906
791
|
request: PiMaterializeRequest<'_>,
|
|
907
792
|
resume: Option<(&SessionId, &Path, &Path)>,
|
|
908
793
|
) -> Result<CommandPlan, ProviderError> {
|
|
909
|
-
let (
|
|
794
|
+
let (_chain, catalog) = resolve_pi_executable_chain()?;
|
|
910
795
|
let model = request
|
|
911
796
|
.model
|
|
912
797
|
.map(|model| select_exact_pi_model(&catalog, model))
|
|
@@ -954,7 +839,6 @@ fn materialize_pi_plan_with_session(
|
|
|
954
839
|
let candidate = candidate_from_mcp_config(request.mcp_config)?;
|
|
955
840
|
let wrapper = write_pi_wrapper(PiWrapperRequest {
|
|
956
841
|
destination: &paths.wrapper,
|
|
957
|
-
adapter: &chain.adapter,
|
|
958
842
|
candidate_executable: &candidate,
|
|
959
843
|
mcp_config: request.mcp_config,
|
|
960
844
|
team_id: request.team_id,
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// The Rust materializer prepends teamMcpDefinition and teamMcpReceiptRoot as JSON.
|
|
2
|
+
// No package discovery, private extension events, settings writes or factory calls.
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { chmodSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { isAbsolute, join } from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
export default function teamAgentMcp(pi) {
|
|
9
|
+
// Keep full UUID entropy and all three native tool names below Pi's 64-character limit.
|
|
10
|
+
const alias = `team_${randomUUID().replaceAll("-", "")}`;
|
|
11
|
+
const wanted = teamMcpDefinition.includeTools;
|
|
12
|
+
const receipt = join(teamMcpReceiptRoot, `${alias}.json`);
|
|
13
|
+
let backend, surface, registration, timer, failure, bindingReady = false, stopped = false, starting = false;
|
|
14
|
+
let wireSuffix;
|
|
15
|
+
let nativeOwned = false;
|
|
16
|
+
const returned = new Set();
|
|
17
|
+
let frame = {
|
|
18
|
+
schema_version: "team-agent.pi-mcp.v1", alias,
|
|
19
|
+
api_shape: Object.fromEntries(["registerMcpServer", "unregisterMcpServer", "getCommands", "getAllTools", "getActiveTools"]
|
|
20
|
+
.map((name) => [name, typeof pi[name]])),
|
|
21
|
+
candidate: teamMcpDefinition.command, tools: wanted,
|
|
22
|
+
workspace: teamMcpDefinition.env.TEAM_AGENT_WORKSPACE,
|
|
23
|
+
agent_id: teamMcpDefinition.env.TEAM_AGENT_ID,
|
|
24
|
+
team_id: teamMcpDefinition.env.TEAM_AGENT_OWNER_TEAM_ID,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function publish(stage, extra = {}) {
|
|
28
|
+
frame = { ...frame, stage, backend, surface, ...extra };
|
|
29
|
+
const temp = `${receipt}.tmp`;
|
|
30
|
+
let written = false;
|
|
31
|
+
try {
|
|
32
|
+
writeFileSync(temp, JSON.stringify(frame) + "\n", { flag: "wx", mode: 0o600 });
|
|
33
|
+
written = true;
|
|
34
|
+
chmodSync(temp, 0o600);
|
|
35
|
+
renameSync(temp, receipt);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (written) {
|
|
38
|
+
try { unlinkSync(temp); } catch { /* only the exact temporary file we created */ }
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fail(ctx, stage, error) {
|
|
45
|
+
clearInterval(timer);
|
|
46
|
+
// Third-party error text can contain credentials: retain correlation, not its content.
|
|
47
|
+
const errorSha256 = createHash("sha256").update(String(error)).digest("hex");
|
|
48
|
+
failure = new Error(`Pi Team MCP unavailable at ${stage}; receipt ${receipt}`);
|
|
49
|
+
try { publish("Failed", { failed_stage: stage, error_sha256: errorSha256 }); }
|
|
50
|
+
catch { failure = new Error("Pi Team MCP failed and its owned receipt could not be written"); }
|
|
51
|
+
ctx.abort();
|
|
52
|
+
ctx.shutdown();
|
|
53
|
+
return failure;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function proxyAvailable() {
|
|
57
|
+
const tool = pi.getAllTools().find((tool) => tool.name === "mcp");
|
|
58
|
+
const properties = tool?.parameters?.properties;
|
|
59
|
+
return properties?.tool?.type === "string" && properties?.args
|
|
60
|
+
&& pi.getActiveTools().includes("mcp");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function directBinding() {
|
|
64
|
+
const namespace = `mcp__${alias}`;
|
|
65
|
+
const tools = pi.getAllTools().filter((tool) => tool.namespace?.name === namespace && tool.exposure !== "hidden");
|
|
66
|
+
const active = new Set(pi.getActiveTools());
|
|
67
|
+
const names = wanted.map((name) => `mcp__${alias}__${name}`);
|
|
68
|
+
if (tools.length !== wanted.length) return undefined;
|
|
69
|
+
if (!names.every((name) => active.has(name) && tools.some((tool) => tool.name === name && tool.exposure === "direct"))) return undefined;
|
|
70
|
+
return names;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function publicBridge(commands) {
|
|
74
|
+
const ownPath = realpathSync(fileURLToPath(import.meta.url));
|
|
75
|
+
const entries = new Set();
|
|
76
|
+
for (const command of commands) {
|
|
77
|
+
if (command.source !== "extension") continue;
|
|
78
|
+
const entry = command.sourceInfo?.path ?? command.path;
|
|
79
|
+
if (!entry || !isAbsolute(entry)) continue;
|
|
80
|
+
const canonical = realpathSync(entry);
|
|
81
|
+
if (canonical === ownPath) continue;
|
|
82
|
+
if (!statSync(canonical).isFile()) throw new Error("Loaded extension entry is not a regular file");
|
|
83
|
+
entries.add(canonical);
|
|
84
|
+
}
|
|
85
|
+
const bridges = [];
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
// Import only the public exports of already-loaded entries, never their default factory.
|
|
88
|
+
const module = await import(pathToFileURL(entry).href);
|
|
89
|
+
if (typeof module.registerMcpServer === "function") bridges.push({ entry, register: module.registerMcpServer });
|
|
90
|
+
}
|
|
91
|
+
if (bridges.length > 1) throw new Error("Ambiguous loaded MCP registration capabilities");
|
|
92
|
+
return bridges[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
96
|
+
if (starting || backend || failure || stopped) return;
|
|
97
|
+
starting = true;
|
|
98
|
+
try {
|
|
99
|
+
publish("ExecutableCatalogReady");
|
|
100
|
+
const native = typeof pi.registerMcpServer === "function" && typeof pi.unregisterMcpServer === "function";
|
|
101
|
+
const commands = typeof pi.getCommands === "function" ? pi.getCommands() : [];
|
|
102
|
+
const builtin = commands.some((command) => command.source === "extension" && command.sourceInfo?.path === "builtin:mcp");
|
|
103
|
+
const deadline = performance.now() + 30_000;
|
|
104
|
+
let loadingTimeout;
|
|
105
|
+
let bridge;
|
|
106
|
+
try {
|
|
107
|
+
if (!(native && builtin)) {
|
|
108
|
+
bridge = await Promise.race([
|
|
109
|
+
publicBridge(commands),
|
|
110
|
+
new Promise((_resolve, reject) => { loadingTimeout = setTimeout(() => reject(new Error("Public MCP capability loading timed out")), 30_000); }),
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
} finally { clearTimeout(loadingTimeout); }
|
|
114
|
+
if (stopped || failure) return;
|
|
115
|
+
if (bridge) {
|
|
116
|
+
backend = "public-bridge";
|
|
117
|
+
surface = "proxy";
|
|
118
|
+
registration = bridge.register({ pi, name: alias, definition: teamMcpDefinition });
|
|
119
|
+
if (!registration || typeof registration.dispose !== "function") throw new Error("Public MCP registration has no owned dispose handle");
|
|
120
|
+
publish("RegistrationSelected", { bridge_entry: bridge.entry });
|
|
121
|
+
} else if (native) {
|
|
122
|
+
backend = "native-api";
|
|
123
|
+
const { command, args, env, cwd } = teamMcpDefinition;
|
|
124
|
+
pi.registerMcpServer(alias, {
|
|
125
|
+
command, args, env, ...(cwd ? { cwd } : {}),
|
|
126
|
+
exposure: "hidden", toolExposure: Object.fromEntries(wanted.map((name) => [name, "direct"])),
|
|
127
|
+
});
|
|
128
|
+
nativeOwned = true;
|
|
129
|
+
publish("RegistrationSelected");
|
|
130
|
+
} else {
|
|
131
|
+
throw new Error("No native MCP API or loaded public MCP registration bridge");
|
|
132
|
+
}
|
|
133
|
+
// Never await connection here: later session_start handlers may own the consumer.
|
|
134
|
+
timer = setInterval(() => {
|
|
135
|
+
if (stopped || failure) return;
|
|
136
|
+
try {
|
|
137
|
+
if (performance.now() > deadline) {
|
|
138
|
+
fail(ctx, "tools-ready-timeout", new Error("MCP consumer did not expose the registered tools"));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const names = backend === "native-api" ? directBinding() : undefined;
|
|
142
|
+
if (names) {
|
|
143
|
+
surface = "direct";
|
|
144
|
+
bindingReady = true;
|
|
145
|
+
clearInterval(timer);
|
|
146
|
+
publish("ToolsAvailable", { actual_tool_names: names, evidence: "pi-runtime-tool-registry" });
|
|
147
|
+
} else if (!builtin && proxyAvailable()) {
|
|
148
|
+
surface = "proxy";
|
|
149
|
+
bindingReady = true;
|
|
150
|
+
clearInterval(timer);
|
|
151
|
+
publish("RegistrationSelected", { evidence: "proxy-present; owned MCP calls still pending" });
|
|
152
|
+
}
|
|
153
|
+
} catch (error) { fail(ctx, "tools-ready", error); }
|
|
154
|
+
}, 25);
|
|
155
|
+
} catch (error) { throw fail(ctx, "registration", error); }
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
async function waitForBinding() {
|
|
159
|
+
while (!bindingReady && !failure && !stopped) await new Promise((resolve) => setTimeout(resolve, 25));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// before_agent_start exceptions are diagnostic in Pi; input must be handled to stop a new run.
|
|
163
|
+
pi.on("input", async (_event, ctx) => {
|
|
164
|
+
await waitForBinding();
|
|
165
|
+
if (failure || stopped) {
|
|
166
|
+
ctx.shutdown();
|
|
167
|
+
return { action: "handled" };
|
|
168
|
+
}
|
|
169
|
+
return { action: "continue" };
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
173
|
+
await waitForBinding();
|
|
174
|
+
if (failure) throw failure;
|
|
175
|
+
if (stopped || !backend) throw fail(ctx, "wire-binding", new Error("No owned MCP registration"));
|
|
176
|
+
if (surface === "direct" && !directBinding()) throw fail(ctx, "wire-binding", new Error("Owned MCP tools became unreachable"));
|
|
177
|
+
if (surface === "proxy" && !proxyAvailable()) throw fail(ctx, "wire-binding", new Error("MCP proxy became unreachable"));
|
|
178
|
+
const examples = wanted.map((tool) => {
|
|
179
|
+
const args = tool === "send_message" ? { to: "leader", content: "..." }
|
|
180
|
+
: tool === "report_result" ? { summary: "..." } : {};
|
|
181
|
+
return surface === "direct"
|
|
182
|
+
? `- ${tool}: mcp__${alias}__${tool}; arguments ${JSON.stringify(args)}`
|
|
183
|
+
: `- ${tool}: mcp(${JSON.stringify({ tool: `${alias}_${tool}`, args })})`;
|
|
184
|
+
});
|
|
185
|
+
const base = wireSuffix && event.systemPrompt.endsWith(wireSuffix)
|
|
186
|
+
? event.systemPrompt.slice(0, -wireSuffix.length) : event.systemPrompt;
|
|
187
|
+
wireSuffix = "\n\n# Team Agent MCP wire binding\n"
|
|
188
|
+
+ "Use these actual tools for the logical Team Agent operations above. Do not pass sender, task_id or schema_version.\n"
|
|
189
|
+
+ examples.join("\n");
|
|
190
|
+
return { systemPrompt: base + wireSuffix };
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
pi.on("before_provider_request", (_event, ctx) => {
|
|
194
|
+
if (failure || stopped || !bindingReady) ctx.abort();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
pi.on("tool_result", (event, ctx) => {
|
|
198
|
+
if (stopped || failure) return;
|
|
199
|
+
const name = event.toolName === "mcp" ? event.input?.tool : event.toolName;
|
|
200
|
+
const tool = wanted.find((tool) => name === (surface === "direct" ? `mcp__${alias}__${tool}` : `${alias}_${tool}`));
|
|
201
|
+
if (!tool) return;
|
|
202
|
+
if (event.isError === false) returned.add(tool);
|
|
203
|
+
try {
|
|
204
|
+
publish(returned.size === wanted.length ? "ToolsVerified" : frame.stage, {
|
|
205
|
+
returned_tools: [...returned], last_call_is_error: event.isError ?? null,
|
|
206
|
+
evidence: "owned tool-result events; business success requires the original results",
|
|
207
|
+
});
|
|
208
|
+
} catch (error) { throw fail(ctx, "tool-receipt", error); }
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
212
|
+
if (stopped) return;
|
|
213
|
+
stopped = true;
|
|
214
|
+
clearInterval(timer);
|
|
215
|
+
const current = registration;
|
|
216
|
+
registration = undefined;
|
|
217
|
+
try {
|
|
218
|
+
if (nativeOwned) {
|
|
219
|
+
nativeOwned = false;
|
|
220
|
+
pi.unregisterMcpServer(alias);
|
|
221
|
+
}
|
|
222
|
+
await current?.dispose();
|
|
223
|
+
publish(failure ? "Failed" : frame.stage, { shutdown: true });
|
|
224
|
+
} catch (error) { throw fail(ctx, "shutdown", error); }
|
|
225
|
+
});
|
|
226
|
+
}
|
|
@@ -2788,43 +2788,6 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2788
2788
|
let _ = std::fs::remove_dir_all(&workspace);
|
|
2789
2789
|
}
|
|
2790
2790
|
|
|
2791
|
-
#[test]
|
|
2792
|
-
fn attach_registry_or_readback_failure_rolls_back_exact_state_bytes() {
|
|
2793
|
-
assert_failure_rolls_back_this_attempt(
|
|
2794
|
-
"attach-failure",
|
|
2795
|
-
MockOps {
|
|
2796
|
-
attach_ok: false,
|
|
2797
|
-
..MockOps::default()
|
|
2798
|
-
},
|
|
2799
|
-
1,
|
|
2800
|
-
0,
|
|
2801
|
-
0,
|
|
2802
|
-
false,
|
|
2803
|
-
);
|
|
2804
|
-
assert_failure_rolls_back_this_attempt(
|
|
2805
|
-
"registry-failure",
|
|
2806
|
-
MockOps {
|
|
2807
|
-
register_ok: false,
|
|
2808
|
-
..MockOps::default()
|
|
2809
|
-
},
|
|
2810
|
-
1,
|
|
2811
|
-
1,
|
|
2812
|
-
0,
|
|
2813
|
-
true,
|
|
2814
|
-
);
|
|
2815
|
-
assert_failure_rolls_back_this_attempt(
|
|
2816
|
-
"readback-failure",
|
|
2817
|
-
MockOps {
|
|
2818
|
-
readback_ok: false,
|
|
2819
|
-
..MockOps::default()
|
|
2820
|
-
},
|
|
2821
|
-
1,
|
|
2822
|
-
1,
|
|
2823
|
-
1,
|
|
2824
|
-
true,
|
|
2825
|
-
);
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
2791
|
#[test]
|
|
2829
2792
|
fn attach_failure_rolls_back_exact_state_bytes() {
|
|
2830
2793
|
assert_failure_rolls_back_this_attempt(
|
|
@@ -365,15 +365,6 @@ fn add_agent_joins_w2_into_running_roster_and_existing_session() {
|
|
|
365
365
|
);
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
// REAL-MACHINE e2e boundary (rt-host-a verifies): after `add-agent w2`, the coordinator delivers
|
|
369
|
-
// `send w2` and w2 reports a result (full round-trip). #[ignore] — needs a live tmux session + worker.
|
|
370
|
-
#[test]
|
|
371
|
-
#[ignore = "real-machine: add-agent then send w2 round-trips"]
|
|
372
|
-
fn add_agent_then_send_w2_round_trips() {
|
|
373
|
-
// The framework asserts: add-agent w2 -> w2 in the live session + roster -> `send w2 <msg>` ->
|
|
374
|
-
// coordinator delivers -> w2 emits a result_envelope (results row). Not unit-testable in-process.
|
|
375
|
-
}
|
|
376
|
-
|
|
377
368
|
#[derive(Clone)]
|
|
378
369
|
struct SocketRecordingRunner {
|
|
379
370
|
recorded: std::sync::Arc<std::sync::Mutex<Vec<Vec<String>>>>,
|