@team-agent/installer 0.5.85 → 0.5.89
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 +2 -2
- package/crates/team-agent/src/cli/emit.rs +56 -12
- package/crates/team-agent/src/cli/mod.rs +0 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +187 -7
- package/crates/team-agent/src/compiler/tests.rs +13 -24
- package/crates/team-agent/src/compiler.rs +5 -57
- package/crates/team-agent/src/leader/start.rs +0 -2
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +0 -6
- package/crates/team-agent/src/lifecycle/launch/identity.rs +39 -22
- package/crates/team-agent/src/lifecycle/launch/layout.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +0 -58
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +0 -3
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +14 -24
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +5 -9
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +0 -1
- package/crates/team-agent/src/lifecycle/launch.rs +0 -9
- package/crates/team-agent/src/lifecycle/restart/common.rs +5 -9
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +42 -46
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +30 -44
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +32 -77
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -13
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +0 -1
- package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +0 -2
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +14 -85
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +0 -5
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +0 -1
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests.rs +0 -1
- package/crates/team-agent/src/lifecycle/types.rs +0 -10
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +4 -248
- package/crates/team-agent/src/model/enums.rs +17 -38
- package/crates/team-agent/src/model/mod.rs +0 -1
- package/crates/team-agent/src/model/spec.rs +31 -91
- package/crates/team-agent/src/provider/adapter.rs +48 -50
- package/crates/team-agent/src/provider/adapters/claude.rs +5 -30
- package/crates/team-agent/src/provider/adapters/codex.rs +5 -25
- package/crates/team-agent/src/provider/adapters/copilot.rs +8 -41
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +4 -8
- package/crates/team-agent/src/provider/adapters/grok.rs +5 -58
- package/crates/team-agent/src/provider/adapters/mod.rs +5 -7
- package/crates/team-agent/src/provider/adapters/pi.rs +1 -56
- package/crates/team-agent/src/provider/bypass_flags.rs +1 -2
- package/crates/team-agent/src/provider/session/capture.rs +4 -4
- package/crates/team-agent/src/provider/tests/adapter.rs +7 -24
- package/crates/team-agent/src/provider/types.rs +1 -1
- package/crates/team-agent/src/provider/wire.rs +1 -1
- package/package.json +4 -4
- package/schemas/team.schema.json +4 -9
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +0 -784
- package/crates/team-agent/src/model/permissions.rs +0 -721
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
//! ---
|
|
2
|
-
//! purpose: 从 spec 里的一个 agent 定义,编译出 worker
|
|
2
|
+
//! purpose: 从 spec 里的一个 agent 定义,编译出 worker 启动所需的系统提示词
|
|
3
3
|
//! contract:
|
|
4
4
|
//! provides:
|
|
5
5
|
//! - name: WorkerCommandAgent
|
|
6
6
|
//! what: 从 YAML 或 JSON 读出的单 agent 命令上下文
|
|
7
7
|
//! - name: compile_worker_system_prompt
|
|
8
8
|
//! what: 按身份、runtime 契约、通信模式、角色正文、输出契约、权限说明拼系统提示词
|
|
9
|
-
//! - name: resolved_tool_strings_for_command
|
|
10
|
-
//! what: 解析出该 agent 的工具串,声明了 bypass 时追加 dangerous_auto_approve 哨兵
|
|
11
9
|
//! depends:
|
|
12
|
-
//! - crate::model::permissions
|
|
13
10
|
//! - crate::communication_mode
|
|
14
|
-
//! - crate::provider::bypass_flags
|
|
15
11
|
//! boundary:
|
|
16
12
|
//! - 只产出字符串,不 spawn 进程、不写盘
|
|
17
13
|
//! - bypass 只认 agent 自身声明,不从 team/runtime/leader argv 继承
|
|
@@ -22,9 +18,7 @@ use std::path::Path;
|
|
|
22
18
|
|
|
23
19
|
use crate::communication_mode::CommunicationMode;
|
|
24
20
|
use crate::lifecycle::types::LifecycleError;
|
|
25
|
-
use crate::model::enums::
|
|
26
|
-
use crate::model::ids::AgentId;
|
|
27
|
-
use crate::model::permissions::{resolve_permissions, AgentPermissionInput};
|
|
21
|
+
use crate::model::enums::Provider;
|
|
28
22
|
|
|
29
23
|
const RUNTIME_CONTRACT_SECTION: &str = r#"# Team Agent Teammate Runtime Contract
|
|
30
24
|
|
|
@@ -57,7 +51,6 @@ pub(crate) struct WorkerCommandAgent {
|
|
|
57
51
|
id: Option<String>,
|
|
58
52
|
provider: Provider,
|
|
59
53
|
role: Option<String>,
|
|
60
|
-
declared_tools: Option<Vec<String>>,
|
|
61
54
|
system_prompt_inline: Option<String>,
|
|
62
55
|
system_prompt_file: Option<String>,
|
|
63
56
|
output_contract_format: Option<String>,
|
|
@@ -94,16 +87,6 @@ impl WorkerCommandAgent {
|
|
|
94
87
|
.get("role")
|
|
95
88
|
.and_then(crate::model::yaml::Value::as_str)
|
|
96
89
|
.map(str::to_string),
|
|
97
|
-
declared_tools: agent
|
|
98
|
-
.get("tools")
|
|
99
|
-
.and_then(crate::model::yaml::Value::as_list)
|
|
100
|
-
.map(|items| {
|
|
101
|
-
items
|
|
102
|
-
.iter()
|
|
103
|
-
.filter_map(crate::model::yaml::Value::as_str)
|
|
104
|
-
.map(str::to_string)
|
|
105
|
-
.collect()
|
|
106
|
-
}),
|
|
107
90
|
system_prompt_inline: system_prompt
|
|
108
91
|
.and_then(|prompt| prompt.get("inline"))
|
|
109
92
|
.and_then(crate::model::yaml::Value::as_str)
|
|
@@ -158,16 +141,6 @@ impl WorkerCommandAgent {
|
|
|
158
141
|
.get("role")
|
|
159
142
|
.and_then(serde_json::Value::as_str)
|
|
160
143
|
.map(str::to_string),
|
|
161
|
-
declared_tools: agent
|
|
162
|
-
.get("tools")
|
|
163
|
-
.and_then(serde_json::Value::as_array)
|
|
164
|
-
.map(|items| {
|
|
165
|
-
items
|
|
166
|
-
.iter()
|
|
167
|
-
.filter_map(serde_json::Value::as_str)
|
|
168
|
-
.map(str::to_string)
|
|
169
|
-
.collect()
|
|
170
|
-
}),
|
|
171
144
|
system_prompt_inline: system_prompt
|
|
172
145
|
.and_then(|prompt| prompt.get("inline"))
|
|
173
146
|
.and_then(serde_json::Value::as_str)
|
|
@@ -233,9 +206,6 @@ pub(crate) fn compile_worker_system_prompt(
|
|
|
233
206
|
if let Some(contract) = output_contract(agent, &report_result) {
|
|
234
207
|
chunks.push(contract);
|
|
235
208
|
}
|
|
236
|
-
if let Some(notes) = permission_notes(agent)? {
|
|
237
|
-
chunks.push(notes);
|
|
238
|
-
}
|
|
239
209
|
Ok(chunks
|
|
240
210
|
.into_iter()
|
|
241
211
|
.filter(|chunk| !chunk.is_empty())
|
|
@@ -254,43 +224,6 @@ Do not invent a second team protocol or assume that a configured lazy MCP server
|
|
|
254
224
|
.to_string()
|
|
255
225
|
}
|
|
256
226
|
|
|
257
|
-
/// ---
|
|
258
|
-
/// purpose: 解析该 agent 最终生效的工具串
|
|
259
|
-
/// params:
|
|
260
|
-
/// provider: 用于查 bypass argv 定义的 provider
|
|
261
|
-
/// returns: 排序后的工具串;agent 声明 bypass 时末尾追加 dangerous_auto_approve
|
|
262
|
-
/// errors: 权限解析失败,或声明了 bypass 但该 provider 没有 bypass argv 定义时返回 RequirementUnmet
|
|
263
|
-
/// ---
|
|
264
|
-
pub(crate) fn resolved_tool_strings_for_command(
|
|
265
|
-
agent: &WorkerCommandAgent,
|
|
266
|
-
provider: Provider,
|
|
267
|
-
) -> Result<Vec<String>, LifecycleError> {
|
|
268
|
-
let mut tools: Vec<String> = resolve_agent_permissions(agent, provider)?
|
|
269
|
-
.sorted_tool_strings()
|
|
270
|
-
.into_iter()
|
|
271
|
-
.map(str::to_string)
|
|
272
|
-
.collect();
|
|
273
|
-
// 0.5.66 bypass 单源:读 agent 自己的 `dangerously_skip_permissions` bool,
|
|
274
|
-
// 不再收 team/runtime/leader argv 派生的 `DangerousApproval`。true → 加
|
|
275
|
-
// `dangerous_auto_approve` 哨兵(adapter 消费点不变);provider 无 bypass
|
|
276
|
-
// argv 定义 → fail-loud,不静默 fallback。
|
|
277
|
-
if agent.dangerously_skip_permissions && provider != Provider::Pi {
|
|
278
|
-
let flag = crate::provider::bypass_flags::provider_bypass_flag(provider).ok_or_else(
|
|
279
|
-
|| {
|
|
280
|
-
LifecycleError::RequirementUnmet(format!(
|
|
281
|
-
"provider {} has no bypass argv flag defined; dangerously_skip_permissions=true cannot be honored",
|
|
282
|
-
provider_display_name(provider)
|
|
283
|
-
))
|
|
284
|
-
},
|
|
285
|
-
)?;
|
|
286
|
-
debug_assert!(flag.starts_with("--"));
|
|
287
|
-
if !tools.iter().any(|tool| tool == "dangerous_auto_approve") {
|
|
288
|
-
tools.push("dangerous_auto_approve".to_string());
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
Ok(tools)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
227
|
/// Provider-facing MCP tool name. Only verified call forms are filled in.
|
|
295
228
|
/// Unverified providers keep the historical dotted spelling.
|
|
296
229
|
fn mcp_tool_name(provider: Provider, server: &str, tool: &str) -> String {
|
|
@@ -308,33 +241,6 @@ fn mcp_tool_name(provider: Provider, server: &str, tool: &str) -> String {
|
|
|
308
241
|
}
|
|
309
242
|
}
|
|
310
243
|
|
|
311
|
-
fn provider_display_name(provider: Provider) -> &'static str {
|
|
312
|
-
match provider {
|
|
313
|
-
Provider::Claude => "claude",
|
|
314
|
-
Provider::ClaudeCode => "claude_code",
|
|
315
|
-
Provider::Codex => "codex",
|
|
316
|
-
Provider::Copilot => "copilot",
|
|
317
|
-
Provider::GeminiCli => "gemini_cli",
|
|
318
|
-
Provider::Grok => "grok",
|
|
319
|
-
Provider::CursorAgent => "cursor_agent",
|
|
320
|
-
Provider::Pi => "pi",
|
|
321
|
-
Provider::Fake => "fake",
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
fn resolve_agent_permissions(
|
|
326
|
-
agent: &WorkerCommandAgent,
|
|
327
|
-
provider: Provider,
|
|
328
|
-
) -> Result<crate::model::permissions::ResolvedPermissions, LifecycleError> {
|
|
329
|
-
resolve_permissions(&AgentPermissionInput {
|
|
330
|
-
id: agent.id.as_deref().map(AgentId::new),
|
|
331
|
-
provider,
|
|
332
|
-
role: agent.role.clone(),
|
|
333
|
-
tools: agent.declared_tools.clone(),
|
|
334
|
-
})
|
|
335
|
-
.map_err(|e| LifecycleError::Compile(e.to_string()))
|
|
336
|
-
}
|
|
337
|
-
|
|
338
244
|
fn runtime_contract_section(send_message: &str, report_result: &str) -> String {
|
|
339
245
|
RUNTIME_CONTRACT_SECTION
|
|
340
246
|
.replace("{send_message}", send_message)
|
|
@@ -405,161 +311,14 @@ fn output_contract(agent: &WorkerCommandAgent, report_result: &str) -> Option<St
|
|
|
405
311
|
.then(|| RESULT_ENVELOPE_OUTPUT_CONTRACT.replace("{report_result}", report_result))
|
|
406
312
|
}
|
|
407
313
|
|
|
408
|
-
fn permission_notes(agent: &WorkerCommandAgent) -> Result<Option<String>, LifecycleError> {
|
|
409
|
-
let permissions = resolve_agent_permissions(agent, agent.provider)?;
|
|
410
|
-
// C-2-1/C-2-2 cr verdict — Copilot 一期 framework 不替决 fs_read/fs_list/git_diff/
|
|
411
|
-
// provider_builtin(provider prompt 控);为诚实(MUST-NOT-13)在 system prompt 内
|
|
412
|
-
// 总是声明这些 provider-level prompt_only 工具,即便角色未显式声明。
|
|
413
|
-
let provider_prompt_only_extras = provider_default_prompt_only_tools(agent.provider);
|
|
414
|
-
let mut prompt_only: std::collections::BTreeSet<String> = permissions
|
|
415
|
-
.resolved_tools
|
|
416
|
-
.iter()
|
|
417
|
-
.filter(|tool| tool.enforcement == Enforcement::PromptOnly)
|
|
418
|
-
.filter_map(|tool| serde_json::to_value(tool.tool).ok())
|
|
419
|
-
.filter_map(|value| value.as_str().map(str::to_string))
|
|
420
|
-
.collect();
|
|
421
|
-
for tool in provider_prompt_only_extras {
|
|
422
|
-
prompt_only.insert((*tool).to_string());
|
|
423
|
-
}
|
|
424
|
-
if prompt_only.is_empty() {
|
|
425
|
-
return Ok(None);
|
|
426
|
-
}
|
|
427
|
-
let prompt_only: Vec<String> = prompt_only.into_iter().collect();
|
|
428
|
-
Ok(Some(format!(
|
|
429
|
-
"Permission note: these tools are prompt-only for this provider and not hard-enforced: {}",
|
|
430
|
-
prompt_only.join(", ")
|
|
431
|
-
)))
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/// C-2-1 cr verdict — provider-level prompt_only tools that the framework cannot
|
|
435
|
-
/// hard-enforce. The system prompt declares them so the worker is honest about
|
|
436
|
-
/// where consent gates actually live (provider prompt vs framework).
|
|
437
|
-
fn provider_default_prompt_only_tools(provider: Provider) -> &'static [&'static str] {
|
|
438
|
-
match provider {
|
|
439
|
-
// C-2-1: fs_read / fs_list / git_diff / provider_builtin 由 provider prompt
|
|
440
|
-
// 控制,framework 不替决(prompt_only 诚实声明)。
|
|
441
|
-
Provider::Copilot => &["fs_list", "fs_read", "git_diff", "provider_builtin"],
|
|
442
|
-
_ => &[],
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
314
|
#[cfg(test)]
|
|
447
315
|
mod tests {
|
|
448
316
|
use super::*;
|
|
449
317
|
|
|
450
|
-
#[test]
|
|
451
|
-
fn empty_tools_use_role_defaults_and_aliases_resolve_before_command() {
|
|
452
|
-
let agent = WorkerCommandAgent {
|
|
453
|
-
id: Some("dev".to_string()),
|
|
454
|
-
provider: Provider::ClaudeCode,
|
|
455
|
-
role: Some("developer".to_string()),
|
|
456
|
-
declared_tools: Some(Vec::new()),
|
|
457
|
-
system_prompt_inline: None,
|
|
458
|
-
system_prompt_file: None,
|
|
459
|
-
output_contract_format: None,
|
|
460
|
-
communication_mode: CommunicationMode::default(),
|
|
461
|
-
dangerously_skip_permissions: false,
|
|
462
|
-
};
|
|
463
|
-
let tools =
|
|
464
|
-
resolved_tool_strings_for_command(&agent, Provider::ClaudeCode).unwrap();
|
|
465
|
-
assert_eq!(
|
|
466
|
-
tools,
|
|
467
|
-
[
|
|
468
|
-
"execute_bash",
|
|
469
|
-
"fs_list",
|
|
470
|
-
"fs_read",
|
|
471
|
-
"fs_write",
|
|
472
|
-
"git_diff",
|
|
473
|
-
"mcp_team",
|
|
474
|
-
"provider_builtin"
|
|
475
|
-
]
|
|
476
|
-
);
|
|
477
|
-
|
|
478
|
-
let agent = WorkerCommandAgent {
|
|
479
|
-
declared_tools: Some(vec!["fs_*".to_string(), "@team-orchestrator".to_string()]),
|
|
480
|
-
..agent
|
|
481
|
-
};
|
|
482
|
-
let tools =
|
|
483
|
-
resolved_tool_strings_for_command(&agent, Provider::ClaudeCode).unwrap();
|
|
484
|
-
assert_eq!(tools, ["fs_list", "fs_read", "fs_write", "mcp_team"]);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// 0.5.66 bypass 单源 §4.1:true → tools 追加 `dangerous_auto_approve` 哨兵。
|
|
488
|
-
#[test]
|
|
489
|
-
fn test_true_field_produces_bypass_flag_argv() {
|
|
490
|
-
let agent = WorkerCommandAgent {
|
|
491
|
-
id: Some("dev".to_string()),
|
|
492
|
-
provider: Provider::ClaudeCode,
|
|
493
|
-
role: Some("developer".to_string()),
|
|
494
|
-
declared_tools: Some(vec!["mcp_team".to_string()]),
|
|
495
|
-
system_prompt_inline: None,
|
|
496
|
-
system_prompt_file: None,
|
|
497
|
-
output_contract_format: None,
|
|
498
|
-
communication_mode: CommunicationMode::default(),
|
|
499
|
-
dangerously_skip_permissions: true,
|
|
500
|
-
};
|
|
501
|
-
let tools = resolved_tool_strings_for_command(&agent, Provider::ClaudeCode).unwrap();
|
|
502
|
-
assert!(
|
|
503
|
-
tools.iter().any(|tool| tool == "dangerous_auto_approve"),
|
|
504
|
-
"true field must append the dangerous_auto_approve sentinel; tools={tools:?}"
|
|
505
|
-
);
|
|
506
|
-
assert!(
|
|
507
|
-
tools.iter().any(|tool| tool == "mcp_team"),
|
|
508
|
-
"normal tools must remain; tools={tools:?}"
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// 0.5.66 bypass 单源 §4.1:false → 不追加哨兵。
|
|
513
|
-
#[test]
|
|
514
|
-
fn test_false_field_produces_no_bypass_flag_argv() {
|
|
515
|
-
let agent = WorkerCommandAgent {
|
|
516
|
-
id: Some("dev".to_string()),
|
|
517
|
-
provider: Provider::ClaudeCode,
|
|
518
|
-
role: Some("developer".to_string()),
|
|
519
|
-
declared_tools: Some(vec!["mcp_team".to_string()]),
|
|
520
|
-
system_prompt_inline: None,
|
|
521
|
-
system_prompt_file: None,
|
|
522
|
-
output_contract_format: None,
|
|
523
|
-
communication_mode: CommunicationMode::default(),
|
|
524
|
-
dangerously_skip_permissions: false,
|
|
525
|
-
};
|
|
526
|
-
let tools = resolved_tool_strings_for_command(&agent, Provider::ClaudeCode).unwrap();
|
|
527
|
-
assert!(
|
|
528
|
-
!tools.iter().any(|tool| tool == "dangerous_auto_approve"),
|
|
529
|
-
"false field must not append the sentinel; tools={tools:?}"
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// 0.5.66 bypass 单源 §4.1:provider 未定义 bypass 参数 + true → fail-loud。
|
|
534
|
-
#[test]
|
|
535
|
-
fn test_provider_without_bypass_flag_definition_errors() {
|
|
536
|
-
let agent = WorkerCommandAgent {
|
|
537
|
-
id: Some("dev".to_string()),
|
|
538
|
-
provider: Provider::GeminiCli,
|
|
539
|
-
role: Some("developer".to_string()),
|
|
540
|
-
declared_tools: Some(vec!["mcp_team".to_string()]),
|
|
541
|
-
system_prompt_inline: None,
|
|
542
|
-
system_prompt_file: None,
|
|
543
|
-
output_contract_format: None,
|
|
544
|
-
communication_mode: CommunicationMode::default(),
|
|
545
|
-
dangerously_skip_permissions: true,
|
|
546
|
-
};
|
|
547
|
-
let err = resolved_tool_strings_for_command(&agent, Provider::GeminiCli).unwrap_err();
|
|
548
|
-
let msg = err.to_string();
|
|
549
|
-
assert!(
|
|
550
|
-
msg.contains("no bypass argv flag defined"),
|
|
551
|
-
"provider without bypass flag + true must fail-loud; got {msg}"
|
|
552
|
-
);
|
|
553
|
-
assert!(
|
|
554
|
-
msg.contains("gemini_cli"),
|
|
555
|
-
"error must name the provider; got {msg}"
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
318
|
#[test]
|
|
560
319
|
fn system_prompt_uses_identity_then_runtime_contract_python_order() {
|
|
561
320
|
// #264 D6: Python truth source (prompt.py:39, live ps confirmed) builds
|
|
562
|
-
// chunks = [identity, TEAMMATE_SYSTEM_PROMPT, role_body, output
|
|
321
|
+
// chunks = [identity, TEAMMATE_SYSTEM_PROMPT, role_body, output].
|
|
563
322
|
// The previous assertion locked the inverted contract-first order with no
|
|
564
323
|
// Python evidence; this is the corrected golden.
|
|
565
324
|
// 0.3.5 union (copilot v2 C-1 / B2 MUST-4 行为层守): identity 必须 FIRST —
|
|
@@ -569,7 +328,6 @@ mod tests {
|
|
|
569
328
|
id: Some("coder".to_string()),
|
|
570
329
|
provider: Provider::Codex,
|
|
571
330
|
role: Some("Runtime Developer".to_string()),
|
|
572
|
-
declared_tools: Some(vec!["mcp_team".to_string()]),
|
|
573
331
|
system_prompt_inline: Some("Implement the assigned slice.".to_string()),
|
|
574
332
|
system_prompt_file: None,
|
|
575
333
|
output_contract_format: Some("result_envelope_v1".to_string()),
|
|
@@ -590,8 +348,7 @@ mod tests {
|
|
|
590
348
|
let output = prompt
|
|
591
349
|
.find("Final completion must call team_orchestrator.report_result exactly once")
|
|
592
350
|
.unwrap();
|
|
593
|
-
|
|
594
|
-
assert!(identity < runtime && runtime < role && role < output && output < permissions);
|
|
351
|
+
assert!(identity < runtime && runtime < role && role < output);
|
|
595
352
|
let slowdown_phrase = format!("500/{}", 500 + 29);
|
|
596
353
|
assert!(prompt.contains(&slowdown_phrase));
|
|
597
354
|
assert!(prompt.contains("Runtime Developer"));
|
|
@@ -605,7 +362,6 @@ mod tests {
|
|
|
605
362
|
id: Some("worker".to_string()),
|
|
606
363
|
provider,
|
|
607
364
|
role: Some("developer".to_string()),
|
|
608
|
-
declared_tools: Some(vec!["mcp_team".to_string()]),
|
|
609
365
|
system_prompt_inline: Some("worker body".to_string()),
|
|
610
366
|
system_prompt_file: None,
|
|
611
367
|
output_contract_format: Some("result_envelope_v1".to_string()),
|
|
@@ -86,9 +86,8 @@ impl ProviderEffort {
|
|
|
86
86
|
matches!(self, Self::Max)
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
///
|
|
90
|
-
///
|
|
91
|
-
/// Copilot/Gemini/CursorAgent/Fake ignore the field (warning at runtime).
|
|
89
|
+
/// Native command support. Claude/Pi accept max; Codex/Grok accept
|
|
90
|
+
/// the other levels. Use resolve_for_provider for admission/ignore policy.
|
|
92
91
|
pub fn is_supported_by(self, provider: Provider) -> bool {
|
|
93
92
|
match provider {
|
|
94
93
|
Provider::Claude | Provider::ClaudeCode | Provider::Pi => true,
|
|
@@ -98,6 +97,21 @@ impl ProviderEffort {
|
|
|
98
97
|
}
|
|
99
98
|
}
|
|
100
99
|
}
|
|
100
|
+
|
|
101
|
+
/// One admission policy for compiler, direct specs and runtime commands.
|
|
102
|
+
/// Ok(None) preserves the legacy warning-and-ignore providers.
|
|
103
|
+
pub fn resolve_for_provider(self, provider: Provider) -> Result<Option<Self>, &'static str> {
|
|
104
|
+
if provider == Provider::CursorAgent {
|
|
105
|
+
return Err("cursor_agent does not support effort; the Cursor CLI has no --effort flag");
|
|
106
|
+
}
|
|
107
|
+
if self.is_supported_by(provider) {
|
|
108
|
+
Ok(Some(self))
|
|
109
|
+
} else if self.is_claude_only() {
|
|
110
|
+
Err("effort 'max' is only supported by claude/claude_code/pi")
|
|
111
|
+
} else {
|
|
112
|
+
Ok(None)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
101
115
|
}
|
|
102
116
|
|
|
103
117
|
/// auth 模式(`AUTH_MODES` `profiles/constants.py:6`)。
|
|
@@ -227,29 +241,6 @@ pub enum PermissionMode {
|
|
|
227
241
|
Trusted,
|
|
228
242
|
}
|
|
229
243
|
|
|
230
|
-
/// 规范化 tool(`CANONICAL_TOOLS` `permissions.py:5-14`)。别名(`fs_*`/`@builtin`/`*`)
|
|
231
|
-
/// 在 `expand_tools` 展开 —— 那是单独的 alias 输入枚举,不在此(step 2 后续)。
|
|
232
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
|
233
|
-
#[serde(rename_all = "snake_case")]
|
|
234
|
-
pub enum Tool {
|
|
235
|
-
FsRead,
|
|
236
|
-
FsWrite,
|
|
237
|
-
FsList,
|
|
238
|
-
ExecuteBash,
|
|
239
|
-
GitDiff,
|
|
240
|
-
Network,
|
|
241
|
-
McpTeam,
|
|
242
|
-
ProviderBuiltin,
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/// provider×tool enforcement(`PROVIDER_ENFORCEMENT` `permissions.py:44-65`)。
|
|
246
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
247
|
-
#[serde(rename_all = "snake_case")]
|
|
248
|
-
pub enum Enforcement {
|
|
249
|
-
Hard,
|
|
250
|
-
PromptOnly,
|
|
251
|
-
}
|
|
252
|
-
|
|
253
244
|
/// tmux pane 存活态(`state.py:336-341`)。**`Unknown` 既不可当 dead 也不可当 live**
|
|
254
245
|
/// (owner-gate `state.py:382` 用 `!= LIVE`)—— 穷尽 match,不 fallthrough(§11)。
|
|
255
246
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
@@ -345,18 +336,6 @@ mod tests {
|
|
|
345
336
|
serde_json::to_string(&DisplayBackend::Adaptive).unwrap(),
|
|
346
337
|
"\"adaptive\"",
|
|
347
338
|
),
|
|
348
|
-
(
|
|
349
|
-
serde_json::to_string(&Tool::ExecuteBash).unwrap(),
|
|
350
|
-
"\"execute_bash\"",
|
|
351
|
-
),
|
|
352
|
-
(
|
|
353
|
-
serde_json::to_string(&Tool::ProviderBuiltin).unwrap(),
|
|
354
|
-
"\"provider_builtin\"",
|
|
355
|
-
),
|
|
356
|
-
(
|
|
357
|
-
serde_json::to_string(&Enforcement::PromptOnly).unwrap(),
|
|
358
|
-
"\"prompt_only\"",
|
|
359
|
-
),
|
|
360
339
|
(
|
|
361
340
|
serde_json::to_string(&PaneLiveness::Unknown).unwrap(),
|
|
362
341
|
"\"unknown\"",
|
|
@@ -6,21 +6,19 @@
|
|
|
6
6
|
//! serde_json `preserve_order` 保证 child 迭代序 == Python 插入序。
|
|
7
7
|
//!
|
|
8
8
|
//! 本文件先落地 `validate_result_envelope`(自包含,step 7/11 的门)。`validate_spec`
|
|
9
|
-
//! 依赖 `
|
|
10
|
-
//! 待那几个叶子模块集成后再加。
|
|
9
|
+
//! 依赖 `find_dependency_cycle`(task_graph)+ yaml Value。
|
|
11
10
|
|
|
12
11
|
use std::path::Path;
|
|
13
12
|
|
|
14
13
|
use serde_json::Value;
|
|
15
14
|
|
|
16
|
-
use crate::model::enums::
|
|
15
|
+
use crate::model::enums::TaskStatus;
|
|
17
16
|
use crate::model::errors::ModelError;
|
|
18
17
|
use crate::model::ids::TaskId;
|
|
19
18
|
use crate::model::task_graph::{find_dependency_cycle, TaskNode};
|
|
20
19
|
use crate::model::yaml::Value as Yaml;
|
|
21
|
-
use crate::model::
|
|
22
|
-
use crate::provider::
|
|
23
|
-
use crate::provider::wire::{is_claude_family, parse_canonical_provider};
|
|
20
|
+
use crate::model::yaml;
|
|
21
|
+
use crate::provider::wire::parse_canonical_provider;
|
|
24
22
|
|
|
25
23
|
/// result_envelope_v1 顶层 required(= allowed)。
|
|
26
24
|
const RESULT_REQUIRED: &[&str] = &[
|
|
@@ -333,12 +331,13 @@ fn basic_schema_errors(spec: &Yaml) -> Vec<String> {
|
|
|
333
331
|
if !matches!(mode, Some("supervisor_worker" | "swarm_limited")) {
|
|
334
332
|
e.push("/team/mode: invalid mode".to_string());
|
|
335
333
|
}
|
|
336
|
-
let leader_keys = &["id", "role", "provider", "model", "
|
|
334
|
+
let leader_keys = &["id", "role", "provider", "model", "context_policy"];
|
|
335
|
+
let leader_allowed = &["id", "role", "provider", "model", "tools", "context_policy"];
|
|
337
336
|
check_keys_y(
|
|
338
337
|
spec.get("leader"),
|
|
339
338
|
"/leader",
|
|
340
339
|
leader_keys,
|
|
341
|
-
|
|
340
|
+
leader_allowed,
|
|
342
341
|
&mut e,
|
|
343
342
|
);
|
|
344
343
|
let cp_keys = &[
|
|
@@ -407,7 +406,6 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
|
|
|
407
406
|
"model",
|
|
408
407
|
"working_directory",
|
|
409
408
|
"system_prompt",
|
|
410
|
-
"tools",
|
|
411
409
|
"dangerously_skip_permissions",
|
|
412
410
|
"preferred_for",
|
|
413
411
|
"avoid_for",
|
|
@@ -465,15 +463,16 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
|
|
|
465
463
|
"{path}/effort: unknown effort '{raw}' (allowed: low|medium|high|xhigh|max)"
|
|
466
464
|
));
|
|
467
465
|
}
|
|
468
|
-
Some(effort)
|
|
466
|
+
Some(effort) => {
|
|
469
467
|
let provider = agent.get("provider").and_then(Yaml::as_str).unwrap_or("");
|
|
470
|
-
if
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
468
|
+
if let Some(provider_enum) = parse_canonical_provider(provider) {
|
|
469
|
+
if let Err(reason) = effort.resolve_for_provider(provider_enum) {
|
|
470
|
+
errors.push(format!(
|
|
471
|
+
"{path}/effort: {reason} (effort: {raw}; provider: {provider})"
|
|
472
|
+
));
|
|
473
|
+
}
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
|
-
Some(_) => {}
|
|
477
476
|
}
|
|
478
477
|
}
|
|
479
478
|
check_keys_y(
|
|
@@ -483,7 +482,6 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
|
|
|
483
482
|
&["inline", "file"],
|
|
484
483
|
errors,
|
|
485
484
|
);
|
|
486
|
-
check_list_y(agent.get("tools"), &format!("{path}/tools"), errors);
|
|
487
485
|
check_list_y(
|
|
488
486
|
agent.get("preferred_for"),
|
|
489
487
|
&format!("{path}/preferred_for"),
|
|
@@ -787,67 +785,6 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
|
|
|
787
785
|
));
|
|
788
786
|
}
|
|
789
787
|
}
|
|
790
|
-
let tools: Vec<&str> = agent
|
|
791
|
-
.get("tools")
|
|
792
|
-
.and_then(Yaml::as_list)
|
|
793
|
-
.unwrap_or(&[])
|
|
794
|
-
.iter()
|
|
795
|
-
.filter_map(Yaml::as_str)
|
|
796
|
-
.collect();
|
|
797
|
-
let expanded_tools = permissions::expand_tool_strings(tools);
|
|
798
|
-
for tool in &expanded_tools {
|
|
799
|
-
if !permissions::is_canonical_tool(tool) {
|
|
800
|
-
e.push(format!(
|
|
801
|
-
"/agents/{idx}/tools: unknown tool {}",
|
|
802
|
-
py_repr_str(tool)
|
|
803
|
-
));
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
if parse_canonical_provider(provider.and_then(Yaml::as_str).unwrap_or(""))
|
|
807
|
-
== Some(Provider::Pi)
|
|
808
|
-
{
|
|
809
|
-
if let Some(category) = first_unsupported_pi_tool_category(
|
|
810
|
-
expanded_tools
|
|
811
|
-
.iter()
|
|
812
|
-
.filter(|tool| permissions::is_canonical_tool(tool))
|
|
813
|
-
.map(String::as_str),
|
|
814
|
-
) {
|
|
815
|
-
e.push(format!(
|
|
816
|
-
"/agents/{idx}/tools: Pi does not support Team Agent tool category {category:?}; remove it from this agent's tools"
|
|
817
|
-
));
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
let leader_tools: Vec<&str> = leader
|
|
823
|
-
.and_then(|l| l.get("tools"))
|
|
824
|
-
.and_then(Yaml::as_list)
|
|
825
|
-
.unwrap_or(&[])
|
|
826
|
-
.iter()
|
|
827
|
-
.filter_map(Yaml::as_str)
|
|
828
|
-
.collect();
|
|
829
|
-
let expanded_leader_tools = permissions::expand_tool_strings(leader_tools);
|
|
830
|
-
for tool in &expanded_leader_tools {
|
|
831
|
-
if !permissions::is_canonical_tool(tool) {
|
|
832
|
-
e.push(format!(
|
|
833
|
-
"/leader/tools: unknown tool {}",
|
|
834
|
-
py_repr_str(tool)
|
|
835
|
-
));
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
if parse_canonical_provider(leader_provider.and_then(Yaml::as_str).unwrap_or(""))
|
|
839
|
-
== Some(Provider::Pi)
|
|
840
|
-
{
|
|
841
|
-
if let Some(category) = first_unsupported_pi_tool_category(
|
|
842
|
-
expanded_leader_tools
|
|
843
|
-
.iter()
|
|
844
|
-
.filter(|tool| permissions::is_canonical_tool(tool))
|
|
845
|
-
.map(String::as_str),
|
|
846
|
-
) {
|
|
847
|
-
e.push(format!(
|
|
848
|
-
"/leader/tools: Pi does not support Team Agent tool category {category:?}; remove it from the leader's tools"
|
|
849
|
-
));
|
|
850
|
-
}
|
|
851
788
|
}
|
|
852
789
|
|
|
853
790
|
let routing = spec.get("routing");
|
|
@@ -1070,7 +1007,7 @@ mod tests {
|
|
|
1070
1007
|
}
|
|
1071
1008
|
|
|
1072
1009
|
#[test]
|
|
1073
|
-
fn
|
|
1010
|
+
fn tools_are_transparent_for_all_providers() {
|
|
1074
1011
|
let pi_text = include_str!("testdata/team.spec.yaml")
|
|
1075
1012
|
.lines()
|
|
1076
1013
|
.filter(|line| {
|
|
@@ -1093,22 +1030,26 @@ mod tests {
|
|
|
1093
1030
|
1,
|
|
1094
1031
|
);
|
|
1095
1032
|
let bad_agent = yaml::loads(&bad_agent_text).unwrap();
|
|
1096
|
-
let error = validate_spec(&bad_agent, Path::new(TD)).unwrap_err().to_string();
|
|
1097
1033
|
assert!(
|
|
1098
|
-
|
|
1099
|
-
"
|
|
1034
|
+
validate_spec(&bad_agent, Path::new(TD)).is_ok(),
|
|
1035
|
+
"Pi tools metadata must be ignored"
|
|
1100
1036
|
);
|
|
1101
1037
|
|
|
1102
|
-
let
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1038
|
+
let missing_tools = pi_text
|
|
1039
|
+
.replacen(
|
|
1040
|
+
" tools:\n - fs_read\n - fs_list\n - mcp_team\n",
|
|
1041
|
+
"",
|
|
1042
|
+
1,
|
|
1043
|
+
)
|
|
1044
|
+
.replacen(
|
|
1045
|
+
" tools:\n - fs_read\n - fs_write\n - fs_list\n - execute_bash\n - mcp_team\n",
|
|
1046
|
+
"",
|
|
1047
|
+
1,
|
|
1048
|
+
);
|
|
1049
|
+
let missing_tools = yaml::loads(&missing_tools).unwrap();
|
|
1109
1050
|
assert!(
|
|
1110
|
-
|
|
1111
|
-
"
|
|
1051
|
+
validate_spec(&missing_tools, Path::new(TD)).is_ok(),
|
|
1052
|
+
"omitted tools metadata must be accepted"
|
|
1112
1053
|
);
|
|
1113
1054
|
}
|
|
1114
1055
|
|
|
@@ -1121,7 +1062,6 @@ mod tests {
|
|
|
1121
1062
|
"/version: must equal 1",
|
|
1122
1063
|
"/team/mode: invalid mode",
|
|
1123
1064
|
"/leader/provider: unknown provider 'badprov'",
|
|
1124
|
-
"/agents/0/tools: unknown tool 'banana'",
|
|
1125
1065
|
]
|
|
1126
1066
|
);
|
|
1127
1067
|
}
|