@team-agent/installer 0.5.68 → 0.5.70
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/diagnose.rs +74 -0
- package/crates/team-agent/src/cli/emit.rs +5 -4
- package/crates/team-agent/src/cli/leader.rs +1 -0
- package/crates/team-agent/src/cli/spec.rs +1 -0
- package/crates/team-agent/src/compiler.rs +54 -7
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
- package/crates/team-agent/src/layout/worker_window_helpers.rs +1 -0
- package/crates/team-agent/src/leader/start.rs +53 -12
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +67 -15
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +18 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +260 -44
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +120 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +5 -1
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +10 -59
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +838 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +9 -3
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +60 -28
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch.rs +10 -2
- package/crates/team-agent/src/lifecycle/profile_launch.rs +3 -2
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +90 -160
- package/crates/team-agent/src/lifecycle/restart/common.rs +136 -28
- package/crates/team-agent/src/lifecycle/restart/remove.rs +37 -1
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +24 -3
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +195 -18
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +30 -12
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_g0.stdout.txt +222 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +5 -2
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +35 -48
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +97 -0
- package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +145 -0
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +243 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +856 -0
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +387 -0
- package/crates/team-agent/src/lifecycle/tests/pi_lifecycle_doctor_red.rs +98 -0
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +216 -0
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests.rs +7 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +61 -4
- package/crates/team-agent/src/messaging/delivery.rs +36 -12
- package/crates/team-agent/src/model/enums.rs +3 -1
- package/crates/team-agent/src/model/permissions.rs +1 -1
- package/crates/team-agent/src/provider/adapter.rs +29 -3
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +5 -2
- package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
- package/crates/team-agent/src/provider/adapters/pi.rs +146 -0
- package/crates/team-agent/src/provider/bypass_flags.rs +1 -0
- package/crates/team-agent/src/provider/classify.rs +2 -2
- package/crates/team-agent/src/provider/faults.rs +1 -1
- package/crates/team-agent/src/provider/session/context_fork.rs +2 -1
- package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/pi.rs +172 -0
- package/crates/team-agent/src/provider/session_scan.rs +4 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +80 -0
- package/crates/team-agent/src/provider/wire.rs +7 -1
- package/crates/team-agent/src/tmux_backend.rs +52 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +9 -5
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
use std::os::unix::fs::PermissionsExt;
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use std::sync::atomic::{AtomicU32, Ordering};
|
|
4
|
+
|
|
5
|
+
use crate::lifecycle::launch::pi_mcp::{
|
|
6
|
+
parse_pi_list_models_table, resolve_pi_executable_chain, validate_pi_adapter_identity,
|
|
7
|
+
validate_pi_executable_chain, validate_pi_wrapper_source, write_pi_wrapper, PiAdapterIdentity,
|
|
8
|
+
PiExecutableChain, PiExecutableFileType, PiWrapperRequest,
|
|
9
|
+
};
|
|
10
|
+
use crate::provider::McpConfig;
|
|
11
|
+
|
|
12
|
+
#[path = "../../../tests/support/hermetic.rs"]
|
|
13
|
+
mod hermetic_guard;
|
|
14
|
+
use hermetic_guard::HermeticTestEnv;
|
|
15
|
+
|
|
16
|
+
const PACKAGE_DIGEST: &str = "ce8b8b6154e83e9732c58bd993e7ed69390617616f4f0e7330274d5ee9e2f620";
|
|
17
|
+
const INDEX_DIGEST: &str = "16d260ac25b66346baab6ecef76680324336953bafc7be8cf95b3df5c611b89e";
|
|
18
|
+
const CATALOG_DIGEST: &str = "726cedb6c3f6fe80a0d7b98918d8ed5063695e01f510a48f46c4bad5daab49fe";
|
|
19
|
+
static NEXT_ROOT: AtomicU32 = AtomicU32::new(0);
|
|
20
|
+
const PI_WRAPPER_CHILD: &str = "TEAM_AGENT_TEST_PI_WRAPPER_CHILD";
|
|
21
|
+
const PI_WRAPPER_AMBIENT_CHILD: &str = "TEAM_AGENT_TEST_PI_WRAPPER_AMBIENT_CHILD";
|
|
22
|
+
const PI_SYMLINK_CHILD: &str = "TEAM_AGENT_TEST_PI_SYMLINK_CHILD";
|
|
23
|
+
const PI_WRAPPER_PARENT_PID: &str = "TEAM_AGENT_TEST_PI_WRAPPER_PARENT_PID";
|
|
24
|
+
const PI_WRAPPER_TEST: &str = concat!(
|
|
25
|
+
"lifecycle::tests::pi_executable_mcp_red::",
|
|
26
|
+
"pi_wrapper_is_atomic_per_seat_and_embeds_exact_candidate"
|
|
27
|
+
);
|
|
28
|
+
const PI_WRAPPER_AMBIENT_TEST: &str = concat!(
|
|
29
|
+
"lifecycle::tests::pi_executable_mcp_red::",
|
|
30
|
+
"pi_wrapper_runtime_registration_does_not_copy_ambient_config"
|
|
31
|
+
);
|
|
32
|
+
const PI_SYMLINK_TEST: &str = concat!(
|
|
33
|
+
"lifecycle::tests::pi_executable_mcp_red::",
|
|
34
|
+
"pi_standard_npm_symlink_is_a_verified_launch_entry"
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
fn run_process_isolated(marker: &str, test_name: &str, body: impl FnOnce()) {
|
|
38
|
+
if std::env::var_os(marker).is_some() {
|
|
39
|
+
body();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let output =
|
|
44
|
+
std::process::Command::new(std::env::current_exe().expect("current lib-test executable"))
|
|
45
|
+
.args(["--exact", test_name, "--nocapture", "--test-threads=1"])
|
|
46
|
+
.env(marker, "1")
|
|
47
|
+
.env(PI_WRAPPER_PARENT_PID, std::process::id().to_string())
|
|
48
|
+
.output()
|
|
49
|
+
.expect("run Pi wrapper send_message fixture in isolated child test process");
|
|
50
|
+
assert!(
|
|
51
|
+
output.status.success(),
|
|
52
|
+
"isolated Pi wrapper child failed: test={test_name} status={:?}\nstdout:\n{}\nstderr:\n{}",
|
|
53
|
+
output.status.code(),
|
|
54
|
+
String::from_utf8_lossy(&output.stdout),
|
|
55
|
+
String::from_utf8_lossy(&output.stderr),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn assert_isolated_child() {
|
|
60
|
+
let parent_pid = std::env::var(PI_WRAPPER_PARENT_PID)
|
|
61
|
+
.expect("isolated child receives the parent test process id");
|
|
62
|
+
assert_ne!(
|
|
63
|
+
std::process::id().to_string(),
|
|
64
|
+
parent_pid,
|
|
65
|
+
"Pi wrapper send_message fixture must not run in the parent lib-test process"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn temp_root(label: &str) -> PathBuf {
|
|
70
|
+
let seq = NEXT_ROOT.fetch_add(1, Ordering::Relaxed);
|
|
71
|
+
let root = std::env::temp_dir().join(format!(
|
|
72
|
+
"team-agent-pi-{label}-{}-{seq}",
|
|
73
|
+
std::process::id()
|
|
74
|
+
));
|
|
75
|
+
std::fs::create_dir_all(&root).expect("create fixture root");
|
|
76
|
+
root
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn adapter_identity(root: &Path) -> PiAdapterIdentity {
|
|
80
|
+
let package_root = root.join("pi-mcp-adapter");
|
|
81
|
+
std::fs::create_dir_all(&package_root).expect("create adapter package root");
|
|
82
|
+
let package_json = package_root.join("package.json");
|
|
83
|
+
let index_ts = package_root.join("index.ts");
|
|
84
|
+
std::fs::write(&package_json, b"{}").expect("write adapter package receipt");
|
|
85
|
+
std::fs::write(&index_ts, b"export const createMcpAdapter = () => {};\n")
|
|
86
|
+
.expect("write loadable adapter entry");
|
|
87
|
+
PiAdapterIdentity {
|
|
88
|
+
package_name: "pi-mcp-adapter".to_string(),
|
|
89
|
+
version: "2.30.0".to_string(),
|
|
90
|
+
extension_entry: "./index.ts".to_string(),
|
|
91
|
+
package_json,
|
|
92
|
+
index_ts,
|
|
93
|
+
package_json_sha256: PACKAGE_DIGEST.to_string(),
|
|
94
|
+
index_ts_sha256: INDEX_DIGEST.to_string(),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fn mcp_config(candidate: &Path, workspace: &Path, agent_id: &str) -> McpConfig {
|
|
99
|
+
McpConfig {
|
|
100
|
+
raw: serde_json::json!({
|
|
101
|
+
"team_orchestrator": {
|
|
102
|
+
"command": candidate,
|
|
103
|
+
"args": ["mcp-server"],
|
|
104
|
+
"env": {
|
|
105
|
+
"TEAM_AGENT_WORKSPACE": workspace,
|
|
106
|
+
"TEAM_AGENT_ID": agent_id,
|
|
107
|
+
"TEAM_AGENT_AGENT_ID": agent_id,
|
|
108
|
+
"TEAM_AGENT_OWNER_TEAM_ID": "team-a",
|
|
109
|
+
"TEAM_AGENT_AUTH_MODE": "subscription"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[test]
|
|
117
|
+
fn pi_executable_chain_freezes_wrapper_real_binary_catalog_and_plugin_identity() {
|
|
118
|
+
let root = temp_root("executable-chain");
|
|
119
|
+
let chain = PiExecutableChain {
|
|
120
|
+
path_entry: PathBuf::from("/Users/fixture/.local/bin/pi"),
|
|
121
|
+
path_entry_type: PiExecutableFileType::Wrapper,
|
|
122
|
+
launch_executable: PathBuf::from("/Users/fixture/.local/bin/pi"),
|
|
123
|
+
real_binary: PathBuf::from(
|
|
124
|
+
"/opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/dist/bundle/cli.js",
|
|
125
|
+
),
|
|
126
|
+
pi_version: "0.84.3".to_string(),
|
|
127
|
+
catalog_sha256: CATALOG_DIGEST.to_string(),
|
|
128
|
+
adapter: adapter_identity(&root),
|
|
129
|
+
};
|
|
130
|
+
validate_pi_executable_chain(&chain).expect("the protocol-capable wrapper chain is valid");
|
|
131
|
+
|
|
132
|
+
let mut homebrew_symlink_entry = chain.clone();
|
|
133
|
+
homebrew_symlink_entry.real_binary = PathBuf::from("/opt/homebrew/bin/pi");
|
|
134
|
+
validate_pi_executable_chain(&homebrew_symlink_entry)
|
|
135
|
+
.expect("the distinct Homebrew symlink entry may resolve to the shebang cli.js target");
|
|
136
|
+
|
|
137
|
+
let mut wrong_version = chain.clone();
|
|
138
|
+
wrong_version.pi_version = "0.84.4".to_string();
|
|
139
|
+
validate_pi_executable_chain(&wrong_version)
|
|
140
|
+
.expect("an arbitrary newer Pi version with the same public protocol is valid");
|
|
141
|
+
|
|
142
|
+
let mut missing_real = chain.clone();
|
|
143
|
+
missing_real.real_binary = PathBuf::new();
|
|
144
|
+
assert!(validate_pi_executable_chain(&missing_real).is_err());
|
|
145
|
+
|
|
146
|
+
let mut wrong_catalog = chain.clone();
|
|
147
|
+
wrong_catalog.catalog_sha256 = "00".repeat(32);
|
|
148
|
+
assert!(validate_pi_executable_chain(&wrong_catalog).is_err());
|
|
149
|
+
|
|
150
|
+
let mut direct_binary = chain.clone();
|
|
151
|
+
direct_binary.path_entry = PathBuf::from("/opt/homebrew/bin/pi");
|
|
152
|
+
direct_binary.path_entry_type = PiExecutableFileType::Binary;
|
|
153
|
+
direct_binary.launch_executable = direct_binary.path_entry.clone();
|
|
154
|
+
direct_binary.real_binary = direct_binary.path_entry.clone();
|
|
155
|
+
assert!(
|
|
156
|
+
validate_pi_executable_chain(&direct_binary).is_err(),
|
|
157
|
+
"a direct Homebrew binary must not replace the verified PATH wrapper launch entry"
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
let mut wrong_plugin = chain;
|
|
161
|
+
wrong_plugin.adapter.version = "2.31.0".to_string();
|
|
162
|
+
wrong_plugin.adapter.package_json_sha256 = "11".repeat(32);
|
|
163
|
+
wrong_plugin.adapter.index_ts_sha256 = "22".repeat(32);
|
|
164
|
+
validate_pi_executable_chain(&wrong_plugin)
|
|
165
|
+
.expect("adapter version and content digests are diagnostic observations only");
|
|
166
|
+
|
|
167
|
+
assert!(parse_pi_list_models_table(b"provider model\nopenai-codex gpt-5.6-luna\n").is_ok());
|
|
168
|
+
assert!(parse_pi_list_models_table(b"provider model\n").is_err());
|
|
169
|
+
assert!(parse_pi_list_models_table(b"provider model\nmalformed\n").is_err());
|
|
170
|
+
std::fs::remove_dir_all(root).expect("remove executable chain fixture");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[test]
|
|
174
|
+
fn pi_standard_npm_symlink_is_a_verified_launch_entry() {
|
|
175
|
+
run_process_isolated(PI_SYMLINK_CHILD, PI_SYMLINK_TEST, || {
|
|
176
|
+
assert_isolated_child();
|
|
177
|
+
let root = temp_root("npm-symlink");
|
|
178
|
+
let bin = root.join("bin");
|
|
179
|
+
let later_bin = root.join("later-bin");
|
|
180
|
+
let package_root = root.join("pi-mcp-adapter");
|
|
181
|
+
std::fs::create_dir_all(&bin).expect("create bin");
|
|
182
|
+
std::fs::create_dir_all(&later_bin).expect("create later bin");
|
|
183
|
+
std::fs::create_dir_all(&package_root).expect("create adapter root");
|
|
184
|
+
let real = root.join("cli.js");
|
|
185
|
+
let script = format!(
|
|
186
|
+
"#!/bin/sh\ncase \"$1\" in\n --version) printf '0.84.4\\n' ;;\n --list-models) printf 'provider model\\nteam-agent qwen3.8-27b\\n' ;;\n list) printf 'npm:pi-mcp-adapter\\n{}\\n' ;;\n *) exit 64 ;;\nesac\n",
|
|
187
|
+
package_root.display()
|
|
188
|
+
);
|
|
189
|
+
std::fs::write(&real, &script).expect("write protocol-capable Pi entry");
|
|
190
|
+
std::fs::set_permissions(&real, std::fs::Permissions::from_mode(0o755))
|
|
191
|
+
.expect("make Pi entry executable");
|
|
192
|
+
std::os::unix::fs::symlink(&real, bin.join("pi")).expect("create npm-style symlink");
|
|
193
|
+
std::fs::write(later_bin.join("pi"), script).expect("write later Pi wrapper");
|
|
194
|
+
std::fs::set_permissions(later_bin.join("pi"), std::fs::Permissions::from_mode(0o755))
|
|
195
|
+
.expect("make later Pi wrapper executable");
|
|
196
|
+
std::fs::write(
|
|
197
|
+
package_root.join("package.json"),
|
|
198
|
+
br#"{"name":"pi-mcp-adapter","version":"2.30.0","pi":{"extensions":["./index.ts"]}}"#,
|
|
199
|
+
)
|
|
200
|
+
.expect("write adapter package");
|
|
201
|
+
std::fs::write(
|
|
202
|
+
package_root.join("index.ts"),
|
|
203
|
+
b"export const createMcpAdapter = () => {};\n",
|
|
204
|
+
)
|
|
205
|
+
.expect("write adapter entry");
|
|
206
|
+
unsafe {
|
|
207
|
+
std::env::set_var(
|
|
208
|
+
"PATH",
|
|
209
|
+
std::env::join_paths([&bin, &later_bin]).expect("join fixture PATH"),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let (chain, models) = resolve_pi_executable_chain()
|
|
214
|
+
.expect("the first PATH Pi entry must define direct Pi behavior");
|
|
215
|
+
assert_eq!(chain.path_entry, bin.join("pi"));
|
|
216
|
+
assert_eq!(chain.path_entry_type, PiExecutableFileType::Symlink);
|
|
217
|
+
assert_eq!(chain.launch_executable, bin.join("pi"));
|
|
218
|
+
assert_eq!(
|
|
219
|
+
chain.real_binary,
|
|
220
|
+
std::fs::canonicalize(&real).expect("canonical real Pi entry")
|
|
221
|
+
);
|
|
222
|
+
assert_eq!(models, ["team-agent/qwen3.8-27b"]);
|
|
223
|
+
std::fs::remove_dir_all(root).expect("remove symlink fixture");
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[test]
|
|
228
|
+
fn pi_adapter_detector_requires_exact_2_30_0_entry_and_digest() {
|
|
229
|
+
let root = temp_root("adapter-identity");
|
|
230
|
+
let exact = adapter_identity(&root);
|
|
231
|
+
validate_pi_adapter_identity(&exact).expect("protocol-capable adapter identity");
|
|
232
|
+
|
|
233
|
+
let mut wrong_name = exact.clone();
|
|
234
|
+
wrong_name.package_name = "pi-mcp-adapter-fork".to_string();
|
|
235
|
+
assert!(validate_pi_adapter_identity(&wrong_name).is_err());
|
|
236
|
+
|
|
237
|
+
let mut missing_entry = exact.clone();
|
|
238
|
+
missing_entry.extension_entry.clear();
|
|
239
|
+
missing_entry.index_ts = PathBuf::new();
|
|
240
|
+
assert!(validate_pi_adapter_identity(&missing_entry).is_err());
|
|
241
|
+
|
|
242
|
+
let mut unloadable_entry = exact.clone();
|
|
243
|
+
unloadable_entry.index_ts = root.join("pi-mcp-adapter/missing.ts");
|
|
244
|
+
assert!(validate_pi_adapter_identity(&unloadable_entry).is_err());
|
|
245
|
+
|
|
246
|
+
let mut newer_observation = exact;
|
|
247
|
+
newer_observation.version = "9.7.3".to_string();
|
|
248
|
+
newer_observation.package_json_sha256 = "11".repeat(32);
|
|
249
|
+
newer_observation.index_ts_sha256 = "22".repeat(32);
|
|
250
|
+
validate_pi_adapter_identity(&newer_observation)
|
|
251
|
+
.expect("version and digests do not define adapter protocol capability");
|
|
252
|
+
std::fs::remove_dir_all(root).expect("remove adapter identity fixture");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#[test]
|
|
256
|
+
fn pi_wrapper_is_atomic_per_seat_and_embeds_exact_candidate() {
|
|
257
|
+
run_process_isolated(PI_WRAPPER_CHILD, PI_WRAPPER_TEST, || {
|
|
258
|
+
let hermetic = HermeticTestEnv::enter("pi-wrapper-send");
|
|
259
|
+
assert_isolated_child();
|
|
260
|
+
pi_wrapper_is_atomic_body(&hermetic);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fn pi_wrapper_is_atomic_body(hermetic: &HermeticTestEnv) {
|
|
265
|
+
let root = hermetic.workspace("wrapper");
|
|
266
|
+
let workspace = root.join("workspace");
|
|
267
|
+
let candidate = root.join("candidate/team-agent");
|
|
268
|
+
std::fs::create_dir_all(candidate.parent().expect("candidate parent"))
|
|
269
|
+
.expect("create candidate parent");
|
|
270
|
+
std::fs::write(&candidate, b"candidate").expect("write candidate fixture");
|
|
271
|
+
let destination = workspace.join(".team/runtime/pi/team-a/worker-a/team-mcp.ts");
|
|
272
|
+
let adapter = adapter_identity(&root);
|
|
273
|
+
|
|
274
|
+
let written = write_pi_wrapper(PiWrapperRequest {
|
|
275
|
+
destination: &destination,
|
|
276
|
+
adapter: &adapter,
|
|
277
|
+
candidate_executable: &candidate,
|
|
278
|
+
mcp_config: &mcp_config(&candidate, &workspace, "worker-a"),
|
|
279
|
+
team_id: "team-a",
|
|
280
|
+
agent_id: "worker-a",
|
|
281
|
+
workspace: &workspace,
|
|
282
|
+
include_tools: &["send_message", "report_result"],
|
|
283
|
+
})
|
|
284
|
+
.expect("atomic wrapper write");
|
|
285
|
+
assert_eq!(written, destination);
|
|
286
|
+
assert_eq!(
|
|
287
|
+
std::fs::metadata(&written)
|
|
288
|
+
.expect("wrapper metadata")
|
|
289
|
+
.permissions()
|
|
290
|
+
.mode()
|
|
291
|
+
& 0o777,
|
|
292
|
+
0o600
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
let source = std::fs::read_to_string(&written).expect("read wrapper");
|
|
296
|
+
assert!(source.contains(candidate.to_string_lossy().as_ref()));
|
|
297
|
+
assert!(source.contains("TEAM_AGENT_ID") && source.contains("worker-a"));
|
|
298
|
+
assert!(source.contains("TEAM_AGENT_OWNER_TEAM_ID") && source.contains("team-a"));
|
|
299
|
+
assert!(source.contains("pi-mcp-adapter:runtime-register:v1"));
|
|
300
|
+
assert!(source.contains("session_start") && source.contains("session_shutdown"));
|
|
301
|
+
assert!(source.contains("includeTools"));
|
|
302
|
+
assert!(source.contains("send_message") && source.contains("report_result"));
|
|
303
|
+
assert!(!source.contains("createMcpAdapter"));
|
|
304
|
+
assert!(!source.contains("directTools"));
|
|
305
|
+
assert!(!source.contains("toolPrefix"));
|
|
306
|
+
assert!(!source.contains("\"lifecycle\""));
|
|
307
|
+
assert!(!source.contains("command: \"team-agent\""));
|
|
308
|
+
assert!(!source.contains("--mcp-config"));
|
|
309
|
+
|
|
310
|
+
let other_destination = workspace.join(".team/runtime/pi/team-a/worker-b/team-mcp.ts");
|
|
311
|
+
write_pi_wrapper(PiWrapperRequest {
|
|
312
|
+
destination: &other_destination,
|
|
313
|
+
adapter: &adapter,
|
|
314
|
+
candidate_executable: &candidate,
|
|
315
|
+
mcp_config: &mcp_config(&candidate, &workspace, "worker-b"),
|
|
316
|
+
team_id: "team-a",
|
|
317
|
+
agent_id: "worker-b",
|
|
318
|
+
workspace: &workspace,
|
|
319
|
+
include_tools: &["send_message", "report_result"],
|
|
320
|
+
})
|
|
321
|
+
.expect("write second seat wrapper");
|
|
322
|
+
let other_source = std::fs::read_to_string(&other_destination).expect("read second wrapper");
|
|
323
|
+
assert!(source.contains("worker-a") && !source.contains("worker-b"));
|
|
324
|
+
assert!(other_source.contains("worker-b") && !other_source.contains("worker-a"));
|
|
325
|
+
assert_ne!(written, other_destination);
|
|
326
|
+
|
|
327
|
+
let entries = std::fs::read_dir(destination.parent().expect("wrapper parent"))
|
|
328
|
+
.expect("list wrapper parent")
|
|
329
|
+
.map(|entry| entry.expect("entry").file_name())
|
|
330
|
+
.collect::<Vec<_>>();
|
|
331
|
+
assert_eq!(
|
|
332
|
+
entries,
|
|
333
|
+
[std::ffi::OsString::from("team-mcp.ts")],
|
|
334
|
+
"atomic write must not leave a temporary or backup sibling"
|
|
335
|
+
);
|
|
336
|
+
std::fs::remove_dir_all(root).expect("remove wrapper fixture");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#[test]
|
|
340
|
+
fn pi_wrapper_runtime_registration_does_not_copy_ambient_config() {
|
|
341
|
+
run_process_isolated(PI_WRAPPER_AMBIENT_CHILD, PI_WRAPPER_AMBIENT_TEST, || {
|
|
342
|
+
let hermetic = HermeticTestEnv::enter("pi-wrapper-negative-send");
|
|
343
|
+
assert_isolated_child();
|
|
344
|
+
pi_wrapper_import_failure_body(&hermetic);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
fn pi_wrapper_import_failure_body(hermetic: &HermeticTestEnv) {
|
|
349
|
+
let root = hermetic.workspace("wrapper-negative");
|
|
350
|
+
let workspace = root.join("workspace");
|
|
351
|
+
let candidate = root.join("candidate/team-agent");
|
|
352
|
+
std::fs::create_dir_all(candidate.parent().expect("candidate parent"))
|
|
353
|
+
.expect("create candidate parent");
|
|
354
|
+
std::fs::write(&candidate, b"candidate").expect("write candidate fixture");
|
|
355
|
+
std::fs::create_dir_all(&workspace).expect("create workspace");
|
|
356
|
+
std::fs::write(
|
|
357
|
+
workspace.join(".mcp.json"),
|
|
358
|
+
r#"{"mcpServers":{"team_orchestrator":{"command":"/tmp/ambient-evil"}}}"#,
|
|
359
|
+
)
|
|
360
|
+
.expect("write malicious ambient config");
|
|
361
|
+
|
|
362
|
+
let destination = workspace.join(".team/runtime/pi/team-a/worker-a/team-mcp.ts");
|
|
363
|
+
let adapter = adapter_identity(&root);
|
|
364
|
+
write_pi_wrapper(PiWrapperRequest {
|
|
365
|
+
destination: &destination,
|
|
366
|
+
adapter: &adapter,
|
|
367
|
+
candidate_executable: &candidate,
|
|
368
|
+
mcp_config: &mcp_config(&candidate, &workspace, "worker-a"),
|
|
369
|
+
team_id: "team-a",
|
|
370
|
+
agent_id: "worker-a",
|
|
371
|
+
workspace: &workspace,
|
|
372
|
+
include_tools: &["send_message", "report_result"],
|
|
373
|
+
})
|
|
374
|
+
.expect("runtime wrapper leaves ambient config to direct Pi");
|
|
375
|
+
let source = std::fs::read_to_string(&destination).expect("wrapper source");
|
|
376
|
+
assert!(!source.contains("ambient-evil"));
|
|
377
|
+
validate_pi_wrapper_source(&source, &adapter, &candidate)
|
|
378
|
+
.expect("runtime registration and exact candidate");
|
|
379
|
+
assert!(!source.contains("createMcpAdapter"));
|
|
380
|
+
assert!(!source.contains("--mcp-config"));
|
|
381
|
+
|
|
382
|
+
let missing_proxy_tools = source.replace("\"includeTools\"", "\"missingTools\"");
|
|
383
|
+
validate_pi_wrapper_source(&missing_proxy_tools, &adapter, &candidate)
|
|
384
|
+
.expect_err("wrapper without the MCP proxy tool allowlist must refuse");
|
|
385
|
+
|
|
386
|
+
std::fs::remove_dir_all(root).expect("remove wrapper fixture");
|
|
387
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
use std::path::{Path, PathBuf};
|
|
2
|
+
|
|
3
|
+
use crate::cli::diagnose::{pi_doctor_facts, PiBackingStatus, PiDoctorInput, PiMcpStatus};
|
|
4
|
+
use crate::lifecycle::launch::pi_mcp::{pi_cleanup_plan, pi_seat_paths, PiCleanupAction};
|
|
5
|
+
use crate::provider::{AuthHintStatus, ClassifySource, TurnState};
|
|
6
|
+
|
|
7
|
+
fn doctor_input() -> PiDoctorInput {
|
|
8
|
+
PiDoctorInput {
|
|
9
|
+
executable_chain_verified: true,
|
|
10
|
+
pi_version: Some("0.84.3".to_string()),
|
|
11
|
+
adapter_version: Some("2.30.0".to_string()),
|
|
12
|
+
catalog_sha256: Some(
|
|
13
|
+
"726cedb6c3f6fe80a0d7b98918d8ed5063695e01f510a48f46c4bad5daab49fe".to_string(),
|
|
14
|
+
),
|
|
15
|
+
selected_model: Some("team-agent/qwen3.8-27b".to_string()),
|
|
16
|
+
candidate_executable: Some(PathBuf::from("/candidate/team-agent")),
|
|
17
|
+
wrapper: Some(PathBuf::from(
|
|
18
|
+
"/workspace/.team/runtime/pi/team-a/worker-a/team-mcp.ts",
|
|
19
|
+
)),
|
|
20
|
+
mcp_roundtrip_at: None,
|
|
21
|
+
backing: PiBackingStatus::Pending,
|
|
22
|
+
activity: TurnState::Unknown,
|
|
23
|
+
activity_source: ClassifySource::SessionFile,
|
|
24
|
+
env_key_names: vec![
|
|
25
|
+
"TEAM_AGENT_ID".to_string(),
|
|
26
|
+
"TEAM_AGENT_OWNER_TEAM_ID".to_string(),
|
|
27
|
+
],
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn pi_stop_retains_backing_and_never_scans_by_process_name() {
|
|
33
|
+
let paths = pi_seat_paths(Path::new("/workspace"), "team-a", "worker-a");
|
|
34
|
+
let stop = pi_cleanup_plan(PiCleanupAction::Stop, &paths);
|
|
35
|
+
assert!(
|
|
36
|
+
stop.delete_paths.is_empty(),
|
|
37
|
+
"stop must retain both the Team-owned wrapper and session backing"
|
|
38
|
+
);
|
|
39
|
+
assert!(stop.retain_paths.contains(&paths.wrapper));
|
|
40
|
+
assert!(stop.retain_paths.contains(&paths.sessions));
|
|
41
|
+
assert_eq!(
|
|
42
|
+
stop.process_name_scan, None,
|
|
43
|
+
"Pi cleanup may use exact recorded pane/child receipts, never a process-name scan"
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
let remove = pi_cleanup_plan(PiCleanupAction::Remove, &paths);
|
|
47
|
+
assert_eq!(remove.delete_paths, [paths.wrapper.clone()]);
|
|
48
|
+
assert!(remove.retain_paths.contains(&paths.sessions));
|
|
49
|
+
assert_eq!(remove.process_name_scan, None);
|
|
50
|
+
assert!(
|
|
51
|
+
remove
|
|
52
|
+
.delete_paths
|
|
53
|
+
.iter()
|
|
54
|
+
.all(|path| path.starts_with(&paths.runtime_root)),
|
|
55
|
+
"remove may delete only exact Team-owned seat paths"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[test]
|
|
60
|
+
fn pi_doctor_separates_configured_roundtrip_backing_and_activity_unknown() {
|
|
61
|
+
let facts = pi_doctor_facts(doctor_input());
|
|
62
|
+
|
|
63
|
+
assert_eq!(facts.mcp, PiMcpStatus::ConfiguredLazy);
|
|
64
|
+
assert!(
|
|
65
|
+
!facts.mcp_connected,
|
|
66
|
+
"wrapper existence is not an MCP roundtrip"
|
|
67
|
+
);
|
|
68
|
+
assert_eq!(facts.backing, PiBackingStatus::Pending);
|
|
69
|
+
assert_eq!(facts.activity, TurnState::Unknown);
|
|
70
|
+
assert_eq!(facts.activity_source, ClassifySource::SessionFile);
|
|
71
|
+
assert_eq!(facts.auth_hint, AuthHintStatus::Unknown);
|
|
72
|
+
|
|
73
|
+
let serialized = serde_json::to_string(&facts).expect("serialize doctor facts");
|
|
74
|
+
assert!(serialized.contains("configured_lazy"));
|
|
75
|
+
assert!(serialized.contains("unknown"));
|
|
76
|
+
for secret_surface in [
|
|
77
|
+
"api_key",
|
|
78
|
+
"access_token",
|
|
79
|
+
"refresh_token",
|
|
80
|
+
"cookie",
|
|
81
|
+
"authorization",
|
|
82
|
+
] {
|
|
83
|
+
assert!(
|
|
84
|
+
!serialized.to_ascii_lowercase().contains(secret_surface),
|
|
85
|
+
"doctor must expose identity/digest/key names, never credential values"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let connected = pi_doctor_facts(PiDoctorInput {
|
|
90
|
+
mcp_roundtrip_at: Some("2026-08-29T10:02:00+00:00".to_string()),
|
|
91
|
+
backing: PiBackingStatus::Captured,
|
|
92
|
+
..doctor_input()
|
|
93
|
+
});
|
|
94
|
+
assert_eq!(connected.mcp, PiMcpStatus::RoundtripVerified);
|
|
95
|
+
assert!(connected.mcp_connected);
|
|
96
|
+
assert_eq!(connected.backing, PiBackingStatus::Captured);
|
|
97
|
+
assert_eq!(connected.activity, TurnState::Unknown);
|
|
98
|
+
}
|