@team-agent/installer 0.5.66 → 0.5.68
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 +8 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -0
- package/crates/team-agent/src/cli/adapters.rs +5 -0
- package/crates/team-agent/src/cli/diagnose.rs +82 -0
- package/crates/team-agent/src/cli/emit.rs +3 -3
- package/crates/team-agent/src/cli/grok_slot.rs +299 -0
- package/crates/team-agent/src/cli/leader.rs +21 -7
- package/crates/team-agent/src/cli/leaders.rs +125 -1
- package/crates/team-agent/src/cli/mod.rs +20 -0
- package/crates/team-agent/src/cli/send/presentation.rs +7 -1
- package/crates/team-agent/src/cli/spec.rs +2 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
- package/crates/team-agent/src/cli/tests/base.rs +26 -3
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
- package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
- package/crates/team-agent/src/communication_mode/mod.rs +6 -4
- package/crates/team-agent/src/compiler.rs +59 -0
- package/crates/team-agent/src/coordinator/backoff.rs +55 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
- package/crates/team-agent/src/coordinator/health.rs +173 -0
- package/crates/team-agent/src/coordinator/mod.rs +31 -0
- package/crates/team-agent/src/coordinator/orphan.rs +31 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
- package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
- package/crates/team-agent/src/coordinator/tick.rs +133 -22
- package/crates/team-agent/src/coordinator/types.rs +49 -0
- package/crates/team-agent/src/db/message_store.rs +39 -5
- package/crates/team-agent/src/layout/worker_env.rs +20 -3
- package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
- package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
- package/crates/team-agent/src/leader/registry.rs +65 -0
- package/crates/team-agent/src/leader/start.rs +920 -63
- package/crates/team-agent/src/lifecycle/display.rs +56 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -12
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +62 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +59 -0
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
- package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -456
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
- package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +62 -2
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +139 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
- package/crates/team-agent/src/lifecycle/launch.rs +99 -29
- package/crates/team-agent/src/lifecycle/lock.rs +42 -1
- package/crates/team-agent/src/lifecycle/mod.rs +11 -0
- package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +124 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +369 -11
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
- package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
- package/crates/team-agent/src/lifecycle/restart.rs +29 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
- package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
- package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
- package/crates/team-agent/src/lifecycle/tests.rs +23 -13
- package/crates/team-agent/src/lifecycle/types.rs +61 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +85 -11
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
- package/crates/team-agent/src/mcp_server/tests.rs +196 -2
- package/crates/team-agent/src/messaging/delivery.rs +405 -40
- package/crates/team-agent/src/messaging/helpers.rs +2 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
- package/crates/team-agent/src/messaging/results.rs +4 -0
- package/crates/team-agent/src/messaging/send.rs +34 -0
- package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
- package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/types.rs +4 -1
- package/crates/team-agent/src/model/enums.rs +14 -5
- package/crates/team-agent/src/model/permissions.rs +3 -0
- package/crates/team-agent/src/os_probe.rs +73 -2
- package/crates/team-agent/src/provider/adapter.rs +231 -4
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
- package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/bypass_flags.rs +46 -2
- package/crates/team-agent/src/provider/classify.rs +4 -2
- package/crates/team-agent/src/provider/faults.rs +2 -1
- package/crates/team-agent/src/provider/mod.rs +11 -0
- package/crates/team-agent/src/provider/session/capture.rs +158 -20
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
- package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
- package/crates/team-agent/src/provider/session/mod.rs +18 -0
- package/crates/team-agent/src/provider/session/resume.rs +57 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
- package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
- package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
- package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
- package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
- package/crates/team-agent/src/provider/session_scan.rs +8 -0
- package/crates/team-agent/src/provider/submit_now.rs +94 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
- package/crates/team-agent/src/provider/types.rs +4 -2
- package/crates/team-agent/src/provider/wire.rs +23 -1
- package/crates/team-agent/src/state/persist.rs +301 -8
- package/crates/team-agent/src/state/repository.rs +5 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
- package/crates/team-agent/src/tmux_backend.rs +1064 -75
- package/crates/team-agent/src/transport/tests/wire.rs +28 -2
- package/crates/team-agent/src/transport.rs +248 -1
- package/npm/install.mjs +129 -73
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +33 -238
- package/skills/team-agent/command-coverage.json +31 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
- package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: g1 五个静默面到达性回归——clone 后 spawn/MCP 不得再夹工具
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: g1_face1_resolve_permissions
|
|
6
|
+
//! what: clone 后 worker_command_context 不得按 leader 三件套再截 argv
|
|
7
|
+
//! - name: g1_face2_disallowed_tools
|
|
8
|
+
//! what: Claude --disallowedTools 不得把源席已声明工具再禁回去
|
|
9
|
+
//! - name: g1_face3_mcp_tool_table
|
|
10
|
+
//! what: grok/claude/cursor 各自注入面仍按源席全集映射,不得只剩只读
|
|
11
|
+
//! - name: g1_face4_mcp_clone_entry
|
|
12
|
+
//! what: MCP clone_agent 与 CLI clone-agent 同一 tools 断言
|
|
13
|
+
//! - name: g1_face5_narrow_source_stays_narrow
|
|
14
|
+
//! what: 源席故意三件套时分身仍是三件,不得扩成 developer 默认
|
|
15
|
+
//! boundary:
|
|
16
|
+
//! - 不改 clone_agent.rs / add-agent / approval / tools 语义
|
|
17
|
+
//! - 不发明 --keep-tools;不把 cursor 未验证的 --allowed-tools 写进产物
|
|
18
|
+
//! maturity: wired
|
|
19
|
+
//! ---
|
|
20
|
+
|
|
21
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
22
|
+
|
|
23
|
+
use std::collections::BTreeSet;
|
|
24
|
+
use std::path::{Path, PathBuf};
|
|
25
|
+
use std::process::Output;
|
|
26
|
+
|
|
27
|
+
use serial_test::serial;
|
|
28
|
+
|
|
29
|
+
#[path = "../../../tests/support/hermetic.rs"]
|
|
30
|
+
mod hermetic_guard;
|
|
31
|
+
use hermetic_guard::HermeticTestEnv;
|
|
32
|
+
|
|
33
|
+
use crate::lifecycle::worker_command_context::{
|
|
34
|
+
resolved_tool_strings_for_command, WorkerCommandAgent,
|
|
35
|
+
};
|
|
36
|
+
use crate::lifecycle::{apply_cursor_mcp_overlay, apply_grok_mcp_overlay};
|
|
37
|
+
use crate::mcp_server::TeamOrchestratorTools;
|
|
38
|
+
use crate::model::enums::{AuthMode, Provider};
|
|
39
|
+
use crate::model::ids::{AgentId, TeamKey};
|
|
40
|
+
use crate::model::permissions::{resolve_permissions, AgentPermissionInput};
|
|
41
|
+
use crate::model::yaml::Value;
|
|
42
|
+
use crate::provider::adapters::claude::claude_disallowed_tools;
|
|
43
|
+
use crate::provider::adapters::grok::grok_disallowed_tools;
|
|
44
|
+
use crate::provider::{get_adapter, McpConfig, ProviderCommandContext};
|
|
45
|
+
use crate::state::persist::load_runtime_state;
|
|
46
|
+
|
|
47
|
+
const TEAM_NAME: &str = "g1sf";
|
|
48
|
+
const SOURCE: &str = "src_worker";
|
|
49
|
+
const CLONE: &str = "new_worker";
|
|
50
|
+
const NARROW: &str = "narrow_src";
|
|
51
|
+
const NARROW_CLONE: &str = "narrow_clone";
|
|
52
|
+
|
|
53
|
+
const SIX: &[&str] = &[
|
|
54
|
+
"execute_bash",
|
|
55
|
+
"fs_list",
|
|
56
|
+
"fs_read",
|
|
57
|
+
"fs_write",
|
|
58
|
+
"mcp_team",
|
|
59
|
+
"provider_builtin",
|
|
60
|
+
];
|
|
61
|
+
const THREE: &[&str] = &["fs_list", "fs_read", "mcp_team"];
|
|
62
|
+
const WRITE_NATIVE: &[&str] = &["Bash", "Edit", "Write", "MultiEdit", "NotebookEdit"];
|
|
63
|
+
|
|
64
|
+
struct Case {
|
|
65
|
+
env: HermeticTestEnv,
|
|
66
|
+
workspace: PathBuf,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
impl Case {
|
|
70
|
+
fn start() -> Self {
|
|
71
|
+
ensure_team_agent_cli();
|
|
72
|
+
let env = HermeticTestEnv::enter("g1sf");
|
|
73
|
+
let workspace = env.workspace("ws");
|
|
74
|
+
write_team_docs(&workspace);
|
|
75
|
+
Self { env, workspace }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn ws(&self) -> &str {
|
|
79
|
+
self.workspace.to_str().expect("ws utf8")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn run(&self, args: &[&str]) -> Output {
|
|
83
|
+
self.env.run_cli(&self.workspace, args)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn shutdown(&self) {
|
|
87
|
+
drop(self.run(&[
|
|
88
|
+
"shutdown",
|
|
89
|
+
"--workspace",
|
|
90
|
+
self.ws(),
|
|
91
|
+
"--team",
|
|
92
|
+
TEAM_NAME,
|
|
93
|
+
"--json",
|
|
94
|
+
]));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fn ensure_team_agent_cli() {
|
|
99
|
+
if let Ok(path) = std::env::var("CARGO_BIN_EXE_team-agent") {
|
|
100
|
+
assert!(
|
|
101
|
+
std::path::Path::new(&path).is_file(),
|
|
102
|
+
"CARGO_BIN_EXE_team-agent missing: {path}"
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
let bin = std::env::current_exe()
|
|
107
|
+
.expect("test exe")
|
|
108
|
+
.parent()
|
|
109
|
+
.and_then(|deps| deps.parent())
|
|
110
|
+
.map(|target| target.join("team-agent"))
|
|
111
|
+
.expect("target/debug/team-agent");
|
|
112
|
+
if bin.is_file() {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
let manifest = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
|
|
116
|
+
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
|
|
117
|
+
let status = std::process::Command::new(&cargo)
|
|
118
|
+
.args([
|
|
119
|
+
"build",
|
|
120
|
+
"-p",
|
|
121
|
+
"team-agent",
|
|
122
|
+
"--bin",
|
|
123
|
+
"team-agent",
|
|
124
|
+
"--manifest-path",
|
|
125
|
+
])
|
|
126
|
+
.arg(std::path::Path::new(&manifest).join("Cargo.toml"))
|
|
127
|
+
.status()
|
|
128
|
+
.unwrap_or_else(|e| panic!("spawn {cargo} build: {e}"));
|
|
129
|
+
assert!(
|
|
130
|
+
status.success(),
|
|
131
|
+
"{cargo} build -p team-agent --bin team-agent failed: {status}"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fn write_team_docs(workspace: &Path) {
|
|
136
|
+
std::fs::create_dir_all(workspace.join("agents")).expect("agents dir");
|
|
137
|
+
std::fs::write(
|
|
138
|
+
workspace.join("TEAM.md"),
|
|
139
|
+
format!(
|
|
140
|
+
"---\nname: {TEAM_NAME}\nobjective: g1 silent faces\nprovider: fake\ndisplay_backend: none\n---\n"
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
.expect("TEAM.md");
|
|
144
|
+
write_role(workspace, SOURCE, SIX);
|
|
145
|
+
write_role(workspace, NARROW, THREE);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fn write_role(workspace: &Path, name: &str, tools: &[&str]) {
|
|
149
|
+
let tools_yaml = tools
|
|
150
|
+
.iter()
|
|
151
|
+
.map(|tool| format!(" - {tool}"))
|
|
152
|
+
.collect::<Vec<_>>()
|
|
153
|
+
.join("\n");
|
|
154
|
+
std::fs::write(
|
|
155
|
+
workspace.join("agents").join(format!("{name}.md")),
|
|
156
|
+
format!(
|
|
157
|
+
"---\nname: {name}\nrole: {name}\nprovider: fake\nmodel: fake\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n{tools_yaml}\n---\n\n{name} body.\n"
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
.expect("role doc");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn all_runtime_specs(workspace: &Path) -> Vec<(std::path::PathBuf, Value)> {
|
|
164
|
+
let runtime = workspace.join(".team").join("runtime");
|
|
165
|
+
let mut parsed = Vec::new();
|
|
166
|
+
if let Ok(entries) = std::fs::read_dir(&runtime) {
|
|
167
|
+
for entry in entries.flatten() {
|
|
168
|
+
let path = entry.path().join("team.spec.yaml");
|
|
169
|
+
if path.is_file() {
|
|
170
|
+
let value =
|
|
171
|
+
crate::model::yaml::loads(&std::fs::read_to_string(&path).expect("spec"))
|
|
172
|
+
.expect("parse spec");
|
|
173
|
+
parsed.push((path, value));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
parsed
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn load_runtime_spec(workspace: &Path) -> Value {
|
|
181
|
+
let parsed = all_runtime_specs(workspace);
|
|
182
|
+
assert!(
|
|
183
|
+
!parsed.is_empty(),
|
|
184
|
+
"runtime spec missing under {}",
|
|
185
|
+
workspace.join(".team").join("runtime").display()
|
|
186
|
+
);
|
|
187
|
+
parsed
|
|
188
|
+
.into_iter()
|
|
189
|
+
.max_by_key(|(_, value)| spec_agent_ids(value).len())
|
|
190
|
+
.expect("runtime spec")
|
|
191
|
+
.1
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn spec_agent_ids(spec: &Value) -> Vec<String> {
|
|
195
|
+
spec.get("agents")
|
|
196
|
+
.and_then(Value::as_list)
|
|
197
|
+
.unwrap_or(&[])
|
|
198
|
+
.iter()
|
|
199
|
+
.filter_map(|row| row.get("id").and_then(Value::as_str).map(str::to_string))
|
|
200
|
+
.collect()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fn runtime_team_key(workspace: &Path) -> String {
|
|
204
|
+
let runtime = workspace.join(".team").join("runtime");
|
|
205
|
+
if runtime.join(TEAM_NAME).join("team.spec.yaml").is_file() {
|
|
206
|
+
return TEAM_NAME.to_string();
|
|
207
|
+
}
|
|
208
|
+
std::fs::read_dir(&runtime)
|
|
209
|
+
.expect("runtime dir")
|
|
210
|
+
.flatten()
|
|
211
|
+
.find(|entry| entry.path().join("team.spec.yaml").is_file())
|
|
212
|
+
.map(|entry| entry.file_name().to_string_lossy().into_owned())
|
|
213
|
+
.unwrap_or_else(|| panic!("runtime team key missing"))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
fn yaml_tool_set(node: &Value) -> BTreeSet<String> {
|
|
217
|
+
node.get("tools")
|
|
218
|
+
.and_then(Value::as_list)
|
|
219
|
+
.unwrap_or_else(|| panic!("tools list missing"))
|
|
220
|
+
.iter()
|
|
221
|
+
.filter_map(Value::as_str)
|
|
222
|
+
.map(str::to_string)
|
|
223
|
+
.collect()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fn spec_agent<'a>(spec: &'a Value, agent: &str) -> &'a Value {
|
|
227
|
+
let agents = spec
|
|
228
|
+
.get("agents")
|
|
229
|
+
.and_then(Value::as_list)
|
|
230
|
+
.expect("spec agents");
|
|
231
|
+
for row in agents {
|
|
232
|
+
if row.get("id").and_then(Value::as_str) == Some(agent) {
|
|
233
|
+
return row;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
panic!(
|
|
237
|
+
"agent {agent} missing from runtime spec; have {:?}",
|
|
238
|
+
spec_agent_ids(spec)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fn spec_tools(workspace: &Path, agent: &str) -> BTreeSet<String> {
|
|
243
|
+
for (_, spec) in all_runtime_specs(workspace) {
|
|
244
|
+
if spec_agent_ids(&spec).iter().any(|id| id == agent) {
|
|
245
|
+
return yaml_tool_set(spec_agent(&spec, agent));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
let inventory: Vec<(String, Vec<String>)> = all_runtime_specs(workspace)
|
|
249
|
+
.into_iter()
|
|
250
|
+
.map(|(path, spec)| (path.display().to_string(), spec_agent_ids(&spec)))
|
|
251
|
+
.collect();
|
|
252
|
+
panic!("agent {agent} missing from every runtime spec; inventory={inventory:?}");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fn leader_spec_tools(workspace: &Path) -> BTreeSet<String> {
|
|
256
|
+
yaml_tool_set(
|
|
257
|
+
load_runtime_spec(workspace)
|
|
258
|
+
.get("leader")
|
|
259
|
+
.expect("spec leader"),
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn set_of(items: &[&str]) -> BTreeSet<String> {
|
|
264
|
+
items.iter().map(|s| (*s).to_string()).collect()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
fn role_tools(path: &Path) -> BTreeSet<String> {
|
|
268
|
+
let (meta, _) = crate::compiler::read_front_matter(path)
|
|
269
|
+
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
|
|
270
|
+
yaml_tool_set(&meta)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn clone_ok(case: &Case, source: &str, dest: &str) {
|
|
274
|
+
let out = case.run(&[
|
|
275
|
+
"clone-agent",
|
|
276
|
+
source,
|
|
277
|
+
"--as",
|
|
278
|
+
dest,
|
|
279
|
+
"--workspace",
|
|
280
|
+
case.ws(),
|
|
281
|
+
"--team",
|
|
282
|
+
TEAM_NAME,
|
|
283
|
+
"--no-display",
|
|
284
|
+
"--json",
|
|
285
|
+
]);
|
|
286
|
+
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
287
|
+
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
288
|
+
assert!(
|
|
289
|
+
out.status.success(),
|
|
290
|
+
"clone-agent {source} --as {dest} must exit 0; code={:?} stderr={stderr} stdout={stdout}",
|
|
291
|
+
out.status.code()
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
fn quick_start(case: &Case) {
|
|
296
|
+
let qs = case.run(&[
|
|
297
|
+
"quick-start",
|
|
298
|
+
case.ws(),
|
|
299
|
+
"--workspace",
|
|
300
|
+
case.ws(),
|
|
301
|
+
"--name",
|
|
302
|
+
TEAM_NAME,
|
|
303
|
+
"--yes",
|
|
304
|
+
"--json",
|
|
305
|
+
]);
|
|
306
|
+
let spec_ok = case
|
|
307
|
+
.workspace
|
|
308
|
+
.join(".team")
|
|
309
|
+
.join("runtime")
|
|
310
|
+
.read_dir()
|
|
311
|
+
.ok()
|
|
312
|
+
.and_then(|entries| {
|
|
313
|
+
entries
|
|
314
|
+
.flatten()
|
|
315
|
+
.find(|e| e.path().join("team.spec.yaml").is_file())
|
|
316
|
+
});
|
|
317
|
+
assert!(
|
|
318
|
+
spec_ok.is_some(),
|
|
319
|
+
"quick-start must leave a runtime spec; stderr={} stdout={}",
|
|
320
|
+
String::from_utf8_lossy(&qs.stderr),
|
|
321
|
+
String::from_utf8_lossy(&qs.stdout)
|
|
322
|
+
);
|
|
323
|
+
assert_eq!(
|
|
324
|
+
spec_tools(&case.workspace, SOURCE),
|
|
325
|
+
set_of(SIX),
|
|
326
|
+
"fixture source must be the six-set"
|
|
327
|
+
);
|
|
328
|
+
assert_eq!(
|
|
329
|
+
leader_spec_tools(&case.workspace),
|
|
330
|
+
set_of(THREE),
|
|
331
|
+
"leader must stay the three-set so the conflict surface exists"
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
fn boot_six_clone() -> Case {
|
|
336
|
+
let case = Case::start();
|
|
337
|
+
quick_start(&case);
|
|
338
|
+
clone_ok(&case, SOURCE, CLONE);
|
|
339
|
+
case
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
fn command_tools(agent_row: &Value, id: &str, provider: Provider) -> Vec<String> {
|
|
343
|
+
let command_agent = WorkerCommandAgent::from_yaml(agent_row, Some(id), provider)
|
|
344
|
+
.unwrap_or_else(|e| panic!("WorkerCommandAgent::from_yaml {id}: {e}"));
|
|
345
|
+
resolved_tool_strings_for_command(&command_agent, provider)
|
|
346
|
+
.unwrap_or_else(|e| panic!("resolved_tool_strings_for_command {id}: {e}"))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
fn permissions_tools_of(row: &serde_json::Value) -> Option<BTreeSet<String>> {
|
|
350
|
+
row.get("permissions")
|
|
351
|
+
.and_then(|permissions| permissions.get("tools"))
|
|
352
|
+
.and_then(|tools| tools.as_array())
|
|
353
|
+
.map(|tools| {
|
|
354
|
+
tools
|
|
355
|
+
.iter()
|
|
356
|
+
.filter_map(|v| v.as_str().map(str::to_string))
|
|
357
|
+
.collect()
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
fn collect_json_tools(value: &serde_json::Value, agent: &str) -> Option<BTreeSet<String>> {
|
|
362
|
+
if let Some(row) = value.pointer(&format!("/agents/{agent}")) {
|
|
363
|
+
if let Some(tools) = permissions_tools_of(row) {
|
|
364
|
+
return Some(tools);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if let Some(teams) = value.get("teams").and_then(|v| v.as_object()) {
|
|
368
|
+
for team in teams.values() {
|
|
369
|
+
if let Some(found) = collect_json_tools(team, agent) {
|
|
370
|
+
return Some(found);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
None
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
fn flag_values<'a>(argv: &'a [String], flag: &str) -> Vec<&'a str> {
|
|
378
|
+
let mut out = Vec::new();
|
|
379
|
+
let mut i = 0;
|
|
380
|
+
while i < argv.len() {
|
|
381
|
+
if argv[i] == flag {
|
|
382
|
+
if let Some(value) = argv.get(i + 1) {
|
|
383
|
+
out.push(value.as_str());
|
|
384
|
+
i += 2;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
i += 1;
|
|
389
|
+
}
|
|
390
|
+
out
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
fn assert_write_tools_not_disallowed(surface: &str, disallowed: &[&str]) {
|
|
394
|
+
let banned: Vec<&str> = disallowed
|
|
395
|
+
.iter()
|
|
396
|
+
.copied()
|
|
397
|
+
.filter(|name| WRITE_NATIVE.contains(name))
|
|
398
|
+
.collect();
|
|
399
|
+
assert!(
|
|
400
|
+
banned.is_empty(),
|
|
401
|
+
"{surface}: six-set clone must not re-ban write/exec native tools; banned={banned:?} disallowed={disallowed:?}"
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
fn overlay_config(agent_id: &str, workspace: &str) -> McpConfig {
|
|
406
|
+
McpConfig {
|
|
407
|
+
raw: serde_json::json!({
|
|
408
|
+
"team_orchestrator": {
|
|
409
|
+
"command": "/bin/team-agent-test",
|
|
410
|
+
"args": ["mcp-server", "--workspace", workspace],
|
|
411
|
+
"env": {
|
|
412
|
+
"TEAM_AGENT_ID": agent_id,
|
|
413
|
+
"TEAM_AGENT_WORKSPACE": workspace,
|
|
414
|
+
"TEAM_AGENT_OWNER_TEAM_ID": TEAM_NAME,
|
|
415
|
+
"TEAM_AGENT_AUTH_MODE": "subscription",
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}),
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
fn assert_provider_mcp_table(case: &Case, provider: Provider, clone_id: &str) {
|
|
423
|
+
let spec = load_runtime_spec(&case.workspace);
|
|
424
|
+
let row = spec_agent(&spec, clone_id);
|
|
425
|
+
let declared = yaml_tool_set(row);
|
|
426
|
+
assert_eq!(
|
|
427
|
+
declared,
|
|
428
|
+
set_of(SIX),
|
|
429
|
+
"clone spec tools must still be six-set before provider mapping"
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
let resolved = command_tools(row, clone_id, provider);
|
|
433
|
+
let resolved_set: BTreeSet<String> = resolved.iter().cloned().collect();
|
|
434
|
+
assert_eq!(
|
|
435
|
+
resolved_set,
|
|
436
|
+
set_of(SIX),
|
|
437
|
+
"{provider:?} resolve_permissions on clone must equal source six-set, not leader three-set; got={resolved:?}"
|
|
438
|
+
);
|
|
439
|
+
assert_ne!(
|
|
440
|
+
resolved_set,
|
|
441
|
+
set_of(THREE),
|
|
442
|
+
"{provider:?} clone tools collapsed onto the leader ceiling"
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
let tool_refs: Vec<&str> = resolved.iter().map(String::as_str).collect();
|
|
446
|
+
match provider {
|
|
447
|
+
Provider::Claude | Provider::ClaudeCode => {
|
|
448
|
+
let disallowed = claude_disallowed_tools(&tool_refs);
|
|
449
|
+
assert_write_tools_not_disallowed("claude disallowedTools", &disallowed);
|
|
450
|
+
}
|
|
451
|
+
Provider::Grok => {
|
|
452
|
+
let disallowed = grok_disallowed_tools(&tool_refs);
|
|
453
|
+
assert_write_tools_not_disallowed("grok disallowedTools", &disallowed);
|
|
454
|
+
}
|
|
455
|
+
Provider::CursorAgent => {}
|
|
456
|
+
other => panic!("face3 unexpected provider {other:?}"),
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
let adapter = get_adapter(provider);
|
|
460
|
+
let mcp = adapter
|
|
461
|
+
.mcp_config(AuthMode::Subscription)
|
|
462
|
+
.unwrap_or_else(|e| panic!("{provider:?} mcp_config: {e}"));
|
|
463
|
+
let grok_model = (provider == Provider::Grok).then_some("grok-4.6");
|
|
464
|
+
let plan = adapter
|
|
465
|
+
.build_command_plan(ProviderCommandContext {
|
|
466
|
+
auth_mode: AuthMode::Subscription,
|
|
467
|
+
mcp_config: Some(&mcp),
|
|
468
|
+
system_prompt: Some("g1 silent face"),
|
|
469
|
+
model: grok_model,
|
|
470
|
+
tools: &tool_refs,
|
|
471
|
+
profile_launch: None,
|
|
472
|
+
agent_id_hint: Some(clone_id),
|
|
473
|
+
effort: None,
|
|
474
|
+
})
|
|
475
|
+
.unwrap_or_else(|e| panic!("{provider:?} build_command_plan: {e}"));
|
|
476
|
+
match provider {
|
|
477
|
+
Provider::Claude | Provider::ClaudeCode => {
|
|
478
|
+
let disallowed = flag_values(&plan.argv, "--disallowedTools");
|
|
479
|
+
assert_write_tools_not_disallowed("claude argv --disallowedTools", &disallowed);
|
|
480
|
+
}
|
|
481
|
+
Provider::Grok => {
|
|
482
|
+
let disallowed = flag_values(&plan.argv, "--disallowedTools");
|
|
483
|
+
assert_write_tools_not_disallowed("grok argv --disallowedTools", &disallowed);
|
|
484
|
+
}
|
|
485
|
+
Provider::CursorAgent => {
|
|
486
|
+
assert!(
|
|
487
|
+
!plan.argv.iter().any(|arg| arg == "--allowed-tools"),
|
|
488
|
+
"cursor must not invent unverified --allowed-tools (would silently whitelist a subset); argv={:?}",
|
|
489
|
+
plan.argv
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
other => panic!("face3 unexpected provider {other:?}"),
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
let overlay_root = case
|
|
496
|
+
.env
|
|
497
|
+
.root()
|
|
498
|
+
.join(format!("overlay-{}", provider_tag(provider)));
|
|
499
|
+
std::fs::create_dir_all(&overlay_root).expect("overlay dir");
|
|
500
|
+
let overlay_ws = overlay_root.to_string_lossy().into_owned();
|
|
501
|
+
let cfg = overlay_config(clone_id, &overlay_ws);
|
|
502
|
+
match provider {
|
|
503
|
+
Provider::Grok => {
|
|
504
|
+
apply_grok_mcp_overlay(&overlay_root, &cfg).expect("grok overlay");
|
|
505
|
+
let text = std::fs::read_to_string(overlay_root.join(".grok/config.toml"))
|
|
506
|
+
.expect("grok config.toml");
|
|
507
|
+
assert!(
|
|
508
|
+
text.contains("[mcp_servers.team_orchestrator]"),
|
|
509
|
+
"grok MCP injection must keep team_orchestrator, not a readonly stub; text={text}"
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
Provider::Claude | Provider::ClaudeCode => {
|
|
513
|
+
assert!(
|
|
514
|
+
mcp.raw.get("team_orchestrator").is_some(),
|
|
515
|
+
"claude MCP config must keep team_orchestrator server; raw={}",
|
|
516
|
+
mcp.raw
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
Provider::CursorAgent => {
|
|
520
|
+
apply_cursor_mcp_overlay(&overlay_root, &cfg).expect("cursor overlay");
|
|
521
|
+
let text = std::fs::read_to_string(overlay_root.join(".cursor/mcp.json"))
|
|
522
|
+
.expect("cursor mcp.json");
|
|
523
|
+
assert!(
|
|
524
|
+
text.contains("\"team_orchestrator\""),
|
|
525
|
+
"cursor MCP injection must keep team_orchestrator; text={text}"
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
other => panic!("face3 unexpected provider {other:?}"),
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
fn provider_tag(provider: Provider) -> &'static str {
|
|
533
|
+
match provider {
|
|
534
|
+
Provider::Grok => "grok",
|
|
535
|
+
Provider::Claude | Provider::ClaudeCode => "claude",
|
|
536
|
+
Provider::CursorAgent => "cursor",
|
|
537
|
+
_ => "other",
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/// Face 1: spawn argv tools come from resolve_permissions on the clone row, not a leader ceiling.
|
|
542
|
+
#[test]
|
|
543
|
+
#[serial(env)]
|
|
544
|
+
fn g1_face1_resolve_permissions() {
|
|
545
|
+
let case = boot_six_clone();
|
|
546
|
+
let spec = load_runtime_spec(&case.workspace);
|
|
547
|
+
let clone_row = spec_agent(&spec, CLONE);
|
|
548
|
+
let resolved = command_tools(clone_row, CLONE, Provider::Fake);
|
|
549
|
+
let resolved_set: BTreeSet<String> = resolved.iter().cloned().collect();
|
|
550
|
+
assert_eq!(
|
|
551
|
+
resolved_set,
|
|
552
|
+
set_of(SIX),
|
|
553
|
+
"face1: resolved_tool_strings_for_command must keep the six-set; got={resolved:?} leader={:?}",
|
|
554
|
+
leader_spec_tools(&case.workspace)
|
|
555
|
+
);
|
|
556
|
+
assert!(
|
|
557
|
+
resolved_set.contains("execute_bash") && resolved_set.contains("fs_write"),
|
|
558
|
+
"face1: write/exec must survive worker_command_context; got={resolved:?}"
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
let direct = resolve_permissions(&AgentPermissionInput {
|
|
562
|
+
id: Some(AgentId::new(CLONE)),
|
|
563
|
+
provider: Provider::Fake,
|
|
564
|
+
role: clone_row
|
|
565
|
+
.get("role")
|
|
566
|
+
.and_then(Value::as_str)
|
|
567
|
+
.map(str::to_string),
|
|
568
|
+
tools: Some(resolved.clone()),
|
|
569
|
+
})
|
|
570
|
+
.expect("resolve_permissions on clone tools");
|
|
571
|
+
let direct_set: BTreeSet<String> = direct
|
|
572
|
+
.sorted_tool_strings()
|
|
573
|
+
.into_iter()
|
|
574
|
+
.map(str::to_string)
|
|
575
|
+
.collect();
|
|
576
|
+
assert_eq!(
|
|
577
|
+
direct_set,
|
|
578
|
+
set_of(SIX),
|
|
579
|
+
"face1: resolve_permissions itself must not intersect leader ceiling"
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
let state = load_runtime_state(&case.workspace).expect("runtime state after clone");
|
|
583
|
+
if let Some(persisted) = collect_json_tools(&state, CLONE) {
|
|
584
|
+
assert_eq!(
|
|
585
|
+
persisted,
|
|
586
|
+
set_of(SIX),
|
|
587
|
+
"face1: persisted spawn permissions must equal source six-set; got={persisted:?}"
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
case.shutdown();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/// Face 2: Claude --disallowedTools must not re-ban tools the clone still declares.
|
|
594
|
+
#[test]
|
|
595
|
+
#[serial(env)]
|
|
596
|
+
fn g1_face2_disallowed_tools() {
|
|
597
|
+
let case = boot_six_clone();
|
|
598
|
+
let spec = load_runtime_spec(&case.workspace);
|
|
599
|
+
let clone_row = spec_agent(&spec, CLONE);
|
|
600
|
+
let resolved = command_tools(clone_row, CLONE, Provider::Claude);
|
|
601
|
+
let tool_refs: Vec<&str> = resolved.iter().map(String::as_str).collect();
|
|
602
|
+
let disallowed = claude_disallowed_tools(&tool_refs);
|
|
603
|
+
assert_write_tools_not_disallowed("face2 claude_disallowed_tools", &disallowed);
|
|
604
|
+
|
|
605
|
+
let adapter = get_adapter(Provider::Claude);
|
|
606
|
+
let mcp = adapter
|
|
607
|
+
.mcp_config(AuthMode::Subscription)
|
|
608
|
+
.expect("claude mcp");
|
|
609
|
+
let plan = adapter
|
|
610
|
+
.build_command_plan(ProviderCommandContext {
|
|
611
|
+
auth_mode: AuthMode::Subscription,
|
|
612
|
+
mcp_config: Some(&mcp),
|
|
613
|
+
system_prompt: Some("g1 face2"),
|
|
614
|
+
model: None,
|
|
615
|
+
tools: &tool_refs,
|
|
616
|
+
profile_launch: None,
|
|
617
|
+
agent_id_hint: Some(CLONE),
|
|
618
|
+
effort: None,
|
|
619
|
+
})
|
|
620
|
+
.expect("claude plan");
|
|
621
|
+
let argv_disallowed = flag_values(&plan.argv, "--disallowedTools");
|
|
622
|
+
assert_write_tools_not_disallowed("face2 argv", &argv_disallowed);
|
|
623
|
+
assert!(
|
|
624
|
+
plan.argv.iter().any(|arg| arg == "--disallowedTools") || argv_disallowed.is_empty(),
|
|
625
|
+
"face2 must actually walk Claude argv construction; argv={:?}",
|
|
626
|
+
plan.argv
|
|
627
|
+
);
|
|
628
|
+
case.shutdown();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/// Face 3 umbrella: all three provider injection surfaces stay at the six-set.
|
|
632
|
+
#[test]
|
|
633
|
+
#[serial(env)]
|
|
634
|
+
fn g1_face3_mcp_tool_table() {
|
|
635
|
+
let case = boot_six_clone();
|
|
636
|
+
assert_provider_mcp_table(&case, Provider::Grok, CLONE);
|
|
637
|
+
assert_provider_mcp_table(&case, Provider::Claude, CLONE);
|
|
638
|
+
assert_provider_mcp_table(&case, Provider::CursorAgent, CLONE);
|
|
639
|
+
case.shutdown();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
#[test]
|
|
643
|
+
#[serial(env)]
|
|
644
|
+
fn g1_face3_mcp_tool_table_grok() {
|
|
645
|
+
let case = boot_six_clone();
|
|
646
|
+
assert_provider_mcp_table(&case, Provider::Grok, CLONE);
|
|
647
|
+
case.shutdown();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
#[test]
|
|
651
|
+
#[serial(env)]
|
|
652
|
+
fn g1_face3_mcp_tool_table_claude() {
|
|
653
|
+
let case = boot_six_clone();
|
|
654
|
+
assert_provider_mcp_table(&case, Provider::Claude, CLONE);
|
|
655
|
+
case.shutdown();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
#[test]
|
|
659
|
+
#[serial(env)]
|
|
660
|
+
fn g1_face3_mcp_tool_table_cursor() {
|
|
661
|
+
let case = boot_six_clone();
|
|
662
|
+
assert_provider_mcp_table(&case, Provider::CursorAgent, CLONE);
|
|
663
|
+
case.shutdown();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/// Face 4: MCP clone_agent(...) and CLI clone-agent must keep the same tools set.
|
|
667
|
+
#[test]
|
|
668
|
+
#[serial(env)]
|
|
669
|
+
fn g1_face4_mcp_clone_entry() {
|
|
670
|
+
// Two isolated teams: a second clone on the same spec can rewrite the
|
|
671
|
+
// agent list from role docs and drop the first dest. Compare tools across
|
|
672
|
+
// workspaces, not two dests on one spec.
|
|
673
|
+
let cli_case = Case::start();
|
|
674
|
+
quick_start(&cli_case);
|
|
675
|
+
clone_ok(&cli_case, SOURCE, CLONE);
|
|
676
|
+
let cli_tools = spec_tools(&cli_case.workspace, CLONE);
|
|
677
|
+
let cli_spec = load_runtime_spec(&cli_case.workspace);
|
|
678
|
+
let cli_resolved = command_tools(spec_agent(&cli_spec, CLONE), CLONE, Provider::Fake);
|
|
679
|
+
|
|
680
|
+
let body_case = Case::start();
|
|
681
|
+
quick_start(&body_case);
|
|
682
|
+
crate::lifecycle::clone_agent(
|
|
683
|
+
&body_case.workspace,
|
|
684
|
+
&AgentId::new(SOURCE),
|
|
685
|
+
&AgentId::new(CLONE),
|
|
686
|
+
None,
|
|
687
|
+
false,
|
|
688
|
+
Some(TEAM_NAME),
|
|
689
|
+
)
|
|
690
|
+
.unwrap_or_else(|e| panic!("shared clone_agent body must succeed: {e}"));
|
|
691
|
+
let body_tools = spec_tools(&body_case.workspace, CLONE);
|
|
692
|
+
let body_spec = load_runtime_spec(&body_case.workspace);
|
|
693
|
+
let body_resolved = command_tools(spec_agent(&body_spec, CLONE), CLONE, Provider::Fake);
|
|
694
|
+
|
|
695
|
+
assert_eq!(cli_tools, set_of(SIX), "CLI clone tools");
|
|
696
|
+
assert_eq!(body_tools, set_of(SIX), "shared-body clone tools");
|
|
697
|
+
assert_eq!(
|
|
698
|
+
cli_tools, body_tools,
|
|
699
|
+
"face4: CLI and shared clone_agent body must keep one tools assertion"
|
|
700
|
+
);
|
|
701
|
+
assert_eq!(
|
|
702
|
+
cli_resolved, body_resolved,
|
|
703
|
+
"face4: spawn-time resolved tools must match across CLI and shared-body clones"
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
let team_key = runtime_team_key(&body_case.workspace);
|
|
707
|
+
let mcp_probe = TeamOrchestratorTools::with_identity(
|
|
708
|
+
&body_case.workspace,
|
|
709
|
+
Some(AgentId::new(SOURCE)),
|
|
710
|
+
Some(TeamKey::new(&team_key)),
|
|
711
|
+
)
|
|
712
|
+
.clone_agent(SOURCE, "mcp_probe", None);
|
|
713
|
+
match mcp_probe {
|
|
714
|
+
Ok(_) => {
|
|
715
|
+
assert_eq!(
|
|
716
|
+
spec_tools(&body_case.workspace, "mcp_probe"),
|
|
717
|
+
set_of(SIX),
|
|
718
|
+
"mcp_probe tools must equal the source six-set"
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
Err(error) => {
|
|
722
|
+
let text = format!("{error:?}");
|
|
723
|
+
assert!(
|
|
724
|
+
text.contains("not found")
|
|
725
|
+
|| text.contains("unknown worker")
|
|
726
|
+
|| text.contains("team select")
|
|
727
|
+
|| text.contains("TeamSelect"),
|
|
728
|
+
"mcp_probe MCP clone_agent must persist six-set or fail-loud; error={text}"
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
cli_case.shutdown();
|
|
733
|
+
body_case.shutdown();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/// Face 5: a deliberately narrow source must stay narrow after clone (no widen-to-six).
|
|
737
|
+
#[test]
|
|
738
|
+
#[serial(env)]
|
|
739
|
+
fn g1_face5_narrow_source_stays_narrow() {
|
|
740
|
+
let case = Case::start();
|
|
741
|
+
quick_start(&case);
|
|
742
|
+
clone_ok(&case, NARROW, NARROW_CLONE);
|
|
743
|
+
|
|
744
|
+
let spec = load_runtime_spec(&case.workspace);
|
|
745
|
+
let clone_row = spec_agent(&spec, NARROW_CLONE);
|
|
746
|
+
let spec_set = yaml_tool_set(clone_row);
|
|
747
|
+
assert_eq!(
|
|
748
|
+
spec_set,
|
|
749
|
+
set_of(THREE),
|
|
750
|
+
"face5: spec tools must stay the three-set; got={spec_set:?}"
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
let resolved: BTreeSet<String> = command_tools(clone_row, NARROW_CLONE, Provider::Fake)
|
|
754
|
+
.into_iter()
|
|
755
|
+
.collect();
|
|
756
|
+
assert_eq!(
|
|
757
|
+
resolved,
|
|
758
|
+
set_of(THREE),
|
|
759
|
+
"face5: resolve_permissions must not fall back to developer defaults and widen; got={resolved:?}"
|
|
760
|
+
);
|
|
761
|
+
assert!(
|
|
762
|
+
!resolved.contains("execute_bash") && !resolved.contains("fs_write"),
|
|
763
|
+
"face5: narrow clone must not gain write/exec; got={resolved:?}"
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
let role = role_tools(
|
|
767
|
+
&case
|
|
768
|
+
.workspace
|
|
769
|
+
.join(".team")
|
|
770
|
+
.join("dynamic-role-files")
|
|
771
|
+
.join(format!("{NARROW_CLONE}.md")),
|
|
772
|
+
);
|
|
773
|
+
assert_eq!(role, set_of(THREE), "face5: role file must stay narrow");
|
|
774
|
+
|
|
775
|
+
let tool_refs: Vec<&str> = resolved.iter().map(String::as_str).collect();
|
|
776
|
+
let disallowed = claude_disallowed_tools(&tool_refs);
|
|
777
|
+
for required in ["Bash", "Edit", "Write"] {
|
|
778
|
+
assert!(
|
|
779
|
+
disallowed.contains(&required),
|
|
780
|
+
"face5: narrow clone MUST disallow {required}; disallowed={disallowed:?}"
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
case.shutdown();
|
|
784
|
+
}
|