@team-agent/installer 0.5.106 → 0.5.108
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 +1 -1
- package/crates/team-agent/src/cli/emit.rs +3 -3
- package/crates/team-agent/src/cli/mod.rs +87 -4
- package/crates/team-agent/src/cli/models.rs +252 -3
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +155 -37
- package/crates/team-agent/src/cli/tests/base.rs +112 -225
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +184 -0
- package/crates/team-agent/src/cli/tests/status_brief.rs +624 -0
- 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/model_catalog.rs +166 -3
- package/crates/team-agent/src/provider/testdata/cursor-list-models-native.stdout +250 -0
- package/crates/team-agent/src/provider/testdata/grok-models-authenticated.stdout +9 -0
- package/crates/team-agent/src/provider/testdata/grok-models-unauthenticated.stdout +7 -0
- 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
|
@@ -119,65 +119,52 @@ Implement bounded tasks and report result_envelope_v1.
|
|
|
119
119
|
// ───────────────────────────── read_front_matter ─────────────────────────────
|
|
120
120
|
|
|
121
121
|
#[test]
|
|
122
|
-
fn
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
("
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
let p = write_tmp("unterminated.md", text);
|
|
169
|
-
let (meta, body) = read_front_matter(&p).expect("unterminated front matter is plain text");
|
|
170
|
-
assert_eq!(meta, Value::Map(Vec::new()));
|
|
171
|
-
assert_eq!(body, text);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
#[test]
|
|
175
|
-
fn front_matter_non_object_falls_back_to_plain_text() {
|
|
176
|
-
let text = "---\n- a\n- b\n---\nbody\n";
|
|
177
|
-
let p = write_tmp("list.md", text);
|
|
178
|
-
let (meta, body) = read_front_matter(&p).expect("non-object front matter is plain text");
|
|
179
|
-
assert_eq!(meta, Value::Map(Vec::new()));
|
|
180
|
-
assert_eq!(body, text);
|
|
122
|
+
fn front_matter_cases() {
|
|
123
|
+
struct Case {
|
|
124
|
+
id: &'static str,
|
|
125
|
+
file: &'static str,
|
|
126
|
+
text: &'static str,
|
|
127
|
+
meta: &'static [(&'static str, &'static str)],
|
|
128
|
+
body: &'static str,
|
|
129
|
+
}
|
|
130
|
+
const CASES: &[Case] = &[
|
|
131
|
+
// No leading "---\n" → ({}, text) verbatim (compiler.py:175-176).
|
|
132
|
+
Case {
|
|
133
|
+
id: "front_matter_no_marker_returns_empty_meta_and_full_text", file: "no_marker.md",
|
|
134
|
+
text: "hello\nworld\n", meta: &[], body: "hello\nworld\n",
|
|
135
|
+
},
|
|
136
|
+
Case {
|
|
137
|
+
id: "front_matter_basic_splits_meta_and_lstrips_body", file: "basic.md",
|
|
138
|
+
text: "---\nname: x\nrole: R\n---\n\nbody line\n", meta: &[("name", "x"), ("role", "R")], body: "body line\n",
|
|
139
|
+
},
|
|
140
|
+
// Closing marker at the blank line → raw "" → {}; body "body\n".
|
|
141
|
+
Case {
|
|
142
|
+
id: "front_matter_empty_block_is_empty_map", file: "empty.md",
|
|
143
|
+
text: "---\n\n---\nbody\n", meta: &[], body: "body\n",
|
|
144
|
+
},
|
|
145
|
+
// body.lstrip("\n") removes leading NEWLINES but keeps the 2-space indent.
|
|
146
|
+
Case {
|
|
147
|
+
id: "front_matter_body_lstrip_strips_only_newlines", file: "lstrip.md",
|
|
148
|
+
text: "---\nname: x\n---\n\n\n indented body\n", meta: &[("name", "x")], body: " indented body\n",
|
|
149
|
+
},
|
|
150
|
+
Case {
|
|
151
|
+
id: "front_matter_unterminated_falls_back_to_plain_text", file: "unterminated.md",
|
|
152
|
+
text: "---\nname: x\n", meta: &[], body: "---\nname: x\n",
|
|
153
|
+
},
|
|
154
|
+
Case {
|
|
155
|
+
id: "front_matter_non_object_falls_back_to_plain_text", file: "list.md",
|
|
156
|
+
text: "---\n- a\n- b\n---\nbody\n", meta: &[], body: "---\n- a\n- b\n---\nbody\n",
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
for case in CASES {
|
|
160
|
+
let p = write_tmp(case.file, case.text);
|
|
161
|
+
let (meta, body) = read_front_matter(&p).unwrap_or_else(|err| panic!("{}: {err}", case.id));
|
|
162
|
+
let expected_meta = Value::Map(case.meta.iter().map(|(key, value)| {
|
|
163
|
+
((*key).to_string(), Value::Str((*value).to_string()))
|
|
164
|
+
}).collect());
|
|
165
|
+
assert_eq!(meta, expected_meta, "{}: metadata", case.id);
|
|
166
|
+
assert_eq!(body, case.body, "{}: body", case.id);
|
|
167
|
+
}
|
|
181
168
|
}
|
|
182
169
|
|
|
183
170
|
// ───────────────────────────── compile_team: full dict parity ─────────────────────────────
|
|
@@ -54,8 +54,7 @@ fn d1_claim_team_owner_includes_os_user() {
|
|
|
54
54
|
// fingerprint/requested_provider/warning. Golden _receiver_from_claim_target (__init__.py:861-877).
|
|
55
55
|
// Rust LeaderReceiver serializes all 17 (no skip_serializing_if) -> 3 always-null extras leak. RED.
|
|
56
56
|
// (The POPULATED tmux values session_name/window_*/pane_* come from the caller-target scan — a
|
|
57
|
-
// deferred real-tmux seam
|
|
58
|
-
// locked here are unchanged by that scan.)
|
|
57
|
+
// deferred real-tmux seam; the KEY-SET + ORDER locked here are unchanged by that scan.)
|
|
59
58
|
#[test]
|
|
60
59
|
#[serial_test::serial(env)]
|
|
61
60
|
fn d2_claim_leader_receiver_is_fifteen_golden_keys_in_order_no_extras() {
|
|
@@ -98,18 +97,6 @@ fn d2_claim_leader_receiver_is_fifteen_golden_keys_in_order_no_extras() {
|
|
|
98
97
|
], "golden _receiver_from_claim_target 15-key set + ORDER (__init__.py:861-877 + BUG-4 socket-qualified receiver)");
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
// D2 seam — the caller-target SCAN that fills session_name/window_index/window_name/pane_index/
|
|
102
|
-
// pane_tty/pane_current_command from core_list_targets (golden _receiver_from_claim_target reads
|
|
103
|
-
// target[...]). Rust make_receiver leaves them None (no scan). Real-tmux: needs core_list_targets.
|
|
104
|
-
#[test]
|
|
105
|
-
#[ignore = "real-tmux seam: leader_receiver session_name/window_*/pane_* are populated from the \
|
|
106
|
-
caller target via core_list_targets (golden _receiver_from_claim_target); Rust has no \
|
|
107
|
-
scan (values null). Porter wires the target scan; this asserts the populated values."]
|
|
108
|
-
fn d2_receiver_populated_from_caller_target_seam() {
|
|
109
|
-
// Golden contract: new_receiver.session_name == caller_target.session_name, etc. Needs a live
|
|
110
|
-
// tmux target list (core_list_targets) — out of in-process scope.
|
|
111
|
-
}
|
|
112
|
-
|
|
113
100
|
// D3 [BLOCK] — bound_pane is RECEIVER-first (golden :624 receiver.pane_id OR owner.pane_id). PROBE-B
|
|
114
101
|
// (receiver=%9, owner=%1, caller=%9): golden ok=True already_bound. Rust derives OWNER-first
|
|
115
102
|
// (owner %1 != caller %9) -> owner live + !confirm -> refused force_confirm_required. RED.
|
|
@@ -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,
|