@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.
Files changed (29) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/tests/base.rs +112 -225
  4. package/crates/team-agent/src/compiler/tests.rs +46 -59
  5. package/crates/team-agent/src/leader/tests/byte_findings.rs +1 -14
  6. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +57 -173
  7. package/crates/team-agent/src/lifecycle/launch/pi_mcp_extension.js +226 -0
  8. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +0 -37
  9. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +0 -9
  10. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_native_mcp_host.mjs +71 -0
  11. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -66
  12. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +0 -392
  13. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +0 -5
  14. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -361
  15. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -16
  16. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +20 -112
  17. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1 -12
  18. package/crates/team-agent/src/lifecycle/tests/pi_native_mcp_capability_red.rs +546 -0
  19. package/crates/team-agent/src/lifecycle/tests.rs +1 -0
  20. package/crates/team-agent/src/lifecycle/worker_command_context.rs +11 -54
  21. package/crates/team-agent/src/messaging/tests/wave2.rs +2 -302
  22. package/crates/team-agent/src/model/name_similarity.rs +52 -67
  23. package/crates/team-agent/src/model/task_graph.rs +37 -124
  24. package/crates/team-agent/src/model/yaml/tests.rs +46 -165
  25. package/crates/team-agent/src/provider/tests/adapter.rs +0 -20
  26. package/crates/team-agent/src/provider/tests/classify.rs +106 -198
  27. package/crates/team-agent/src/tmux_backend/tests.rs +178 -501
  28. package/crates/team-agent/src/transport/tests/wire.rs +0 -55
  29. package/package.json +4 -4
@@ -1,5 +1,5 @@
1
1
  //! ---
2
- //! purpose: Pi executable/catalog/adapter 验证、per-seat wrapper 与共享启动计划物化
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: 校验 pi-mcp-adapter 的公共 package/entry 能力
187
- /// returns: package identity 与可解析的 absolute entry 完整时成功
188
- /// errors: package 或 entry 缺失时返回 ProviderError;version/digests 仅保留为观测值
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
- validate_pi_adapter_identity(&chain.adapter)
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
- validate_pi_adapter_identity(request.adapter)?;
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::to_string_pretty(&serde_json::Value::Object(server))
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
- r#"const MCP_RUNTIME_REGISTER_EVENT = "pi-mcp-adapter:runtime-register:v1";
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 只做 per-seat runtime MCP registration
385
- /// returns: wrapper 保留 direct Pi 配置且冻结 seat identity 时成功
386
- /// errors: identity、runtime registration 或 candidate 缺失时返回 ProviderError
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
- validate_pi_adapter_identity(adapter)?;
394
- let candidate = candidate_executable.to_string_lossy();
395
- if !source.contains(candidate.as_ref())
396
- || !source.contains("pi-mcp-adapter:runtime-register:v1")
397
- || !source.contains("session_start")
398
- || !source.contains("session_shutdown")
399
- || !source.contains("team_orchestrator")
400
- || !source.contains("includeTools")
401
- || source.contains("createMcpAdapter")
402
- || source.contains("directTools")
403
- || source.contains("toolPrefix")
404
- || source.contains("--mcp-config")
405
- {
406
- return Err(ProviderError::Command(
407
- "Pi wrapper source does not preserve direct Pi plus per-seat runtime MCP".to_string(),
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.adapter, request.candidate_executable)?;
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、catalog 与 adapter
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 (chain, catalog) = resolve_pi_executable_chain()?;
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>>>>,