@team-agent/installer 0.5.72 → 0.5.73
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/leader/start.rs +8 -0
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +43 -7
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +71 -1
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +396 -3
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +3 -1
- package/crates/team-agent/src/provider/adapters/pi.rs +12 -6
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +1 -1
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -162,6 +162,7 @@ pub(crate) fn prepare_leader_start_with_nested_attach(
|
|
|
162
162
|
std::env::var_os("TMUX").is_some(),
|
|
163
163
|
managed_client_attach_mode,
|
|
164
164
|
managed_exec_provider,
|
|
165
|
+
allow_nested_attach,
|
|
165
166
|
)?;
|
|
166
167
|
Ok(PreparedLeaderStart {
|
|
167
168
|
plan,
|
|
@@ -196,6 +197,7 @@ pub(crate) fn leader_start_plan_after_ambient_authority_check(
|
|
|
196
197
|
in_tmux,
|
|
197
198
|
managed_client_attach_mode,
|
|
198
199
|
false,
|
|
200
|
+
false,
|
|
199
201
|
)
|
|
200
202
|
}
|
|
201
203
|
|
|
@@ -210,6 +212,7 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
210
212
|
in_tmux: bool,
|
|
211
213
|
managed_client_attach_mode: Option<ManagedClientAttachMode>,
|
|
212
214
|
managed_exec_provider: bool,
|
|
215
|
+
allow_nested_attach: bool,
|
|
213
216
|
) -> Result<LeaderStartPlan, LeaderError> {
|
|
214
217
|
if attach_session.is_some() && !confirm_attach {
|
|
215
218
|
return Err(LeaderError::Start(
|
|
@@ -327,6 +330,11 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
327
330
|
tool_categories: &tools,
|
|
328
331
|
team_mcp_tools: &team_mcp_tools,
|
|
329
332
|
mcp_config: &mcp_config,
|
|
333
|
+
session_scope: crate::lifecycle::launch::pi_mcp::pi_leader_session_scope(
|
|
334
|
+
provider_args,
|
|
335
|
+
external_path,
|
|
336
|
+
allow_nested_attach,
|
|
337
|
+
),
|
|
330
338
|
},
|
|
331
339
|
)
|
|
332
340
|
.map_err(|error| LeaderError::Start(error.to_string()))?
|
|
@@ -816,6 +816,27 @@ pub(crate) fn resolve_pi_executable_chain(
|
|
|
816
816
|
Ok((chain, models))
|
|
817
817
|
}
|
|
818
818
|
|
|
819
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
820
|
+
pub(crate) enum PiSessionScope {
|
|
821
|
+
Isolated,
|
|
822
|
+
NativeDefault,
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/// Zero-argument managed Pi is the interactive leader entry. Only that route
|
|
826
|
+
/// may use Pi's cwd-keyed native session root; every explicit or worker route
|
|
827
|
+
/// remains Team Agent session-isolated.
|
|
828
|
+
pub(crate) fn pi_leader_session_scope(
|
|
829
|
+
provider_args: &[String],
|
|
830
|
+
external_path: bool,
|
|
831
|
+
allow_nested_attach: bool,
|
|
832
|
+
) -> PiSessionScope {
|
|
833
|
+
if provider_args.is_empty() && !external_path && !allow_nested_attach {
|
|
834
|
+
PiSessionScope::NativeDefault
|
|
835
|
+
} else {
|
|
836
|
+
PiSessionScope::Isolated
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
819
840
|
pub(crate) struct PiMaterializeRequest<'a> {
|
|
820
841
|
pub workspace: &'a Path,
|
|
821
842
|
pub team_id: &'a str,
|
|
@@ -826,11 +847,13 @@ pub(crate) struct PiMaterializeRequest<'a> {
|
|
|
826
847
|
pub tool_categories: &'a [&'a str],
|
|
827
848
|
pub team_mcp_tools: &'a [&'a str],
|
|
828
849
|
pub mcp_config: &'a McpConfig,
|
|
850
|
+
pub session_scope: PiSessionScope,
|
|
829
851
|
}
|
|
830
852
|
|
|
831
853
|
/// ---
|
|
832
854
|
/// purpose: 共享物化 Pi leader 与 TeamMate 的 wrapper、UUID 和 CommandPlan
|
|
833
|
-
/// returns: fresh Pi CommandPlan
|
|
855
|
+
/// returns: fresh Pi CommandPlan;isolated seats carry their session root, while the
|
|
856
|
+
/// interactive leader preserves Pi's native default root
|
|
834
857
|
/// errors: resolver、exact model、wrapper 或 argv 构造失败时返回 ProviderError
|
|
835
858
|
/// ---
|
|
836
859
|
pub(crate) fn materialize_pi_plan(
|
|
@@ -863,8 +886,18 @@ fn materialize_pi_plan_with_session(
|
|
|
863
886
|
.map(|model| select_exact_pi_model(&catalog, model))
|
|
864
887
|
.transpose()?;
|
|
865
888
|
let paths = pi_seat_paths(request.workspace, request.team_id, request.agent_id);
|
|
889
|
+
let session_dir = match request.session_scope {
|
|
890
|
+
PiSessionScope::Isolated => Some(paths.sessions.as_path()),
|
|
891
|
+
PiSessionScope::NativeDefault => None,
|
|
892
|
+
};
|
|
866
893
|
let resume = if let Some((session_id, session_path, spawn_cwd)) = resume {
|
|
867
|
-
let
|
|
894
|
+
let session_dir = session_dir.ok_or_else(|| {
|
|
895
|
+
ProviderError::ResumeUnavailable(format!(
|
|
896
|
+
"Pi session root is unavailable {}",
|
|
897
|
+
paths.sessions.display()
|
|
898
|
+
))
|
|
899
|
+
})?;
|
|
900
|
+
let session_root = std::fs::canonicalize(session_dir).map_err(|error| {
|
|
868
901
|
ProviderError::ResumeUnavailable(format!(
|
|
869
902
|
"Pi session root is unavailable {}: {error}",
|
|
870
903
|
paths.sessions.display()
|
|
@@ -885,8 +918,11 @@ fn materialize_pi_plan_with_session(
|
|
|
885
918
|
let _ = spawn_cwd;
|
|
886
919
|
Some((session_id, exact_path))
|
|
887
920
|
} else {
|
|
888
|
-
|
|
889
|
-
.map_err(|error|
|
|
921
|
+
if let Some(session_dir) = session_dir {
|
|
922
|
+
std::fs::create_dir_all(session_dir).map_err(|error| {
|
|
923
|
+
ProviderError::Io(format!("{}: {error}", session_dir.display()))
|
|
924
|
+
})?;
|
|
925
|
+
}
|
|
890
926
|
None
|
|
891
927
|
};
|
|
892
928
|
let candidate = candidate_from_mcp_config(request.mcp_config)?;
|
|
@@ -908,7 +944,7 @@ fn materialize_pi_plan_with_session(
|
|
|
908
944
|
effort: request.effort,
|
|
909
945
|
system_prompt: request.system_prompt,
|
|
910
946
|
tool_categories: request.tool_categories,
|
|
911
|
-
session_dir
|
|
947
|
+
session_dir,
|
|
912
948
|
session: PiSessionSelector::Resume { path: &exact_path },
|
|
913
949
|
agent_id: request.agent_id,
|
|
914
950
|
})?;
|
|
@@ -922,7 +958,7 @@ fn materialize_pi_plan_with_session(
|
|
|
922
958
|
effort: request.effort,
|
|
923
959
|
system_prompt: request.system_prompt,
|
|
924
960
|
tool_categories: request.tool_categories,
|
|
925
|
-
session_dir
|
|
961
|
+
session_dir,
|
|
926
962
|
session: PiSessionSelector::Fresh {
|
|
927
963
|
session_id: session_id.as_str(),
|
|
928
964
|
},
|
|
@@ -933,7 +969,7 @@ fn materialize_pi_plan_with_session(
|
|
|
933
969
|
Ok(CommandPlan {
|
|
934
970
|
argv,
|
|
935
971
|
expected_session_id,
|
|
936
|
-
provider_projects_root:
|
|
972
|
+
provider_projects_root: session_dir.map(Path::to_path_buf),
|
|
937
973
|
managed_mcp_config: false,
|
|
938
974
|
})
|
|
939
975
|
}
|
|
@@ -189,6 +189,7 @@ pub(super) fn spawn_agents(
|
|
|
189
189
|
tool_categories: &resolved_tool_refs,
|
|
190
190
|
team_mcp_tools: &["send_message", "report_result"],
|
|
191
191
|
mcp_config: &mcp_config,
|
|
192
|
+
session_scope: crate::lifecycle::launch::pi_mcp::PiSessionScope::Isolated,
|
|
192
193
|
},
|
|
193
194
|
)
|
|
194
195
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?
|
|
@@ -402,6 +402,7 @@ pub(super) fn spawn_agent_window(
|
|
|
402
402
|
tool_categories: &resolved_tool_refs,
|
|
403
403
|
team_mcp_tools: &["send_message", "report_result"],
|
|
404
404
|
mcp_config: &mcp_config,
|
|
405
|
+
session_scope: crate::lifecycle::launch::pi_mcp::PiSessionScope::Isolated,
|
|
405
406
|
};
|
|
406
407
|
match resume_session_id {
|
|
407
408
|
Some(session_id) => {
|
|
@@ -22,7 +22,7 @@ fn command_for_model(model: &str) -> Vec<String> {
|
|
|
22
22
|
effort: Some(ProviderEffort::Medium),
|
|
23
23
|
system_prompt: "worker contract",
|
|
24
24
|
tool_categories: &["mcp_team"],
|
|
25
|
-
session_dir: Path::new("/workspace/.team/runtime/pi/t1/w1/sessions"),
|
|
25
|
+
session_dir: Some(Path::new("/workspace/.team/runtime/pi/t1/w1/sessions")),
|
|
26
26
|
session: PiSessionSelector::Fresh {
|
|
27
27
|
session_id: "716ba0cb-c491-4471-b41a-43c8d5e1f05a",
|
|
28
28
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use std::path::Path;
|
|
2
2
|
|
|
3
|
+
use crate::lifecycle::launch::pi_mcp::{pi_leader_session_scope, PiSessionScope};
|
|
3
4
|
use crate::model::enums::ProviderEffort;
|
|
4
5
|
use crate::provider::adapters::pi::{build_pi_command_argv, PiCommandRequest, PiSessionSelector};
|
|
5
6
|
|
|
@@ -11,7 +12,9 @@ fn request(session: PiSessionSelector<'static>) -> PiCommandRequest<'static> {
|
|
|
11
12
|
effort: Some(ProviderEffort::High),
|
|
12
13
|
system_prompt: "frozen worker prompt",
|
|
13
14
|
tool_categories: &["mcp_team", "fs_read", "fs_write", "execute_bash"],
|
|
14
|
-
session_dir: Path::new(
|
|
15
|
+
session_dir: Some(Path::new(
|
|
16
|
+
"/workspace/.team/runtime/pi/team-a/worker-a/sessions",
|
|
17
|
+
)),
|
|
15
18
|
session,
|
|
16
19
|
agent_id: "worker-a",
|
|
17
20
|
}
|
|
@@ -30,6 +33,73 @@ fn pi_command_omits_unset_model_and_thinking_flags() {
|
|
|
30
33
|
assert!(!argv.iter().any(|arg| arg == "--thinking"), "argv={argv:?}");
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
fn leader_request() -> PiCommandRequest<'static> {
|
|
37
|
+
PiCommandRequest {
|
|
38
|
+
executable: Path::new("/verified/pi"),
|
|
39
|
+
extension: Path::new("/workspace/.team/runtime/pi/current/leader/team-mcp.ts"),
|
|
40
|
+
model: None,
|
|
41
|
+
effort: None,
|
|
42
|
+
system_prompt: "frozen leader prompt",
|
|
43
|
+
tool_categories: &["mcp_team", "fs_read", "fs_write", "execute_bash"],
|
|
44
|
+
session_dir: None,
|
|
45
|
+
session: PiSessionSelector::Fresh {
|
|
46
|
+
session_id: "da8c3622-2378-4d05-a26c-e826a6ef6d63",
|
|
47
|
+
},
|
|
48
|
+
agent_id: "leader",
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[test]
|
|
53
|
+
fn pi_interactive_leader_argv_preserves_native_session_discovery() {
|
|
54
|
+
let argv = build_pi_command_argv(leader_request()).expect("native Pi leader command");
|
|
55
|
+
|
|
56
|
+
assert_eq!(
|
|
57
|
+
argv,
|
|
58
|
+
[
|
|
59
|
+
"/verified/pi",
|
|
60
|
+
"-e",
|
|
61
|
+
"/workspace/.team/runtime/pi/current/leader/team-mcp.ts",
|
|
62
|
+
"--append-system-prompt",
|
|
63
|
+
"frozen leader prompt",
|
|
64
|
+
"--session-id",
|
|
65
|
+
"da8c3622-2378-4d05-a26c-e826a6ef6d63",
|
|
66
|
+
"--name",
|
|
67
|
+
"leader",
|
|
68
|
+
]
|
|
69
|
+
.map(str::to_string)
|
|
70
|
+
);
|
|
71
|
+
assert!(!argv.iter().any(|arg| arg == "--session-dir"));
|
|
72
|
+
for forbidden in ["--cwd", "--workspace", "--session"] {
|
|
73
|
+
assert!(!argv.iter().any(|arg| arg == forbidden), "argv={argv:?}");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#[test]
|
|
78
|
+
fn pi_leader_session_scope_keeps_explicit_and_external_routes_isolated() {
|
|
79
|
+
assert_eq!(
|
|
80
|
+
pi_leader_session_scope(&[], false, false),
|
|
81
|
+
PiSessionScope::NativeDefault
|
|
82
|
+
);
|
|
83
|
+
for (args, external_path, allow_nested_attach) in [
|
|
84
|
+
(
|
|
85
|
+
vec![
|
|
86
|
+
"--model".to_string(),
|
|
87
|
+
"openai-codex/gpt-5.6-sol".to_string(),
|
|
88
|
+
],
|
|
89
|
+
false,
|
|
90
|
+
false,
|
|
91
|
+
),
|
|
92
|
+
(Vec::new(), true, false),
|
|
93
|
+
(Vec::new(), false, true),
|
|
94
|
+
] {
|
|
95
|
+
assert_eq!(
|
|
96
|
+
pi_leader_session_scope(&args, external_path, allow_nested_attach),
|
|
97
|
+
PiSessionScope::Isolated,
|
|
98
|
+
"only the zero-argument managed route may use Pi's native root"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
33
103
|
#[test]
|
|
34
104
|
fn pi_command_preserves_direct_pi_defaults_with_only_team_runtime_additions() {
|
|
35
105
|
let fresh = build_pi_command_argv(request(PiSessionSelector::Fresh {
|
|
@@ -59,7 +59,9 @@ fn command(tools: &[&str], effort: ProviderEffort, model: &str) -> Result<Vec<St
|
|
|
59
59
|
effort: Some(effort),
|
|
60
60
|
system_prompt: "Pi worker contract.",
|
|
61
61
|
tool_categories: tools,
|
|
62
|
-
session_dir: Path::new(
|
|
62
|
+
session_dir: Some(Path::new(
|
|
63
|
+
"/workspace/.team/runtime/pi/t1/pi-worker/sessions",
|
|
64
|
+
)),
|
|
63
65
|
session: PiSessionSelector::Fresh {
|
|
64
66
|
session_id: "c5ddf218-24a5-4e74-960c-ab6606ea7e8c",
|
|
65
67
|
},
|
|
@@ -2,13 +2,19 @@ use std::os::unix::fs::PermissionsExt;
|
|
|
2
2
|
use std::path::{Path, PathBuf};
|
|
3
3
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
4
4
|
|
|
5
|
+
use crate::lifecycle::launch::launch_with_transport_in_workspace;
|
|
5
6
|
use crate::lifecycle::launch::pi_mcp::{
|
|
7
|
+
PiAdapterIdentity, PiExecutableChain, PiExecutableFileType, PiMaterializeRequest,
|
|
8
|
+
PiSessionScope, PiWrapperRequest, materialize_pi_plan, materialize_pi_resume_plan,
|
|
6
9
|
parse_pi_list_models_table, pi_seat_paths, resolve_pi_executable_chain,
|
|
7
10
|
validate_pi_adapter_identity, validate_pi_executable_chain, validate_pi_wrapper_source,
|
|
8
|
-
write_pi_wrapper, write_pi_wrapper_with_publish,
|
|
9
|
-
PiExecutableFileType, PiWrapperRequest,
|
|
11
|
+
write_pi_wrapper, write_pi_wrapper_with_publish,
|
|
10
12
|
};
|
|
11
|
-
use crate::
|
|
13
|
+
use crate::lifecycle::start_agent_with_transport;
|
|
14
|
+
use crate::model::enums::ProviderEffort;
|
|
15
|
+
use crate::provider::session_scan::{CaptureSessionContext, scan_session_candidates_once};
|
|
16
|
+
use crate::provider::{McpConfig, Provider, SessionId};
|
|
17
|
+
use crate::transport::test_support::OfflineTransport;
|
|
12
18
|
|
|
13
19
|
#[path = "../../../tests/support/hermetic.rs"]
|
|
14
20
|
mod hermetic_guard;
|
|
@@ -34,6 +40,11 @@ const PI_SYMLINK_TEST: &str = concat!(
|
|
|
34
40
|
"lifecycle::tests::pi_executable_mcp_red::",
|
|
35
41
|
"pi_standard_npm_symlink_is_a_verified_launch_entry"
|
|
36
42
|
);
|
|
43
|
+
const PI_MATERIALIZER_CHILD: &str = "TEAM_AGENT_TEST_PI_MATERIALIZER_CHILD";
|
|
44
|
+
const PI_MATERIALIZER_TEST: &str = concat!(
|
|
45
|
+
"lifecycle::tests::pi_executable_mcp_red::",
|
|
46
|
+
"pi_materializer_and_worker_routes_use_the_recorded_scope"
|
|
47
|
+
);
|
|
37
48
|
|
|
38
49
|
fn run_process_isolated(marker: &str, test_name: &str, body: impl FnOnce()) {
|
|
39
50
|
if std::env::var_os(marker).is_some() {
|
|
@@ -504,3 +515,385 @@ fn pi_wrapper_import_failure_body(hermetic: &HermeticTestEnv) {
|
|
|
504
515
|
|
|
505
516
|
std::fs::remove_dir_all(root).expect("remove wrapper fixture");
|
|
506
517
|
}
|
|
518
|
+
|
|
519
|
+
#[test]
|
|
520
|
+
fn pi_materializer_and_worker_routes_use_the_recorded_scope() {
|
|
521
|
+
run_process_isolated(PI_MATERIALIZER_CHILD, PI_MATERIALIZER_TEST, || {
|
|
522
|
+
let hermetic = HermeticTestEnv::enter("pi-materializer-scope");
|
|
523
|
+
assert_isolated_child();
|
|
524
|
+
pi_materializer_and_worker_routes_body(&hermetic);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
fn pi_materializer_and_worker_routes_body(hermetic: &HermeticTestEnv) {
|
|
529
|
+
let fixture_root = hermetic.root().join("materializer-fixture");
|
|
530
|
+
let bin = fixture_root.join("bin");
|
|
531
|
+
let adapter_root = fixture_root.join("pi-mcp-adapter");
|
|
532
|
+
let candidate = fixture_root.join("candidate/team-agent");
|
|
533
|
+
let real = fixture_root.join("pi-real");
|
|
534
|
+
std::fs::create_dir_all(&bin).expect("create Pi fixture bin");
|
|
535
|
+
std::fs::create_dir_all(&adapter_root).expect("create Pi adapter fixture");
|
|
536
|
+
std::fs::create_dir_all(candidate.parent().expect("candidate parent"))
|
|
537
|
+
.expect("create candidate parent");
|
|
538
|
+
std::fs::write(&candidate, b"candidate").expect("write candidate fixture");
|
|
539
|
+
std::fs::write(
|
|
540
|
+
adapter_root.join("package.json"),
|
|
541
|
+
br#"{"name":"pi-mcp-adapter","version":"2.30.0","pi":{"extensions":["./index.ts"]}}"#,
|
|
542
|
+
)
|
|
543
|
+
.expect("write adapter package");
|
|
544
|
+
std::fs::write(
|
|
545
|
+
adapter_root.join("index.ts"),
|
|
546
|
+
b"export const createMcpAdapter = () => {};\n",
|
|
547
|
+
)
|
|
548
|
+
.expect("write adapter entry");
|
|
549
|
+
let pi_script = format!(
|
|
550
|
+
"#!/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 0 ;;\nesac\n",
|
|
551
|
+
adapter_root.display()
|
|
552
|
+
);
|
|
553
|
+
std::fs::write(&real, &pi_script).expect("write Pi real fixture");
|
|
554
|
+
std::fs::set_permissions(&real, std::fs::Permissions::from_mode(0o755))
|
|
555
|
+
.expect("make Pi real fixture executable");
|
|
556
|
+
std::os::unix::fs::symlink(&real, bin.join("pi")).expect("create Pi PATH symlink");
|
|
557
|
+
let _path = hermetic.with_env(
|
|
558
|
+
"PATH",
|
|
559
|
+
std::env::join_paths([&bin])
|
|
560
|
+
.expect("join Pi fixture PATH")
|
|
561
|
+
.to_str()
|
|
562
|
+
.expect("Pi fixture PATH is UTF-8"),
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
let native_workspace = hermetic.workspace("native");
|
|
566
|
+
let native_paths = pi_seat_paths(&native_workspace, "team-a", "leader");
|
|
567
|
+
let native_config = mcp_config(&candidate, &native_workspace, "leader");
|
|
568
|
+
let native = materialize_pi_plan(PiMaterializeRequest {
|
|
569
|
+
workspace: &native_workspace,
|
|
570
|
+
team_id: "team-a",
|
|
571
|
+
agent_id: "leader",
|
|
572
|
+
model: None,
|
|
573
|
+
effort: None,
|
|
574
|
+
system_prompt: "native leader fixture",
|
|
575
|
+
tool_categories: &["mcp_team"],
|
|
576
|
+
team_mcp_tools: &["send_message"],
|
|
577
|
+
mcp_config: &native_config,
|
|
578
|
+
session_scope: PiSessionScope::NativeDefault,
|
|
579
|
+
})
|
|
580
|
+
.expect("NativeDefault materializer");
|
|
581
|
+
assert!(
|
|
582
|
+
native.provider_projects_root.is_none(),
|
|
583
|
+
"NativeDefault must not expose a provider capture root: {native:?}"
|
|
584
|
+
);
|
|
585
|
+
assert!(
|
|
586
|
+
!native_paths.sessions.exists(),
|
|
587
|
+
"NativeDefault must not create the Team Agent seat session root"
|
|
588
|
+
);
|
|
589
|
+
for forbidden in ["--session-dir", "--cwd", "--workspace", "--session"] {
|
|
590
|
+
assert!(
|
|
591
|
+
!native.argv.iter().any(|arg| arg == forbidden),
|
|
592
|
+
"NativeDefault fresh argv must omit {forbidden}: {:?}",
|
|
593
|
+
native.argv
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
let isolated_workspace = hermetic.workspace("isolated");
|
|
598
|
+
let isolated_paths = pi_seat_paths(&isolated_workspace, "team-a", "worker-a");
|
|
599
|
+
let isolated_config = mcp_config(&candidate, &isolated_workspace, "worker-a");
|
|
600
|
+
let isolated = materialize_pi_plan(PiMaterializeRequest {
|
|
601
|
+
workspace: &isolated_workspace,
|
|
602
|
+
team_id: "team-a",
|
|
603
|
+
agent_id: "worker-a",
|
|
604
|
+
model: Some("team-agent/qwen3.8-27b"),
|
|
605
|
+
effort: Some(ProviderEffort::Medium),
|
|
606
|
+
system_prompt: "isolated worker fixture",
|
|
607
|
+
tool_categories: &["mcp_team"],
|
|
608
|
+
team_mcp_tools: &["send_message"],
|
|
609
|
+
mcp_config: &isolated_config,
|
|
610
|
+
session_scope: PiSessionScope::Isolated,
|
|
611
|
+
})
|
|
612
|
+
.expect("Isolated materializer");
|
|
613
|
+
assert_eq!(
|
|
614
|
+
isolated.provider_projects_root.as_deref(),
|
|
615
|
+
Some(isolated_paths.sessions.as_path())
|
|
616
|
+
);
|
|
617
|
+
assert_session_pair(&isolated.argv, &isolated_paths.sessions);
|
|
618
|
+
assert!(!isolated.argv.iter().any(|arg| arg == "--cwd"));
|
|
619
|
+
assert!(!isolated.argv.iter().any(|arg| arg == "--workspace"));
|
|
620
|
+
|
|
621
|
+
let session_id = SessionId::new("pi-materializer-resume");
|
|
622
|
+
let exact = isolated_paths.sessions.join("2026/09/exact.jsonl");
|
|
623
|
+
std::fs::create_dir_all(exact.parent().expect("exact session parent"))
|
|
624
|
+
.expect("create exact session parent");
|
|
625
|
+
std::fs::write(&exact, b"{}\n").expect("write exact session fixture");
|
|
626
|
+
let resumed = materialize_pi_resume_plan(
|
|
627
|
+
PiMaterializeRequest {
|
|
628
|
+
workspace: &isolated_workspace,
|
|
629
|
+
team_id: "team-a",
|
|
630
|
+
agent_id: "worker-a",
|
|
631
|
+
model: None,
|
|
632
|
+
effort: None,
|
|
633
|
+
system_prompt: "isolated resume fixture",
|
|
634
|
+
tool_categories: &["mcp_team"],
|
|
635
|
+
team_mcp_tools: &["send_message"],
|
|
636
|
+
mcp_config: &isolated_config,
|
|
637
|
+
session_scope: PiSessionScope::Isolated,
|
|
638
|
+
},
|
|
639
|
+
&session_id,
|
|
640
|
+
&exact,
|
|
641
|
+
&isolated_workspace,
|
|
642
|
+
)
|
|
643
|
+
.expect("Isolated exact-path resume materializer");
|
|
644
|
+
assert_eq!(
|
|
645
|
+
resumed.provider_projects_root.as_deref(),
|
|
646
|
+
Some(isolated_paths.sessions.as_path())
|
|
647
|
+
);
|
|
648
|
+
assert_session_pair(&resumed.argv, &isolated_paths.sessions);
|
|
649
|
+
assert!(
|
|
650
|
+
resumed
|
|
651
|
+
.argv
|
|
652
|
+
.windows(2)
|
|
653
|
+
.any(|pair| { pair == ["--session", exact.to_string_lossy().as_ref()] })
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
let native_resume = materialize_pi_resume_plan(
|
|
657
|
+
PiMaterializeRequest {
|
|
658
|
+
workspace: &native_workspace,
|
|
659
|
+
team_id: "team-a",
|
|
660
|
+
agent_id: "leader",
|
|
661
|
+
model: None,
|
|
662
|
+
effort: None,
|
|
663
|
+
system_prompt: "native resume fixture",
|
|
664
|
+
tool_categories: &["mcp_team"],
|
|
665
|
+
team_mcp_tools: &["send_message"],
|
|
666
|
+
mcp_config: &native_config,
|
|
667
|
+
session_scope: PiSessionScope::NativeDefault,
|
|
668
|
+
},
|
|
669
|
+
&session_id,
|
|
670
|
+
&exact,
|
|
671
|
+
&native_workspace,
|
|
672
|
+
);
|
|
673
|
+
assert!(
|
|
674
|
+
matches!(
|
|
675
|
+
native_resume,
|
|
676
|
+
Err(crate::provider::ProviderError::ResumeUnavailable(_))
|
|
677
|
+
),
|
|
678
|
+
"NativeDefault resume must fail closed before emitting a native HOME path: {native_resume:?}"
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
let home_candidate = hermetic.home().join(".pi/agent/sessions/home.jsonl");
|
|
682
|
+
std::fs::create_dir_all(home_candidate.parent().expect("home session parent"))
|
|
683
|
+
.expect("create home session parent");
|
|
684
|
+
std::fs::write(&home_candidate, b"{}\n").expect("write home candidate");
|
|
685
|
+
let other_paths = pi_seat_paths(&isolated_workspace, "team-a", "worker-b");
|
|
686
|
+
let other_candidate = other_paths.sessions.join("other.jsonl");
|
|
687
|
+
std::fs::create_dir_all(other_candidate.parent().expect("other session parent"))
|
|
688
|
+
.expect("create other session parent");
|
|
689
|
+
std::fs::write(&other_candidate, b"{}\n").expect("write other seat candidate");
|
|
690
|
+
for (label, outside) in [("HOME", &home_candidate), ("other seat", &other_candidate)] {
|
|
691
|
+
let refused = materialize_pi_resume_plan(
|
|
692
|
+
PiMaterializeRequest {
|
|
693
|
+
workspace: &isolated_workspace,
|
|
694
|
+
team_id: "team-a",
|
|
695
|
+
agent_id: "worker-a",
|
|
696
|
+
model: None,
|
|
697
|
+
effort: None,
|
|
698
|
+
system_prompt: "isolated negative resume fixture",
|
|
699
|
+
tool_categories: &["mcp_team"],
|
|
700
|
+
team_mcp_tools: &["send_message"],
|
|
701
|
+
mcp_config: &isolated_config,
|
|
702
|
+
session_scope: PiSessionScope::Isolated,
|
|
703
|
+
},
|
|
704
|
+
&session_id,
|
|
705
|
+
&outside,
|
|
706
|
+
&isolated_workspace,
|
|
707
|
+
);
|
|
708
|
+
assert!(
|
|
709
|
+
matches!(
|
|
710
|
+
refused,
|
|
711
|
+
Err(crate::provider::ProviderError::ResumeUnavailable(_))
|
|
712
|
+
),
|
|
713
|
+
"{label} candidate must be refused by the exact seat-root check: {refused:?}"
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
let worker_workspace = hermetic.workspace("worker-spawn");
|
|
718
|
+
let spec_path = worker_workspace
|
|
719
|
+
.join(".team")
|
|
720
|
+
.join("team-a")
|
|
721
|
+
.join("team.spec.yaml");
|
|
722
|
+
std::fs::create_dir_all(spec_path.parent().expect("worker Pi spec parent"))
|
|
723
|
+
.expect("create worker Pi spec parent");
|
|
724
|
+
let spec = crate::model::yaml::loads(
|
|
725
|
+
"team:\n name: team-a\nruntime:\n session_name: team-workers\n display_backend: tmux_attach\nagents:\n - id: worker-a\n role: Worker A\n provider: pi\n model: team-agent/qwen3.8-27b\n auth_mode: subscription\n dangerously_skip_permissions: false\n tools:\n - mcp_team\n - id: worker-b\n role: Worker B\n provider: pi\n model: team-agent/qwen3.8-27b\n auth_mode: subscription\n dangerously_skip_permissions: false\n tools:\n - mcp_team\n",
|
|
726
|
+
)
|
|
727
|
+
.expect("parse worker Pi spec");
|
|
728
|
+
std::fs::write(&spec_path, crate::model::yaml::dumps(&spec)).expect("write worker Pi spec");
|
|
729
|
+
let transport = OfflineTransport::new();
|
|
730
|
+
let started = launch_with_transport_in_workspace(
|
|
731
|
+
&worker_workspace,
|
|
732
|
+
&spec_path,
|
|
733
|
+
false,
|
|
734
|
+
true,
|
|
735
|
+
true,
|
|
736
|
+
&transport,
|
|
737
|
+
)
|
|
738
|
+
.expect("worker fresh spawn construction")
|
|
739
|
+
.started;
|
|
740
|
+
assert_eq!(started.len(), 2, "both Pi worker seats must be spawned");
|
|
741
|
+
for worker in ["worker-a", "worker-b"] {
|
|
742
|
+
let paths = pi_seat_paths(&worker_workspace, "team-a", worker);
|
|
743
|
+
let started_agent = started
|
|
744
|
+
.iter()
|
|
745
|
+
.find(|entry| entry.agent_id.as_str() == worker)
|
|
746
|
+
.expect("started worker entry");
|
|
747
|
+
assert_eq!(
|
|
748
|
+
started_agent.provider_projects_root.as_deref(),
|
|
749
|
+
Some(paths.sessions.as_path()),
|
|
750
|
+
"fresh worker must expose its exact seat capture root"
|
|
751
|
+
);
|
|
752
|
+
let record = transport
|
|
753
|
+
.spawn_records()
|
|
754
|
+
.into_iter()
|
|
755
|
+
.find(|(_, argv)| {
|
|
756
|
+
argv.iter()
|
|
757
|
+
.any(|arg| arg.as_str() == paths.wrapper.to_string_lossy().as_ref())
|
|
758
|
+
})
|
|
759
|
+
.expect("transport spawn record for worker seat");
|
|
760
|
+
assert_session_pair(&record.1, &paths.sessions);
|
|
761
|
+
assert!(
|
|
762
|
+
!record
|
|
763
|
+
.1
|
|
764
|
+
.iter()
|
|
765
|
+
.any(|arg| arg.as_str() == home_candidate.to_string_lossy().as_ref())
|
|
766
|
+
);
|
|
767
|
+
assert!(
|
|
768
|
+
!record
|
|
769
|
+
.1
|
|
770
|
+
.iter()
|
|
771
|
+
.any(|arg| arg.as_str() == other_candidate.to_string_lossy().as_ref())
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
let pending_session_id = started_agent
|
|
775
|
+
.pending_session_id
|
|
776
|
+
.clone()
|
|
777
|
+
.expect("fresh worker carries a pending Pi session id");
|
|
778
|
+
write_pi_session_header(
|
|
779
|
+
&home_candidate,
|
|
780
|
+
pending_session_id.as_str(),
|
|
781
|
+
&worker_workspace,
|
|
782
|
+
"2099-01-01T00:00:00Z",
|
|
783
|
+
);
|
|
784
|
+
let other_worker = if worker == "worker-a" {
|
|
785
|
+
"worker-b"
|
|
786
|
+
} else {
|
|
787
|
+
"worker-a"
|
|
788
|
+
};
|
|
789
|
+
let cross_seat = pi_seat_paths(&worker_workspace, "team-a", other_worker)
|
|
790
|
+
.sessions
|
|
791
|
+
.join("foreign.jsonl");
|
|
792
|
+
write_pi_session_header(
|
|
793
|
+
&cross_seat,
|
|
794
|
+
pending_session_id.as_str(),
|
|
795
|
+
&worker_workspace,
|
|
796
|
+
"2099-01-01T00:00:00Z",
|
|
797
|
+
);
|
|
798
|
+
let candidates = scan_session_candidates_once(
|
|
799
|
+
Provider::Pi,
|
|
800
|
+
&CaptureSessionContext {
|
|
801
|
+
agent_id: worker.to_string(),
|
|
802
|
+
spawn_cwd: worker_workspace.clone(),
|
|
803
|
+
pane_id: None,
|
|
804
|
+
pane_pid: None,
|
|
805
|
+
spawned_at: Some(started_agent.spawned_at.clone()),
|
|
806
|
+
expected_session_id: Some(pending_session_id),
|
|
807
|
+
provider_projects_root: Some(paths.sessions.clone()),
|
|
808
|
+
},
|
|
809
|
+
)
|
|
810
|
+
.expect("scan the recorded worker seat only");
|
|
811
|
+
assert!(
|
|
812
|
+
candidates.is_empty(),
|
|
813
|
+
"HOME and another worker seat must not become capture candidates: {candidates:?}"
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
let restart_workspace = hermetic.workspace("worker-restart");
|
|
818
|
+
let restart_paths = pi_seat_paths(&restart_workspace, "team-a", "worker-a");
|
|
819
|
+
let restart_exact = restart_paths.sessions.join("2026/09/restart.jsonl");
|
|
820
|
+
write_pi_session_header(
|
|
821
|
+
&restart_exact,
|
|
822
|
+
"pi-restart-session",
|
|
823
|
+
&restart_workspace,
|
|
824
|
+
"2026-09-01T00:00:01Z",
|
|
825
|
+
);
|
|
826
|
+
crate::state::persist::save_runtime_state(
|
|
827
|
+
&restart_workspace,
|
|
828
|
+
&serde_json::json!({
|
|
829
|
+
"session_name": "team-workers",
|
|
830
|
+
"active_team_key": "team-a",
|
|
831
|
+
"agents": {
|
|
832
|
+
"worker-a": {
|
|
833
|
+
"id": "worker-a",
|
|
834
|
+
"status": "running",
|
|
835
|
+
"provider": "pi",
|
|
836
|
+
"role": "Worker A",
|
|
837
|
+
"model": "team-agent/qwen3.8-27b",
|
|
838
|
+
"auth_mode": "subscription",
|
|
839
|
+
"tools": ["mcp_team"],
|
|
840
|
+
"session_id": "pi-restart-session",
|
|
841
|
+
"rollout_path": restart_exact.to_string_lossy(),
|
|
842
|
+
"spawn_cwd": restart_workspace.to_string_lossy(),
|
|
843
|
+
"first_send_at": "2026-09-01T00:00:00Z"
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}),
|
|
847
|
+
)
|
|
848
|
+
.expect("write restart state");
|
|
849
|
+
let restart_transport = OfflineTransport::new();
|
|
850
|
+
let _ = start_agent_with_transport(
|
|
851
|
+
&restart_workspace,
|
|
852
|
+
&crate::model::ids::AgentId::new("worker-a"),
|
|
853
|
+
false,
|
|
854
|
+
false,
|
|
855
|
+
false,
|
|
856
|
+
None,
|
|
857
|
+
&restart_transport,
|
|
858
|
+
);
|
|
859
|
+
let restart_record = restart_transport
|
|
860
|
+
.spawn_records()
|
|
861
|
+
.into_iter()
|
|
862
|
+
.next()
|
|
863
|
+
.expect("restart must reach the worker spawn construction");
|
|
864
|
+
assert_session_pair(&restart_record.1, &restart_paths.sessions);
|
|
865
|
+
assert!(
|
|
866
|
+
restart_record
|
|
867
|
+
.1
|
|
868
|
+
.windows(2)
|
|
869
|
+
.any(|pair| { pair == ["--session", restart_exact.to_string_lossy().as_ref()] })
|
|
870
|
+
);
|
|
871
|
+
assert!(
|
|
872
|
+
!restart_record
|
|
873
|
+
.1
|
|
874
|
+
.iter()
|
|
875
|
+
.any(|arg| arg.as_str() == home_candidate.to_string_lossy().as_ref())
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
fn assert_session_pair(argv: &[String], session_root: &Path) {
|
|
880
|
+
let expected = session_root.to_string_lossy();
|
|
881
|
+
assert!(
|
|
882
|
+
argv.windows(2)
|
|
883
|
+
.any(|pair| pair[0] == "--session-dir" && pair[1].as_str() == expected.as_ref()),
|
|
884
|
+
"argv must contain exact --session-dir pair for {}: {argv:?}",
|
|
885
|
+
session_root.display()
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
fn write_pi_session_header(path: &Path, id: &str, cwd: &Path, timestamp: &str) {
|
|
890
|
+
std::fs::create_dir_all(path.parent().expect("session parent")).expect("create session parent");
|
|
891
|
+
let header = serde_json::json!({
|
|
892
|
+
"type": "session",
|
|
893
|
+
"version": 3,
|
|
894
|
+
"id": id,
|
|
895
|
+
"cwd": cwd,
|
|
896
|
+
"timestamp": timestamp,
|
|
897
|
+
});
|
|
898
|
+
std::fs::write(path, format!("{header}\n")).expect("write Pi session header");
|
|
899
|
+
}
|
|
@@ -60,7 +60,9 @@ fn resume_argv(path: &Path) -> Result<Vec<String>, ProviderError> {
|
|
|
60
60
|
effort: Some(ProviderEffort::Medium),
|
|
61
61
|
system_prompt: "worker contract",
|
|
62
62
|
tool_categories: &["mcp_team"],
|
|
63
|
-
session_dir: Path::new(
|
|
63
|
+
session_dir: Some(Path::new(
|
|
64
|
+
"/workspace/.team/runtime/pi/team-a/worker-a/sessions",
|
|
65
|
+
)),
|
|
64
66
|
session: PiSessionSelector::Resume { path },
|
|
65
67
|
agent_id: "worker-a",
|
|
66
68
|
})
|
|
@@ -50,7 +50,8 @@ pub(crate) struct PiCommandRequest<'a> {
|
|
|
50
50
|
pub effort: Option<ProviderEffort>,
|
|
51
51
|
pub system_prompt: &'a str,
|
|
52
52
|
pub tool_categories: &'a [&'a str],
|
|
53
|
-
|
|
53
|
+
/// Team-owned session root for isolated seats; `None` preserves Pi's native default.
|
|
54
|
+
pub session_dir: Option<&'a Path>,
|
|
54
55
|
pub session: PiSessionSelector<'a>,
|
|
55
56
|
pub agent_id: &'a str,
|
|
56
57
|
}
|
|
@@ -65,14 +66,15 @@ pub(crate) fn build_pi_command_argv(
|
|
|
65
66
|
) -> Result<Vec<String>, ProviderError> {
|
|
66
67
|
if request.executable.as_os_str().is_empty()
|
|
67
68
|
|| request.extension.as_os_str().is_empty()
|
|
68
|
-
|| request
|
|
69
|
+
|| request
|
|
70
|
+
.session_dir
|
|
71
|
+
.is_some_and(|session_dir| session_dir.as_os_str().is_empty())
|
|
69
72
|
|| request.model.is_some_and(|model| model.trim().is_empty())
|
|
70
73
|
|| request.system_prompt.trim().is_empty()
|
|
71
74
|
|| request.agent_id.trim().is_empty()
|
|
72
75
|
{
|
|
73
76
|
return Err(ProviderError::Command(
|
|
74
|
-
"Pi command requires executable, extension, prompt,
|
|
75
|
-
.to_string(),
|
|
77
|
+
"Pi command requires executable, extension, prompt, and agent id".to_string(),
|
|
76
78
|
));
|
|
77
79
|
}
|
|
78
80
|
|
|
@@ -110,9 +112,13 @@ pub(crate) fn build_pi_command_argv(
|
|
|
110
112
|
argv.extend([
|
|
111
113
|
"--append-system-prompt".to_string(),
|
|
112
114
|
request.system_prompt.to_string(),
|
|
113
|
-
"--session-dir".to_string(),
|
|
114
|
-
request.session_dir.to_string_lossy().into_owned(),
|
|
115
115
|
]);
|
|
116
|
+
if let Some(session_dir) = request.session_dir {
|
|
117
|
+
argv.extend([
|
|
118
|
+
"--session-dir".to_string(),
|
|
119
|
+
session_dir.to_string_lossy().into_owned(),
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
116
122
|
// The shared lifecycle materializer always revalidates resume root/path/header.
|
|
117
123
|
// Keep the pure argv builder usable before a fixture root is materialized, while
|
|
118
124
|
// refusing a missing exact file once the recorded path's parent exists.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.73",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.73",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.73",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.73"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: team-agent
|
|
3
3
|
description: Use only when the user explicitly asks to start, operate, inspect, shutdown, or restart a Team Agent team. Treat the team-agent CLI as a sealed appliance.
|
|
4
4
|
requires_team_agent: ">=0.5.0"
|
|
5
|
-
last_verified_against: "0.5.
|
|
5
|
+
last_verified_against: "0.5.73"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Team Agent
|