@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
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: 各 provider 的 MCP 配置落盘与 argv 指向,含 grok 目录作用域配置的清洗与前置检查
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: resolve_mcp_config
|
|
6
|
+
//! what: 把 MCP 配置里的 workspace、agent_id 与 team_id 占位符替换掉
|
|
7
|
+
//! - name: write_worker_mcp_config_for_provider
|
|
8
|
+
//! what: 写出 per-agent 的 MCP 配置文件,copilot 走字段名翻译
|
|
9
|
+
//! - name: apply_grok_mcp_overlay
|
|
10
|
+
//! what: 写 workspace 下 grok 的项目作用域配置,是该文件的唯一写者
|
|
11
|
+
//! - name: reconcile_grok_toml_per_seat_keys
|
|
12
|
+
//! what: 起 grok 席前清掉共享 toml 里遗留的 per-seat 键并留审计事件
|
|
13
|
+
//! - name: ensure_grok_login_and_folder_trust
|
|
14
|
+
//! what: grok 未登录或目录未信任时拒绝起席
|
|
15
|
+
//! - name: point_native_mcp_config_at_file
|
|
16
|
+
//! what: 把 argv 里的 MCP 配置参数改指到已落盘的文件
|
|
17
|
+
//! depends:
|
|
18
|
+
//! - crate::provider::McpConfig
|
|
19
|
+
//! - crate::model::permissions
|
|
20
|
+
//! - crate::event_log::EventLog
|
|
21
|
+
//! - crate::lifecycle::profile_launch
|
|
22
|
+
//! - std::fs
|
|
23
|
+
//! boundary:
|
|
24
|
+
//! - 不读也不写 provider 的凭据文件,只判断登录态文件是否存在且非空
|
|
25
|
+
//! - 审计事件只落键名,不落键值
|
|
26
|
+
//! - 清洗共享 toml 时不删未知键,未知不等于可删
|
|
27
|
+
//! - 清洗后校验失败一律拒绝起席,不带着脏配置继续
|
|
28
|
+
//! maturity: wired
|
|
29
|
+
//! ---
|
|
1
30
|
use std::collections::{BTreeMap, BTreeSet};
|
|
2
31
|
use std::path::{Path, PathBuf};
|
|
3
32
|
use std::process::Command;
|
|
@@ -14,6 +43,10 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
|
|
|
14
43
|
|
|
15
44
|
use super::*;
|
|
16
45
|
|
|
46
|
+
/// ---
|
|
47
|
+
/// purpose: 把 MCP 配置里的占位符替换成本次 workspace、席位与团队
|
|
48
|
+
/// returns: 替换后的配置
|
|
49
|
+
/// ---
|
|
17
50
|
pub(crate) fn resolve_mcp_config(
|
|
18
51
|
config: crate::provider::McpConfig,
|
|
19
52
|
workspace: &Path,
|
|
@@ -25,6 +58,10 @@ pub(crate) fn resolve_mcp_config(
|
|
|
25
58
|
}
|
|
26
59
|
}
|
|
27
60
|
|
|
61
|
+
/// ---
|
|
62
|
+
/// purpose: 递归替换 JSON 里字符串中的三个占位符
|
|
63
|
+
/// returns: 同结构的新值,非字符串标量原样返回
|
|
64
|
+
/// ---
|
|
28
65
|
pub(super) fn resolve_mcp_placeholders(
|
|
29
66
|
value: serde_json::Value,
|
|
30
67
|
workspace: &Path,
|
|
@@ -57,6 +94,12 @@ pub(super) fn resolve_mcp_placeholders(
|
|
|
57
94
|
}
|
|
58
95
|
}
|
|
59
96
|
|
|
97
|
+
/// ---
|
|
98
|
+
/// purpose: 写出 per-agent 的 MCP 配置文件,不做 provider 特化
|
|
99
|
+
/// returns: 写出的文件路径
|
|
100
|
+
/// errors: 建目录、序列化或写文件失败时返回 StatePersist
|
|
101
|
+
/// contract_id: lifecycle.mcp_config.write_worker_config
|
|
102
|
+
/// ---
|
|
60
103
|
pub(crate) fn write_worker_mcp_config(
|
|
61
104
|
workspace: &Path,
|
|
62
105
|
agent_id: &str,
|
|
@@ -65,6 +108,14 @@ pub(crate) fn write_worker_mcp_config(
|
|
|
65
108
|
write_worker_mcp_config_for_provider(workspace, agent_id, config, None)
|
|
66
109
|
}
|
|
67
110
|
|
|
111
|
+
/// ---
|
|
112
|
+
/// purpose: 写出 per-agent 的 MCP 配置文件,copilot 先把 type 字段翻成 transport
|
|
113
|
+
/// params:
|
|
114
|
+
/// provider: 为 copilot 时做字段名翻译,其余原样写
|
|
115
|
+
/// returns: 写出的文件路径
|
|
116
|
+
/// errors: 建目录、序列化或写文件失败时返回 StatePersist
|
|
117
|
+
/// contract_id: lifecycle.mcp_config.write_worker_config
|
|
118
|
+
/// ---
|
|
68
119
|
/// C-3-4 cr verdict v2 — Copilot 的 mcp config schema 字段名是 `transport`
|
|
69
120
|
/// (实测 cmd-mcp-add 原文取值 stdio|http|sse),不是 canonical 的 `type`。当
|
|
70
121
|
/// provider==Copilot 时写出文件前先做 type→transport 翻译;其它 provider 不动。
|
|
@@ -95,6 +146,474 @@ pub(crate) fn write_worker_mcp_config_for_provider(
|
|
|
95
146
|
Ok(path)
|
|
96
147
|
}
|
|
97
148
|
|
|
149
|
+
/// Grok MCP 是**目录作用域**(`mcp_injection: dir_scoped`):CLI 只读
|
|
150
|
+
/// `<cwd>/.grok/config.toml`。同目录后起席位会覆盖这份文件,先起席位的
|
|
151
|
+
/// 身份被回溯性改写且不报错。本版 worker cwd 恒为 workspace(D5),
|
|
152
|
+
/// exclusive 检查也不读 per-agent cwd ⇒ 一个 workspace 只支持一个 grok 席。
|
|
153
|
+
/// 这是 grok 的 provider 能力边界,不是框架通则(claude/codex 走 argv
|
|
154
|
+
/// `--mcp-config`,同目录多席没有这个问题)。
|
|
155
|
+
struct GrokOccupant {
|
|
156
|
+
id: String,
|
|
157
|
+
spawned_at: Option<String>,
|
|
158
|
+
status: String,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fn agent_is_grok(agent: &Value) -> bool {
|
|
162
|
+
agent
|
|
163
|
+
.get("provider")
|
|
164
|
+
.and_then(Value::as_str)
|
|
165
|
+
.and_then(crate::lifecycle::profile_launch::parse_provider)
|
|
166
|
+
== Some(Provider::Grok)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn status_is_live(status: &str) -> bool {
|
|
170
|
+
!matches!(
|
|
171
|
+
status,
|
|
172
|
+
"stopped" | "stopping" | "removed" | "spawn_failed" | "failed"
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn occupant_from_state_row(id: &str, agent: &serde_json::Value) -> Option<GrokOccupant> {
|
|
177
|
+
let provider = agent
|
|
178
|
+
.get("provider")
|
|
179
|
+
.and_then(serde_json::Value::as_str)
|
|
180
|
+
.and_then(crate::lifecycle::profile_launch::parse_provider);
|
|
181
|
+
if provider != Some(Provider::Grok) {
|
|
182
|
+
return None;
|
|
183
|
+
}
|
|
184
|
+
let status = agent
|
|
185
|
+
.get("status")
|
|
186
|
+
.and_then(serde_json::Value::as_str)
|
|
187
|
+
.unwrap_or("running");
|
|
188
|
+
if !status_is_live(status) {
|
|
189
|
+
return None;
|
|
190
|
+
}
|
|
191
|
+
Some(GrokOccupant {
|
|
192
|
+
id: id.to_string(),
|
|
193
|
+
spawned_at: agent
|
|
194
|
+
.get("spawned_at")
|
|
195
|
+
.and_then(serde_json::Value::as_str)
|
|
196
|
+
.map(str::to_string),
|
|
197
|
+
status: status.to_string(),
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fn live_grok_occupants_from_state(workspace: &Path) -> Result<Vec<GrokOccupant>, LifecycleError> {
|
|
202
|
+
let path = crate::state::persist::runtime_state_path(workspace);
|
|
203
|
+
if !path.exists() {
|
|
204
|
+
return Ok(Vec::new());
|
|
205
|
+
}
|
|
206
|
+
let state = crate::state::persist::load_runtime_state(workspace).map_err(|e| {
|
|
207
|
+
LifecycleError::StatePersist(format!(
|
|
208
|
+
"cannot count live grok seats (state unreadable): {e}"
|
|
209
|
+
))
|
|
210
|
+
})?;
|
|
211
|
+
let mut out = Vec::new();
|
|
212
|
+
if let Some(agents) = state.get("agents").and_then(serde_json::Value::as_object) {
|
|
213
|
+
for (id, agent) in agents {
|
|
214
|
+
if let Some(row) = occupant_from_state_row(id, agent) {
|
|
215
|
+
out.push(row);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if let Some(teams) = state.get("teams").and_then(serde_json::Value::as_object) {
|
|
220
|
+
for team in teams.values() {
|
|
221
|
+
let Some(agents) = team.get("agents").and_then(serde_json::Value::as_object) else {
|
|
222
|
+
continue;
|
|
223
|
+
};
|
|
224
|
+
for (id, agent) in agents {
|
|
225
|
+
if out.iter().any(|o| o.id == *id) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if let Some(row) = occupant_from_state_row(id, agent) {
|
|
229
|
+
out.push(row);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
Ok(out)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
fn spec_grok_ids(spec: &Value) -> Vec<String> {
|
|
238
|
+
let mut ids = Vec::new();
|
|
239
|
+
for agent in spec_agent_values(spec) {
|
|
240
|
+
if agent_is_paused(agent) || !agent_is_grok(agent) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if let Some(id) = agent.get("id").and_then(Value::as_str) {
|
|
244
|
+
if !ids.iter().any(|existing| existing == id) {
|
|
245
|
+
ids.push(id.to_string());
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
ids
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// ---
|
|
253
|
+
/// purpose: 清掉 grok 共享 toml 里遗留的 per-seat 键,并写一条只含键名的审计事件
|
|
254
|
+
/// returns: 文件不存在或本就没有 per-seat 键时直接成功
|
|
255
|
+
/// errors: 文件读不出、清洗后仍残留或审计事件写不出时返回 RequirementUnmet
|
|
256
|
+
/// ---
|
|
257
|
+
/// Reconcile `<cwd>/.grok/config.toml` before any grok start: drop leftover
|
|
258
|
+
/// framework per-seat keys (`is_per_seat_env_key`), leave user/shared keys,
|
|
259
|
+
/// emit an audit event with names only. Hung only from
|
|
260
|
+
/// [`apply_grok_mcp_overlay`] — the unique writer, so launch/restart/resume
|
|
261
|
+
/// all pass through. Clean-failure keeps the previous refuse shape.
|
|
262
|
+
pub(crate) fn reconcile_grok_toml_per_seat_keys(workspace: &Path) -> Result<(), LifecycleError> {
|
|
263
|
+
let path = workspace.join(".grok").join("config.toml");
|
|
264
|
+
if !path.exists() {
|
|
265
|
+
return Ok(());
|
|
266
|
+
}
|
|
267
|
+
let text = std::fs::read_to_string(&path).map_err(|error| {
|
|
268
|
+
LifecycleError::RequirementUnmet(format!(
|
|
269
|
+
"error: cannot judge grok shared slot (unreadable {})\n\
|
|
270
|
+
reason: {error}\n\
|
|
271
|
+
action: fix permissions on that file before adding another grok seat",
|
|
272
|
+
path.display()
|
|
273
|
+
))
|
|
274
|
+
})?;
|
|
275
|
+
let keys = super::per_seat_keys_in_toml(&text);
|
|
276
|
+
if keys.is_empty() {
|
|
277
|
+
return Ok(());
|
|
278
|
+
}
|
|
279
|
+
let (cleaned, removed) = super::strip_per_seat_keys_from_toml(&text);
|
|
280
|
+
let dir = workspace.join(".grok");
|
|
281
|
+
if let Err(_error) = write_grok_config_toml(&dir, &cleaned) {
|
|
282
|
+
return Err(refuse_dirty_grok_toml(workspace, &keys));
|
|
283
|
+
}
|
|
284
|
+
let after =
|
|
285
|
+
std::fs::read_to_string(&path).map_err(|_| refuse_dirty_grok_toml(workspace, &keys))?;
|
|
286
|
+
if !super::per_seat_keys_in_toml(&after).is_empty() {
|
|
287
|
+
return Err(refuse_dirty_grok_toml(workspace, &keys));
|
|
288
|
+
}
|
|
289
|
+
crate::event_log::EventLog::new(workspace)
|
|
290
|
+
.write(
|
|
291
|
+
crate::lifecycle::types::event_names::GROK_TOML_PER_SEAT_KEYS_CLEARED,
|
|
292
|
+
serde_json::json!({
|
|
293
|
+
"path": path.display().to_string(),
|
|
294
|
+
"keys": removed,
|
|
295
|
+
}),
|
|
296
|
+
)
|
|
297
|
+
.map_err(|error| {
|
|
298
|
+
LifecycleError::RequirementUnmet(format!(
|
|
299
|
+
"error: cannot audit grok shared-slot cleanup ({})\n\
|
|
300
|
+
reason: {error}\n\
|
|
301
|
+
action: fix permissions on .team/logs then retry",
|
|
302
|
+
path.display()
|
|
303
|
+
))
|
|
304
|
+
})?;
|
|
305
|
+
Ok(())
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
fn refuse_dirty_grok_toml(workspace: &Path, keys: &[(String, String)]) -> LifecycleError {
|
|
309
|
+
let named = keys
|
|
310
|
+
.iter()
|
|
311
|
+
.map(|(key, _value)| key.as_str())
|
|
312
|
+
.collect::<Vec<_>>()
|
|
313
|
+
.join(", ");
|
|
314
|
+
LifecycleError::RequirementUnmet(format!(
|
|
315
|
+
"error: grok shared slot still carries per-seat keys ({named})\n\
|
|
316
|
+
reason: .grok/config.toml is directory-scoped; per-seat keys would be inherited by every grok seat\n\
|
|
317
|
+
workspace: {}\n\
|
|
318
|
+
action: remove per-seat keys from the toml (identity belongs on pane env)",
|
|
319
|
+
workspace.display()
|
|
320
|
+
))
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
fn write_grok_config_toml(dir: &Path, body: &str) -> Result<PathBuf, std::io::Error> {
|
|
324
|
+
std::fs::create_dir_all(dir)?;
|
|
325
|
+
let path = dir.join("config.toml");
|
|
326
|
+
let tmp = dir.join("config.toml.tmp");
|
|
327
|
+
std::fs::write(&tmp, body.as_bytes())?;
|
|
328
|
+
std::fs::rename(&tmp, &path)?;
|
|
329
|
+
Ok(path)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/// ---
|
|
333
|
+
/// purpose: 构造同一 workspace 已被别的 grok 席占用的拒绝错误
|
|
334
|
+
/// params:
|
|
335
|
+
/// seats: 已占用该目录的席位名,写进错误正文
|
|
336
|
+
/// returns: 带 error/reason/workspace/grok_seats 的 RequirementUnmet
|
|
337
|
+
/// ---
|
|
338
|
+
pub(crate) fn grok_shared_cwd_error(cwd: &Path, seats: &[String]) -> LifecycleError {
|
|
339
|
+
LifecycleError::RequirementUnmet(format!(
|
|
340
|
+
"error: grok seat already occupies this workspace\n\
|
|
341
|
+
reason: grok MCP is directory-scoped (<cwd>/.grok/config.toml); a second seat overwrites TEAM_AGENT_ID\n\
|
|
342
|
+
workspace: {}\n\
|
|
343
|
+
grok_seats: {}",
|
|
344
|
+
cwd.display(),
|
|
345
|
+
seats.join(", "),
|
|
346
|
+
))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
fn grok_occupied_cwd_error(
|
|
350
|
+
cwd: &Path,
|
|
351
|
+
incoming: &str,
|
|
352
|
+
occupants: &[GrokOccupant],
|
|
353
|
+
) -> LifecycleError {
|
|
354
|
+
let holder = occupants
|
|
355
|
+
.iter()
|
|
356
|
+
.find(|o| o.id != incoming)
|
|
357
|
+
.or_else(|| occupants.first());
|
|
358
|
+
let holder_id = holder.map(|o| o.id.as_str()).unwrap_or("unknown");
|
|
359
|
+
let started = holder
|
|
360
|
+
.and_then(|o| o.spawned_at.as_deref())
|
|
361
|
+
.unwrap_or("unknown");
|
|
362
|
+
let names = occupants
|
|
363
|
+
.iter()
|
|
364
|
+
.map(|o| {
|
|
365
|
+
if let Some(at) = &o.spawned_at {
|
|
366
|
+
format!("{} (status={}, started {})", o.id, o.status, at)
|
|
367
|
+
} else {
|
|
368
|
+
format!("{} (status={})", o.id, o.status)
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
.collect::<Vec<_>>()
|
|
372
|
+
.join(", ");
|
|
373
|
+
LifecycleError::RequirementUnmet(format!(
|
|
374
|
+
"error: grok seat {holder_id} already occupies this workspace (started {started})\n\
|
|
375
|
+
incoming: {incoming}\n\
|
|
376
|
+
reason: grok MCP is directory-scoped (<cwd>/.grok/config.toml); a second seat overwrites TEAM_AGENT_ID and the first seat's first turn would inherit the wrong identity\n\
|
|
377
|
+
workspace: {}\n\
|
|
378
|
+
grok_seats: {names}",
|
|
379
|
+
cwd.display(),
|
|
380
|
+
))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/// ---
|
|
384
|
+
/// purpose: 起 grok 席前确认已登录且该目录已被信任
|
|
385
|
+
/// params:
|
|
386
|
+
/// cwd: 席位的工作目录,用于判断目录信任
|
|
387
|
+
/// returns: 通过返回空值;关闭 folder-trust 的环境变量下跳过信任检查
|
|
388
|
+
/// errors: HOME 未设、登录态文件缺失或为空、目录未被信任时返回 RequirementUnmet
|
|
389
|
+
/// ---
|
|
390
|
+
/// 未登录 / 目录未信任时不许起出「能收信、没有手」的 grok 席。
|
|
391
|
+
/// 登录态看 `$HOME/.grok/auth.json`;目录信任看 `$HOME/.grok/trusted_folders.toml`
|
|
392
|
+
/// (与 grok `--trust` / `/hooks-trust` 同一份;未信任则项目作用域 MCP 不生效)。
|
|
393
|
+
/// `GROK_FOLDER_TRUST=0` 时 grok 自己关掉 folder-trust,本检查跟着放行。
|
|
394
|
+
pub(crate) fn ensure_grok_login_and_folder_trust(cwd: &Path) -> Result<(), LifecycleError> {
|
|
395
|
+
let home = std::env::var_os("HOME").map(PathBuf::from).ok_or_else(|| {
|
|
396
|
+
LifecycleError::RequirementUnmet(
|
|
397
|
+
"error: HOME is unset; cannot verify grok login or folder trust\n\
|
|
398
|
+
action: export HOME and retry"
|
|
399
|
+
.to_string(),
|
|
400
|
+
)
|
|
401
|
+
})?;
|
|
402
|
+
let grok_home = home.join(".grok");
|
|
403
|
+
if !grok_auth_present(&grok_home) {
|
|
404
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
405
|
+
"error: grok is not logged in (missing or empty {})\n\
|
|
406
|
+
action: run `grok login` then retry add-agent/launch",
|
|
407
|
+
grok_home.join("auth.json").display()
|
|
408
|
+
)));
|
|
409
|
+
}
|
|
410
|
+
if folder_trust_required() && !grok_folder_is_trusted(&grok_home, cwd) {
|
|
411
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
412
|
+
"error: grok folder is not trusted; project-scope MCP at {}/.grok/config.toml will not load\n\
|
|
413
|
+
cwd: {}\n\
|
|
414
|
+
action: from that directory run `grok --trust` (or `/hooks-trust` in a grok session), then retry",
|
|
415
|
+
cwd.display(),
|
|
416
|
+
cwd.display()
|
|
417
|
+
)));
|
|
418
|
+
}
|
|
419
|
+
Ok(())
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
fn grok_auth_present(grok_home: &Path) -> bool {
|
|
423
|
+
let path = grok_home.join("auth.json");
|
|
424
|
+
let Ok(text) = std::fs::read_to_string(path) else {
|
|
425
|
+
return false;
|
|
426
|
+
};
|
|
427
|
+
let Ok(value) = serde_json::from_str::<serde_json::Value>(&text) else {
|
|
428
|
+
return false;
|
|
429
|
+
};
|
|
430
|
+
value.as_object().is_some_and(|map| !map.is_empty())
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
fn folder_trust_required() -> bool {
|
|
434
|
+
!matches!(
|
|
435
|
+
std::env::var("GROK_FOLDER_TRUST").ok().as_deref(),
|
|
436
|
+
Some("0") | Some("false") | Some("off")
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
fn grok_folder_is_trusted(grok_home: &Path, cwd: &Path) -> bool {
|
|
441
|
+
let text = std::fs::read_to_string(grok_home.join("trusted_folders.toml")).unwrap_or_default();
|
|
442
|
+
let cwd = std::fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
|
|
443
|
+
grok_trusted_folders(&text).into_iter().any(|trusted| {
|
|
444
|
+
let trusted = std::fs::canonicalize(&trusted).unwrap_or(trusted);
|
|
445
|
+
cwd == trusted || cwd.starts_with(&trusted)
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
fn grok_trusted_folders(text: &str) -> Vec<PathBuf> {
|
|
450
|
+
let mut current = None;
|
|
451
|
+
let mut out = Vec::new();
|
|
452
|
+
for line in text.lines() {
|
|
453
|
+
let trimmed = line.trim();
|
|
454
|
+
if let Some(rest) = trimmed.strip_prefix("[folders.\"") {
|
|
455
|
+
current = rest.strip_suffix("\"]").map(str::to_string);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if trimmed.starts_with('[') {
|
|
459
|
+
current = None;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if trimmed == "trusted = true" {
|
|
463
|
+
if let Some(path) = current.take() {
|
|
464
|
+
out.push(PathBuf::from(path));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
out
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/// ---
|
|
472
|
+
/// purpose: 写 workspace 下 grok 的项目作用域 MCP 配置,服务名固定为 team_orchestrator
|
|
473
|
+
/// params:
|
|
474
|
+
/// mcp_config: 已解析的 MCP 配置,须含 team_orchestrator 条目与 command
|
|
475
|
+
/// returns: 成功返回空值;写前先清 per-seat 键,已有表里的未知环境键会被保留
|
|
476
|
+
/// errors: 缺条目或缺 command 时返回 StatePersist,清洗失败透传 RequirementUnmet,写文件失败返回 StatePersist
|
|
477
|
+
/// ---
|
|
478
|
+
/// Grok CLI 没有 `--mcp-config`。同 `apply_cursor_agent_rules_overlay`:launch
|
|
479
|
+
/// 路径写一份 provider 实际会读的文件。Grok 只认项目作用域
|
|
480
|
+
/// `<cwd>/.grok/config.toml`(`grok mcp add --scope project` 的产物)。
|
|
481
|
+
///
|
|
482
|
+
/// Unique writer of `<cwd>/.grok/config.toml`. Reconcile leftover per-seat
|
|
483
|
+
/// keys here so restart/resume cannot skip the upgrade migration.
|
|
484
|
+
///
|
|
485
|
+
/// `McpConfig.raw` 与写出的 grok 表名都必须是 `team_orchestrator`,与
|
|
486
|
+
/// `worker_command_context` 契约(grok: `team_orchestrator__send_message`)对齐。
|
|
487
|
+
/// grok 按 server 名给工具加命名空间,写成 `team-agent` 会变成 `team-agent__*`。
|
|
488
|
+
pub fn apply_grok_mcp_overlay(
|
|
489
|
+
workspace: &Path,
|
|
490
|
+
mcp_config: &crate::provider::McpConfig,
|
|
491
|
+
) -> Result<(), LifecycleError> {
|
|
492
|
+
// adapter.rs 只产出 team_orchestrator。team-agent 是 0.5.67 overlay
|
|
493
|
+
// 误用的旧 inbound key,读侧留一版以免夹具还带旧名;0.5.68 若无引用再删。
|
|
494
|
+
let server = mcp_config
|
|
495
|
+
.raw
|
|
496
|
+
.get("team_orchestrator")
|
|
497
|
+
.or_else(|| mcp_config.raw.get("team-agent"))
|
|
498
|
+
.ok_or_else(|| {
|
|
499
|
+
LifecycleError::StatePersist(
|
|
500
|
+
"grok MCP overlay requires team_orchestrator in resolved mcp config".to_string(),
|
|
501
|
+
)
|
|
502
|
+
})?;
|
|
503
|
+
let command = server
|
|
504
|
+
.get("command")
|
|
505
|
+
.and_then(serde_json::Value::as_str)
|
|
506
|
+
.filter(|value| !value.is_empty())
|
|
507
|
+
.ok_or_else(|| {
|
|
508
|
+
LifecycleError::StatePersist("grok MCP overlay missing command".to_string())
|
|
509
|
+
})?;
|
|
510
|
+
let args = server
|
|
511
|
+
.get("args")
|
|
512
|
+
.and_then(serde_json::Value::as_array)
|
|
513
|
+
.map(|items| {
|
|
514
|
+
items
|
|
515
|
+
.iter()
|
|
516
|
+
.filter_map(serde_json::Value::as_str)
|
|
517
|
+
.map(str::to_string)
|
|
518
|
+
.collect::<Vec<_>>()
|
|
519
|
+
})
|
|
520
|
+
.unwrap_or_default();
|
|
521
|
+
let env = server
|
|
522
|
+
.get("env")
|
|
523
|
+
.and_then(serde_json::Value::as_object)
|
|
524
|
+
.map(|map| {
|
|
525
|
+
map.iter()
|
|
526
|
+
.filter_map(|(key, value)| {
|
|
527
|
+
value.as_str().map(|text| (key.clone(), text.to_string()))
|
|
528
|
+
})
|
|
529
|
+
.collect::<BTreeMap<_, _>>()
|
|
530
|
+
})
|
|
531
|
+
.unwrap_or_default();
|
|
532
|
+
|
|
533
|
+
// Per-seat keys live on the pane env. Unknown keys on the existing
|
|
534
|
+
// table stay: a full stanza rewrite must not treat "not in our list"
|
|
535
|
+
// as permission to delete.
|
|
536
|
+
reconcile_grok_toml_per_seat_keys(workspace)?;
|
|
537
|
+
let dir = workspace.join(".grok");
|
|
538
|
+
let path = dir.join("config.toml");
|
|
539
|
+
let existing = std::fs::read_to_string(&path).unwrap_or_default();
|
|
540
|
+
let preserved = super::non_per_seat_env_in_tables(
|
|
541
|
+
&existing,
|
|
542
|
+
&["mcp_servers.team_orchestrator", "mcp_servers.team-agent"],
|
|
543
|
+
);
|
|
544
|
+
let mut env = env;
|
|
545
|
+
env.retain(|key, value| !super::is_per_seat_env_key(key) && !value.trim().is_empty());
|
|
546
|
+
for (key, value) in preserved {
|
|
547
|
+
env.entry(key).or_insert(value);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
let stanza = render_grok_team_agent_stanza(command, &args, &env);
|
|
551
|
+
// 同时摘掉新表和 0.5.67 误写的 [mcp_servers.team-agent],否则改名后两套
|
|
552
|
+
// team MCP 并存,grok 会列出两份指向同一进程的工具。
|
|
553
|
+
let body = upsert_toml_table_prefixes(
|
|
554
|
+
&existing,
|
|
555
|
+
&["mcp_servers.team_orchestrator", "mcp_servers.team-agent"],
|
|
556
|
+
&stanza,
|
|
557
|
+
);
|
|
558
|
+
write_grok_config_toml(&dir, &body)
|
|
559
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", path.display())))?;
|
|
560
|
+
Ok(())
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
fn render_grok_team_agent_stanza(
|
|
564
|
+
command: &str,
|
|
565
|
+
args: &[String],
|
|
566
|
+
env: &BTreeMap<String, String>,
|
|
567
|
+
) -> String {
|
|
568
|
+
let mut out = String::from("[mcp_servers.team_orchestrator]\n");
|
|
569
|
+
out.push_str(&format!("command = {}\n", toml_quote(command)));
|
|
570
|
+
out.push_str("args = [\n");
|
|
571
|
+
for arg in args {
|
|
572
|
+
out.push_str(&format!(" {},\n", toml_quote(arg)));
|
|
573
|
+
}
|
|
574
|
+
out.push_str("]\n");
|
|
575
|
+
out.push_str("enabled = true\n");
|
|
576
|
+
if !env.is_empty() {
|
|
577
|
+
out.push_str("\n[mcp_servers.team_orchestrator.env]\n");
|
|
578
|
+
for (key, value) in env {
|
|
579
|
+
out.push_str(&format!("{key} = {}\n", toml_quote(value)));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
out
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
fn toml_quote(value: &str) -> String {
|
|
586
|
+
format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
fn upsert_toml_table_prefixes(existing: &str, tables: &[&str], stanza: &str) -> String {
|
|
590
|
+
let mut out = String::new();
|
|
591
|
+
let mut skip = false;
|
|
592
|
+
for line in existing.lines() {
|
|
593
|
+
let trimmed = line.trim();
|
|
594
|
+
if trimmed.starts_with('[') && trimmed.ends_with(']') {
|
|
595
|
+
let name = &trimmed[1..trimmed.len() - 1];
|
|
596
|
+
skip = tables
|
|
597
|
+
.iter()
|
|
598
|
+
.any(|table| name == *table || name.starts_with(&format!("{table}.")));
|
|
599
|
+
}
|
|
600
|
+
if !skip {
|
|
601
|
+
out.push_str(line);
|
|
602
|
+
out.push('\n');
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
let trimmed = out.trim_end();
|
|
606
|
+
if trimmed.is_empty() {
|
|
607
|
+
stanza.to_string()
|
|
608
|
+
} else {
|
|
609
|
+
format!("{trimmed}\n\n{stanza}")
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/// ---
|
|
614
|
+
/// purpose: 把 MCP 配置里每个 server 的 type 字段名换成 transport
|
|
615
|
+
/// returns: 同结构的新值,其余字段全部保留;非对象原样返回
|
|
616
|
+
/// ---
|
|
98
617
|
/// C-3-4 cr verdict v2 — McpConfig.raw 是 `{name: {type, command, args, env}}` 形;
|
|
99
618
|
/// copilot mcp add schema 取 `transport` 替 `type`(stdio|http|sse 同值)。仅
|
|
100
619
|
/// 字段名变换,其余字段全保留。
|
|
@@ -121,6 +640,12 @@ pub(super) fn copilot_translate_mcp_servers(raw: &serde_json::Value) -> serde_js
|
|
|
121
640
|
serde_json::Value::Object(translated)
|
|
122
641
|
}
|
|
123
642
|
|
|
643
|
+
/// ---
|
|
644
|
+
/// purpose: 把 argv 里 provider 原生的 MCP 配置参数改指到已落盘的文件
|
|
645
|
+
/// params:
|
|
646
|
+
/// argv: 就地改写;claude 系改 --mcp-config 的值,copilot 改 --additional-mcp-config 并加 @ 前缀
|
|
647
|
+
/// returns: argv 里没有对应参数时什么都不做
|
|
648
|
+
/// ---
|
|
124
649
|
pub(crate) fn point_native_mcp_config_at_file(
|
|
125
650
|
argv: &mut [String],
|
|
126
651
|
provider: Provider,
|
|
@@ -150,6 +675,11 @@ pub(crate) fn point_native_mcp_config_at_file(
|
|
|
150
675
|
}
|
|
151
676
|
}
|
|
152
677
|
|
|
678
|
+
/// ---
|
|
679
|
+
/// purpose: 解析该席位的权限并序列化成 JSON
|
|
680
|
+
/// returns: 含 agent_id、provider、排序后的工具串、逐工具的执行强度与是否有仅提示项
|
|
681
|
+
/// errors: 权限解析失败时返回 ModelError
|
|
682
|
+
/// ---
|
|
153
683
|
pub(super) fn permissions_json(
|
|
154
684
|
agent: &Value,
|
|
155
685
|
id: &str,
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: 生命周期操作前的 owner 门,判断调用方是否有权动这个团队
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: ensure_owner_allowed
|
|
6
|
+
//! what: 读 runtime state 后过 owner 门
|
|
7
|
+
//! - name: ensure_owner_allowed_for_state
|
|
8
|
+
//! what: 对给定 state 过 owner 门,席位对自己的操作直接放行
|
|
9
|
+
//! - name: state_path
|
|
10
|
+
//! what: 给出该 workspace 的 runtime state 路径
|
|
11
|
+
//! depends:
|
|
12
|
+
//! - crate::state::owner_gate
|
|
13
|
+
//! - crate::state::identity
|
|
14
|
+
//! - crate::state::projection
|
|
15
|
+
//! - crate::state::persist
|
|
16
|
+
//! boundary:
|
|
17
|
+
//! - 只判权限,不改 state
|
|
18
|
+
//! - 本文件的 pane 存活探针恒报存活,存活判定不在这一层
|
|
19
|
+
//! maturity: wired
|
|
20
|
+
//! ---
|
|
1
21
|
use std::collections::{BTreeMap, BTreeSet};
|
|
2
22
|
use std::path::{Path, PathBuf};
|
|
3
23
|
use std::process::Command;
|
|
@@ -14,12 +34,24 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
|
|
|
14
34
|
|
|
15
35
|
use super::*;
|
|
16
36
|
|
|
37
|
+
/// ---
|
|
38
|
+
/// purpose: 读出 runtime state 后过 owner 门
|
|
39
|
+
/// returns: 通过返回空值
|
|
40
|
+
/// errors: 读 state 失败返回 StatePersist,owner 门拒绝返回 OwnerRefused
|
|
41
|
+
/// ---
|
|
17
42
|
pub(crate) fn ensure_owner_allowed(workspace: &Path) -> Result<(), LifecycleError> {
|
|
18
43
|
let state = crate::state::persist::load_runtime_state(workspace)
|
|
19
44
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
20
45
|
ensure_owner_allowed_for_state(&state, None)
|
|
21
46
|
}
|
|
22
47
|
|
|
48
|
+
/// ---
|
|
49
|
+
/// purpose: 对给定 state 过 owner 门
|
|
50
|
+
/// params:
|
|
51
|
+
/// target_role: 操作目标席位;调用方就是该席位且团队相符时直接放行
|
|
52
|
+
/// returns: 通过返回空值
|
|
53
|
+
/// errors: 取调用方身份失败返回 StatePersist,owner 门给出拒绝理由时返回 OwnerRefused
|
|
54
|
+
/// ---
|
|
23
55
|
pub(crate) fn ensure_owner_allowed_for_state(
|
|
24
56
|
state: &serde_json::Value,
|
|
25
57
|
target_role: Option<&AgentId>,
|
|
@@ -50,6 +82,10 @@ pub(crate) fn ensure_owner_allowed_for_state(
|
|
|
50
82
|
Ok(())
|
|
51
83
|
}
|
|
52
84
|
|
|
85
|
+
/// ---
|
|
86
|
+
/// purpose: 判断调用方是否就是目标席位本人
|
|
87
|
+
/// returns: 环境里的席位 id 与团队 id 同时与目标相符时为 true
|
|
88
|
+
/// ---
|
|
53
89
|
pub(super) fn caller_is_target_role_in_team(
|
|
54
90
|
target_team: &str,
|
|
55
91
|
target_role: Option<&AgentId>,
|
|
@@ -61,6 +97,10 @@ pub(super) fn caller_is_target_role_in_team(
|
|
|
61
97
|
&& std::env::var("TEAM_AGENT_TEAM_ID").ok().as_deref() == Some(target_team)
|
|
62
98
|
}
|
|
63
99
|
|
|
100
|
+
/// ---
|
|
101
|
+
/// purpose: 给出该 workspace 的 runtime state 文件路径
|
|
102
|
+
/// returns: runtime state 路径
|
|
103
|
+
/// ---
|
|
64
104
|
pub(crate) fn state_path(workspace: &Path) -> std::path::PathBuf {
|
|
65
105
|
crate::state::persist::runtime_state_path(workspace)
|
|
66
106
|
}
|
|
@@ -1,7 +1,31 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: plan 多 stage 状态机的建立与按 report_result 的被动推进
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: start_plan
|
|
6
|
+
//! what: 读 plan 文件、建 PlanState 并落盘,对已存在的 plan 幂等返回其状态
|
|
7
|
+
//! - name: handle_report_result
|
|
8
|
+
//! what: 扫 orchestrator 目录里 running 的 plan,条件命中则推进或完成
|
|
9
|
+
//! depends:
|
|
10
|
+
//! - crate::lifecycle::helpers
|
|
11
|
+
//! - crate::model::yaml
|
|
12
|
+
//! - crate::model::paths
|
|
13
|
+
//! boundary:
|
|
14
|
+
//! - 只推进状态机与落盘,不向席位投递 prompt
|
|
15
|
+
//! - 条件文法是封闭集,不做自由表达式求值
|
|
16
|
+
//! maturity: wired
|
|
17
|
+
//! ---
|
|
1
18
|
use super::*;
|
|
2
19
|
|
|
3
20
|
// ── lifecycle::orchestrator —— plan 多 stage 状态机(起步与推进)─────────────
|
|
4
21
|
|
|
22
|
+
/// ---
|
|
23
|
+
/// purpose: 读 plan 文件并建立 PlanState 落盘
|
|
24
|
+
/// params:
|
|
25
|
+
/// start: 当前实现未使用
|
|
26
|
+
/// returns: 已有状态文件时返回它的进度;否则新建 Running 状态并返回,当前 stage 为 1
|
|
27
|
+
/// errors: plan 文件不存在、读不出、YAML 非法、id 不合法或没有 stage 时返回 InvalidPlan 或 InvalidPlanId
|
|
28
|
+
/// ---
|
|
5
29
|
/// `start_plan(workspace, plan_path, start)`(`orchestrator/__init__.py:26`)。load_plan
|
|
6
30
|
/// (YAML 校验)→ 持久化 PlanState → 派发首 stage。对已 running/halted/completed 幂等返回。
|
|
7
31
|
pub(crate) fn start_plan(
|
|
@@ -54,6 +78,13 @@ pub(crate) fn start_plan(
|
|
|
54
78
|
})
|
|
55
79
|
}
|
|
56
80
|
|
|
81
|
+
/// ---
|
|
82
|
+
/// purpose: 收到 report_result 后推进匹配的 plan
|
|
83
|
+
/// params:
|
|
84
|
+
/// envelope: report_result 信封,条件按其字段取值比对
|
|
85
|
+
/// returns: 命中则返回推进后的 Running 或 Completed,否则 NoMatch;orchestrator 目录不存在时也返回 NoMatch
|
|
86
|
+
/// errors: 目录项读不出或任一状态文件解析失败时返回 InvalidPlan
|
|
87
|
+
/// ---
|
|
57
88
|
/// `handle_report_result(workspace, envelope)`(`orchestrator/__init__.py:79`)。被 step11
|
|
58
89
|
/// report_result 路径调用以推进 stage;条件匹配 → 推进/完成,否则 `NoMatch`。
|
|
59
90
|
pub(crate) fn handle_report_result(
|