@team-agent/installer 0.5.113 → 0.5.114
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 +252 -84
- package/Cargo.toml +3 -1
- package/crates/team-agent/Cargo.toml +3 -0
- package/crates/team-agent/src/cli/adapters.rs +28 -15
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +55 -20
- package/crates/team-agent/src/cli/leader.rs +16 -0
- package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
- package/crates/team-agent/src/cli/mod.rs +152 -8
- package/crates/team-agent/src/cli/models.rs +68 -18
- package/crates/team-agent/src/cli/spec.rs +7 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +88 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
- package/crates/team-agent/src/compiler.rs +66 -18
- package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
- package/crates/team-agent/src/contract_runtime/config.rs +309 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +26 -8
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/prompt.rs +906 -0
- package/crates/team-agent/src/leader/start.rs +260 -16
- package/crates/team-agent/src/leader/tests/mod.rs +2 -0
- package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +472 -388
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
- package/crates/team-agent/src/lifecycle/launch.rs +39 -28
- package/crates/team-agent/src/lifecycle/types.rs +4 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
- package/crates/team-agent/src/main.rs +6 -0
- package/crates/team-agent/src/messaging/delivery.rs +50 -1
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
- package/crates/team-agent/src/model/spec.rs +24 -12
- package/crates/team-agent/src/state/persist.rs +73 -0
- package/package.json +4 -4
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
//! Process-isolated launch planning, not native provider inference.
|
|
2
|
+
#![cfg(unix)]
|
|
3
|
+
#[path = "../../../tests/support/hermetic.rs"]
|
|
4
|
+
mod hermetic;
|
|
5
|
+
use crate::leader::{LeaderError, LeaderStartMode};
|
|
6
|
+
use crate::lifecycle::launch::pi_mcp::{materialize_pi_plan, PiMaterializeRequest, PiSessionScope};
|
|
7
|
+
use crate::provider::{McpConfig, Provider};
|
|
8
|
+
use hermetic::HermeticTestEnv;
|
|
9
|
+
use serde_json::json;
|
|
10
|
+
use std::fs;
|
|
11
|
+
use std::os::unix::fs::{symlink, PermissionsExt};
|
|
12
|
+
use std::path::{Path, PathBuf};
|
|
13
|
+
use std::process::Command;
|
|
14
|
+
|
|
15
|
+
const SENTINEL: &str = "LEADER_ONLY_309_SENTINEL_🙂";
|
|
16
|
+
const CHILD: &str = "TEAM_AGENT_TEST_LEADER_PROMPT_CHILD";
|
|
17
|
+
fn isolated(name: &str, body: impl FnOnce()) {
|
|
18
|
+
if std::env::var(CHILD).ok().as_deref() == Some(name) {
|
|
19
|
+
body();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
let output = Command::new(std::env::current_exe().unwrap())
|
|
23
|
+
.args([
|
|
24
|
+
"--exact",
|
|
25
|
+
&format!("leader::tests::prompt_contract::{name}"),
|
|
26
|
+
"--nocapture",
|
|
27
|
+
"--test-threads=1",
|
|
28
|
+
])
|
|
29
|
+
.env(CHILD, name)
|
|
30
|
+
.output()
|
|
31
|
+
.unwrap();
|
|
32
|
+
assert!(
|
|
33
|
+
output.status.success(),
|
|
34
|
+
"isolated test failed: {:?}\n{}\n{}",
|
|
35
|
+
output.status,
|
|
36
|
+
String::from_utf8_lossy(&output.stdout),
|
|
37
|
+
String::from_utf8_lossy(&output.stderr)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
fn native_bin(h: &HermeticTestEnv) -> PathBuf {
|
|
41
|
+
let bin = h.workspace("bin");
|
|
42
|
+
for name in ["claude", "codex", "grok", "agent", "copilot", "tmux"] {
|
|
43
|
+
let path = bin.join(name);
|
|
44
|
+
let source = if name == "tmux" {
|
|
45
|
+
"#!/bin/sh\nif [ \"$1\" = -V ]; then echo 'tmux 3.3'; exit 0; fi\nexit 1\n"
|
|
46
|
+
} else {
|
|
47
|
+
"#!/bin/sh\nexit 77\n"
|
|
48
|
+
};
|
|
49
|
+
fs::write(&path, source).unwrap();
|
|
50
|
+
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
|
|
51
|
+
}
|
|
52
|
+
let real = h.root().join("pi-real");
|
|
53
|
+
fs::write(&real, "#!/bin/sh\ncase \"$1\" in\n--version) echo '1.0.0';;\n--list-models) printf 'provider model\\nteam-agent test-model\\n';;\n*) exit 77;;\nesac\n").unwrap();
|
|
54
|
+
fs::set_permissions(&real, fs::Permissions::from_mode(0o755)).unwrap();
|
|
55
|
+
symlink(real, bin.join("pi")).unwrap();
|
|
56
|
+
bin
|
|
57
|
+
}
|
|
58
|
+
fn plan(
|
|
59
|
+
provider: Provider,
|
|
60
|
+
workspace: &Path,
|
|
61
|
+
args: &[String],
|
|
62
|
+
attach: bool,
|
|
63
|
+
) -> Result<crate::leader::LeaderStartPlan, LeaderError> {
|
|
64
|
+
crate::leader::start::leader_start_plan_after_ambient_authority_check(
|
|
65
|
+
provider, args, workspace, attach, attach, None, true,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
fn prompt_text(argv: &[String], flag: &str) -> String {
|
|
69
|
+
let index = argv.iter().position(|arg| arg == flag).unwrap();
|
|
70
|
+
argv[index + 1].clone()
|
|
71
|
+
}
|
|
72
|
+
fn stable_argv(plan: &crate::leader::LeaderStartPlan) -> (Vec<String>, Vec<String>) {
|
|
73
|
+
// Fresh Pi session UUIDs are intentionally regenerated; all other bytes must match.
|
|
74
|
+
let normalize = |argv: &[String]| {
|
|
75
|
+
argv.iter()
|
|
76
|
+
.map(|arg| {
|
|
77
|
+
if let Some(index) = plan
|
|
78
|
+
.provider_argv
|
|
79
|
+
.iter()
|
|
80
|
+
.position(|token| token == "--session-id")
|
|
81
|
+
{
|
|
82
|
+
arg.replace(&plan.provider_argv[index + 1], "<fresh-session-id>")
|
|
83
|
+
} else {
|
|
84
|
+
arg.clone()
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
.collect()
|
|
88
|
+
};
|
|
89
|
+
(normalize(&plan.argv), normalize(&plan.provider_argv))
|
|
90
|
+
}
|
|
91
|
+
fn assert_no_payload_tree(path: &Path) {
|
|
92
|
+
for entry in fs::read_dir(path).unwrap() {
|
|
93
|
+
let path = entry.unwrap().path();
|
|
94
|
+
if path.is_dir() {
|
|
95
|
+
assert_no_payload_tree(&path);
|
|
96
|
+
} else if let Ok(bytes) = fs::read(&path) {
|
|
97
|
+
assert!(
|
|
98
|
+
!String::from_utf8_lossy(&bytes).contains(SENTINEL),
|
|
99
|
+
"payload persisted outside global file: {}",
|
|
100
|
+
path.display()
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
#[test]
|
|
106
|
+
fn leader_prompt_plans_preserve_absent_baseline_append_only_leaders_and_never_materialize_into_workers(
|
|
107
|
+
) {
|
|
108
|
+
isolated("leader_prompt_plans_preserve_absent_baseline_append_only_leaders_and_never_materialize_into_workers", || {
|
|
109
|
+
let h = HermeticTestEnv::enter("leader-prompt-plans");
|
|
110
|
+
let bin = native_bin(&h);
|
|
111
|
+
let _path = h.with_env("PATH", bin.to_str().unwrap());
|
|
112
|
+
let _routing = h.with_env("TEAM_AGENT_CLI_ARGV_ROUTING", "off");
|
|
113
|
+
let config = crate::leader::prompt::config_path().unwrap();
|
|
114
|
+
let providers = [Provider::Pi, Provider::Claude, Provider::ClaudeCode, Provider::Codex,
|
|
115
|
+
Provider::Grok, Provider::CursorAgent, Provider::Copilot];
|
|
116
|
+
for provider in providers {
|
|
117
|
+
let workspace = h.workspace(&format!("provider-{}", crate::provider::wire::provider_wire(provider)));
|
|
118
|
+
let baseline = plan(provider, &workspace, &[], false).unwrap();
|
|
119
|
+
assert!(baseline.leader_prompt.is_none());
|
|
120
|
+
fs::create_dir_all(config.parent().unwrap()).unwrap(); fs::write(&config, []).unwrap();
|
|
121
|
+
let empty = plan(provider, &workspace, &[], false).unwrap();
|
|
122
|
+
assert_eq!(stable_argv(&empty), stable_argv(&baseline), "empty configuration changed argv for {provider:?}");
|
|
123
|
+
assert_eq!(empty.leader_env, baseline.leader_env);
|
|
124
|
+
fs::write(&config, SENTINEL).unwrap();
|
|
125
|
+
if matches!(provider, Provider::CursorAgent | Provider::Copilot) {
|
|
126
|
+
let LeaderError::Prompt(error) = plan(provider, &workspace, &[], false).unwrap_err() else { panic!("typed unsupported") };
|
|
127
|
+
assert_eq!(error.reason, "leader_prompt_unsupported");
|
|
128
|
+
} else {
|
|
129
|
+
let configured = plan(provider, &workspace, &[], false).unwrap();
|
|
130
|
+
assert_eq!(configured.mode, baseline.mode);
|
|
131
|
+
assert_eq!(configured.leader_env, baseline.leader_env);
|
|
132
|
+
assert!(configured.leader_prompt.is_some());
|
|
133
|
+
assert!(configured.provider_argv.iter().any(|arg| arg.contains(SENTINEL)));
|
|
134
|
+
if provider == Provider::Pi {
|
|
135
|
+
assert_eq!(prompt_text(&configured.provider_argv, "--append-system-prompt"),
|
|
136
|
+
format!("{}\n\n{SENTINEL}", prompt_text(&baseline.provider_argv, "--append-system-prompt")));
|
|
137
|
+
}
|
|
138
|
+
// Only the process argv carries the global payload, never the wrapper/state/env.
|
|
139
|
+
assert_no_payload_tree(&workspace);
|
|
140
|
+
}
|
|
141
|
+
fs::remove_file(&config).unwrap();
|
|
142
|
+
}
|
|
143
|
+
fs::write(&config, SENTINEL).unwrap();
|
|
144
|
+
let workspace = h.workspace("worker");
|
|
145
|
+
let candidate = h.root().join("candidate"); fs::write(&candidate, "candidate").unwrap();
|
|
146
|
+
let mcp = McpConfig { raw: json!({"team_orchestrator": {
|
|
147
|
+
"command": candidate, "args": ["mcp-server"], "cwd": workspace,
|
|
148
|
+
"env": {"TEAM_AGENT_ID": "worker", "TEAM_AGENT_OWNER_TEAM_ID": "current"}
|
|
149
|
+
}}) };
|
|
150
|
+
for body in [SENTINEL.as_bytes(), &[0xff][..]] {
|
|
151
|
+
fs::write(&config, body).unwrap();
|
|
152
|
+
let worker = materialize_pi_plan(PiMaterializeRequest {
|
|
153
|
+
workspace: &workspace, team_id: "current", agent_id: "worker", model: None,
|
|
154
|
+
effort: None, system_prompt: "Compiled Worker contract; unchanged", team_mcp_tools: &["report_result"],
|
|
155
|
+
mcp_config: &mcp, session_scope: PiSessionScope::Isolated,
|
|
156
|
+
}).unwrap();
|
|
157
|
+
assert!(!worker.argv.iter().any(|arg| arg.contains(SENTINEL)));
|
|
158
|
+
assert!(!mcp.raw.to_string().contains(SENTINEL));
|
|
159
|
+
assert_no_payload_tree(&workspace);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
#[test]
|
|
164
|
+
fn leader_prompt_attach_skips_corrupt_configuration_and_all_injection_for_every_leader_entry() {
|
|
165
|
+
isolated(
|
|
166
|
+
"leader_prompt_attach_skips_corrupt_configuration_and_all_injection_for_every_leader_entry",
|
|
167
|
+
|| {
|
|
168
|
+
let h = HermeticTestEnv::enter("leader-prompt-attach");
|
|
169
|
+
let bin = native_bin(&h);
|
|
170
|
+
let _path = h.with_env("PATH", bin.to_str().unwrap());
|
|
171
|
+
let _routing = h.with_env("TEAM_AGENT_CLI_ARGV_ROUTING", "off");
|
|
172
|
+
let config = crate::leader::prompt::config_path().unwrap();
|
|
173
|
+
fs::create_dir_all(config.parent().unwrap()).unwrap();
|
|
174
|
+
for provider in [
|
|
175
|
+
Provider::Pi,
|
|
176
|
+
Provider::Claude,
|
|
177
|
+
Provider::ClaudeCode,
|
|
178
|
+
Provider::Codex,
|
|
179
|
+
Provider::Grok,
|
|
180
|
+
Provider::CursorAgent,
|
|
181
|
+
Provider::Copilot,
|
|
182
|
+
] {
|
|
183
|
+
let workspace = h.workspace(&format!("attach-{provider:?}"));
|
|
184
|
+
fs::write(&config, []).unwrap();
|
|
185
|
+
let baseline = plan(provider, &workspace, &[], true).unwrap();
|
|
186
|
+
for body in [SENTINEL.as_bytes(), &[0xff][..]] {
|
|
187
|
+
fs::write(&config, body).unwrap();
|
|
188
|
+
let attached = plan(provider, &workspace, &[], true).unwrap();
|
|
189
|
+
assert_eq!(attached.mode, LeaderStartMode::AttachExisting);
|
|
190
|
+
assert!(attached.leader_prompt.is_none());
|
|
191
|
+
assert_eq!(attached.argv, baseline.argv);
|
|
192
|
+
assert_eq!(attached.provider_argv, baseline.provider_argv);
|
|
193
|
+
assert_eq!(attached.leader_env, baseline.leader_env);
|
|
194
|
+
assert!(!workspace.join(".team").exists());
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
#[test]
|
|
201
|
+
fn leader_prompt_routes_are_merged_or_rejected_before_pi_artifacts() {
|
|
202
|
+
isolated(
|
|
203
|
+
"leader_prompt_routes_are_merged_or_rejected_before_pi_artifacts",
|
|
204
|
+
|| {
|
|
205
|
+
let h = HermeticTestEnv::enter("leader-prompt-routes");
|
|
206
|
+
let bin = native_bin(&h);
|
|
207
|
+
let _path = h.with_env("PATH", bin.to_str().unwrap());
|
|
208
|
+
let _routing = h.with_env("TEAM_AGENT_CLI_ARGV_ROUTING", "on");
|
|
209
|
+
let config = crate::leader::prompt::config_path().unwrap();
|
|
210
|
+
fs::create_dir_all(config.parent().unwrap()).unwrap();
|
|
211
|
+
fs::write(&config, SENTINEL).unwrap();
|
|
212
|
+
let route = crate::provider::argv_route::config_path().unwrap();
|
|
213
|
+
fs::write(
|
|
214
|
+
&route,
|
|
215
|
+
json!({"schema_version":1,"enabled":true,"providers":{
|
|
216
|
+
"claude":["--append-system-prompt=from route"],
|
|
217
|
+
"codex":["-c", "developer_instructions=\"from route\""],
|
|
218
|
+
"grok":["--rules", "from route"]
|
|
219
|
+
}})
|
|
220
|
+
.to_string(),
|
|
221
|
+
)
|
|
222
|
+
.unwrap();
|
|
223
|
+
for provider in [Provider::Claude, Provider::Codex, Provider::Grok] {
|
|
224
|
+
let workspace = h.workspace(&format!("route-{provider:?}"));
|
|
225
|
+
let argv = plan(provider, &workspace, &[], false)
|
|
226
|
+
.unwrap()
|
|
227
|
+
.provider_argv;
|
|
228
|
+
assert!(argv
|
|
229
|
+
.iter()
|
|
230
|
+
.any(|arg| arg.contains("from route") && arg.contains(SENTINEL)));
|
|
231
|
+
let args = match provider {
|
|
232
|
+
Provider::Codex => vec![
|
|
233
|
+
"--config".into(),
|
|
234
|
+
"developer_instructions=\"explicit\"".into(),
|
|
235
|
+
],
|
|
236
|
+
Provider::Grok => vec!["--rules".into(), "explicit".into()],
|
|
237
|
+
_ => vec!["--append-system-prompt".into(), "explicit".into()],
|
|
238
|
+
};
|
|
239
|
+
let LeaderError::Prompt(error) =
|
|
240
|
+
plan(provider, &workspace, &args, false).unwrap_err()
|
|
241
|
+
else {
|
|
242
|
+
panic!("typed duplicate refusal")
|
|
243
|
+
};
|
|
244
|
+
assert_eq!(error.reason, "leader_prompt_conflict");
|
|
245
|
+
assert!(!workspace.join(".team").exists());
|
|
246
|
+
}
|
|
247
|
+
for tokens in [
|
|
248
|
+
json!(["--append-system-prompt", "route override"]),
|
|
249
|
+
json!(["--"]),
|
|
250
|
+
] {
|
|
251
|
+
fs::write(
|
|
252
|
+
&route,
|
|
253
|
+
json!({"schema_version":1,"enabled":true,"providers":{"pi":tokens}})
|
|
254
|
+
.to_string(),
|
|
255
|
+
)
|
|
256
|
+
.unwrap();
|
|
257
|
+
let workspace = h.workspace("pi-route-refusal");
|
|
258
|
+
let LeaderError::Prompt(error) =
|
|
259
|
+
plan(Provider::Pi, &workspace, &[], false).unwrap_err()
|
|
260
|
+
else {
|
|
261
|
+
panic!("typed Pi route refusal")
|
|
262
|
+
};
|
|
263
|
+
assert_eq!(error.reason, "leader_prompt_conflict");
|
|
264
|
+
assert!(!workspace.join(".team").exists());
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
}
|
|
@@ -343,6 +343,10 @@ pub struct LeaderStartPlan {
|
|
|
343
343
|
pub argv: Vec<String>,
|
|
344
344
|
/// For managed launches, the provider command argv spawned inside the leader pane.
|
|
345
345
|
pub provider_argv: Vec<String>,
|
|
346
|
+
/// Payload-free launch snapshot; prompt-bearing diagnostics must withhold argv/output.
|
|
347
|
+
pub leader_prompt: Option<super::prompt::PromptMetadata>,
|
|
348
|
+
/// Native identity allocated before launch, not the Team Agent owner UUID.
|
|
349
|
+
pub session_capture: Option<crate::provider::CaptureSessionContext>,
|
|
346
350
|
/// For managed launches, the window that hosts the leader provider pane.
|
|
347
351
|
pub leader_window: Option<WindowName>,
|
|
348
352
|
/// True for external/current-pane leader compatibility paths.
|
|
@@ -456,6 +460,8 @@ pub enum LeaderError {
|
|
|
456
460
|
/// provider 命令未安装 / leader 启动前置失败(`leader_start_plan` raise RuntimeError)。
|
|
457
461
|
#[error("leader start error: {0}")]
|
|
458
462
|
Start(String),
|
|
463
|
+
#[error(transparent)]
|
|
464
|
+
Prompt(#[from] super::prompt::PromptError),
|
|
459
465
|
/// tmux target scan / set-environment / send 失败(transport 层冒泡)。
|
|
460
466
|
#[error("tmux error: {0}")]
|
|
461
467
|
Tmux(String),
|
|
@@ -53,6 +53,10 @@ pub mod state;
|
|
|
53
53
|
// step 6 (compiler) — TEAM.md + agents/*.md → 规范 team.spec dict(doc→spec 纯变换)。
|
|
54
54
|
pub mod compiler;
|
|
55
55
|
|
|
56
|
+
// Isolated new-provider runtime; legacy provider dispatch/state are not migrated.
|
|
57
|
+
#[cfg(unix)]
|
|
58
|
+
pub mod contract_runtime;
|
|
59
|
+
|
|
56
60
|
// Official communication-mode catalog. Configuration compilation and every
|
|
57
61
|
// future projection consumer must use this typed single source of truth.
|
|
58
62
|
pub mod communication_mode;
|