@team-agent/installer 0.3.3 → 0.3.5
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 +8 -0
- package/crates/team-agent/src/cli/diagnose.rs +52 -11
- package/crates/team-agent/src/cli/emit.rs +3 -2
- package/crates/team-agent/src/cli/mod.rs +225 -80
- package/crates/team-agent/src/cli/send.rs +1 -0
- package/crates/team-agent/src/cli/status_port.rs +135 -7
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +8 -1
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +39 -0
- package/crates/team-agent/src/cli/types.rs +5 -1
- package/crates/team-agent/src/compiler/tests.rs +2 -2
- package/crates/team-agent/src/compiler.rs +1 -1
- package/crates/team-agent/src/coordinator/backoff.rs +57 -9
- package/crates/team-agent/src/coordinator/health.rs +65 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +28 -16
- package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +87 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +4 -2
- package/crates/team-agent/src/coordinator/tick.rs +195 -43
- package/crates/team-agent/src/leader/helpers.rs +2 -0
- package/crates/team-agent/src/leader/rediscover.rs +1 -0
- package/crates/team-agent/src/leader/start.rs +9 -1
- package/crates/team-agent/src/leader/takeover.rs +18 -1
- package/crates/team-agent/src/lifecycle/display.rs +3 -3
- package/crates/team-agent/src/lifecycle/launch.rs +772 -285
- package/crates/team-agent/src/lifecycle/mod.rs +1 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +110 -4
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +4 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +16 -5
- package/crates/team-agent/src/lifecycle/restart/common.rs +35 -25
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +31 -25
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/core.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +5 -3
- package/crates/team-agent/src/lifecycle/types.rs +4 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +361 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +2 -1
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +14 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +15 -1
- package/crates/team-agent/src/mcp_server/tools.rs +65 -9
- package/crates/team-agent/src/mcp_server/wire.rs +2 -1
- package/crates/team-agent/src/message_store.rs +80 -0
- package/crates/team-agent/src/messaging/results.rs +76 -5
- package/crates/team-agent/src/messaging/send.rs +3 -1
- package/crates/team-agent/src/messaging/types.rs +15 -1
- package/crates/team-agent/src/messaging/watchers.rs +68 -30
- package/crates/team-agent/src/model/enums.rs +7 -1
- package/crates/team-agent/src/model/permissions.rs +7 -0
- package/crates/team-agent/src/model/spec.rs +3 -1
- package/crates/team-agent/src/provider/adapter.rs +472 -7
- package/crates/team-agent/src/provider/classify.rs +6 -2
- package/crates/team-agent/src/provider/faults.rs +3 -2
- package/crates/team-agent/src/provider/startup_prompt.rs +25 -7
- package/crates/team-agent/src/provider/types.rs +11 -0
- package/crates/team-agent/src/session_capture.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +95 -19
- package/crates/team-agent/src/tmux_backend/tests.rs +8 -7
- package/crates/team-agent/src/tmux_backend.rs +134 -6
- package/crates/team-agent/src/transport.rs +32 -0
- package/package.json +4 -4
|
@@ -72,9 +72,8 @@ pub fn launch_with_transport_in_workspace(
|
|
|
72
72
|
spec_path.display()
|
|
73
73
|
)));
|
|
74
74
|
}
|
|
75
|
-
let text = std::fs::read_to_string(spec_path)
|
|
76
|
-
LifecycleError::Compile(format!("{}: {e}", spec_path.display()))
|
|
77
|
-
})?;
|
|
75
|
+
let text = std::fs::read_to_string(spec_path)
|
|
76
|
+
.map_err(|e| LifecycleError::Compile(format!("{}: {e}", spec_path.display())))?;
|
|
78
77
|
let spec = yaml::loads(&text).map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
79
78
|
let session_name = spec_session_name(&spec);
|
|
80
79
|
let safety = effective_runtime_config(&spec)?;
|
|
@@ -101,8 +100,23 @@ pub fn launch_with_transport_in_workspace(
|
|
|
101
100
|
let started = if dry_run {
|
|
102
101
|
Vec::new()
|
|
103
102
|
} else {
|
|
104
|
-
let started = spawn_agents(
|
|
105
|
-
|
|
103
|
+
let started = spawn_agents(
|
|
104
|
+
workspace,
|
|
105
|
+
spec_path,
|
|
106
|
+
&spec,
|
|
107
|
+
&session_name,
|
|
108
|
+
&safety,
|
|
109
|
+
transport,
|
|
110
|
+
)?;
|
|
111
|
+
persist_spawn_agent_state(
|
|
112
|
+
workspace,
|
|
113
|
+
spec_path,
|
|
114
|
+
&spec,
|
|
115
|
+
&session_name,
|
|
116
|
+
transport,
|
|
117
|
+
&started,
|
|
118
|
+
&safety,
|
|
119
|
+
)?;
|
|
106
120
|
started
|
|
107
121
|
};
|
|
108
122
|
Ok(LaunchReport {
|
|
@@ -126,6 +140,10 @@ fn spawn_agents(
|
|
|
126
140
|
transport: &dyn Transport,
|
|
127
141
|
) -> Result<Vec<StartedAgent>, LifecycleError> {
|
|
128
142
|
let team_dir = spec_path.parent().unwrap_or_else(|| Path::new("."));
|
|
143
|
+
let runtime_fast = matches!(
|
|
144
|
+
spec.get("runtime").and_then(|v| v.get("fast")),
|
|
145
|
+
Some(Value::Bool(true))
|
|
146
|
+
);
|
|
129
147
|
let mut started = Vec::new();
|
|
130
148
|
for agent in spec_agent_values(spec) {
|
|
131
149
|
let Some(agent_id_raw) = agent.get("id").and_then(Value::as_str) else {
|
|
@@ -152,59 +170,167 @@ fn spawn_agents(
|
|
|
152
170
|
// has both the role instruction AND the callable Team Agent MCP capability.
|
|
153
171
|
// probe5 RED proved that `build_command(.., None, None, ..)` left the worker
|
|
154
172
|
// without `report_result`; placeholders are substituted at spawn time.
|
|
155
|
-
let
|
|
156
|
-
|
|
157
|
-
|
|
173
|
+
let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
|
|
174
|
+
agent,
|
|
175
|
+
Some(agent_id_raw),
|
|
176
|
+
provider,
|
|
177
|
+
);
|
|
178
|
+
let system_prompt =
|
|
179
|
+
crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
|
|
180
|
+
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
181
|
+
&command_agent,
|
|
182
|
+
provider,
|
|
183
|
+
safety,
|
|
184
|
+
)?;
|
|
185
|
+
let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
158
186
|
let mcp_team_id =
|
|
159
187
|
runtime_active_team_key_for_spawn(workspace, spec_path, spec, session_name);
|
|
160
188
|
let mcp_config = adapter
|
|
161
189
|
.mcp_config(auth_mode)
|
|
162
190
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
163
191
|
let mcp_config = resolve_mcp_config(mcp_config, workspace, agent_id_raw, &mcp_team_id);
|
|
164
|
-
let mcp_config_path =
|
|
165
|
-
let profile_dir = team_dir.join("profiles");
|
|
166
|
-
let profile_launch = crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
|
|
192
|
+
let mcp_config_path = write_worker_mcp_config_for_provider(
|
|
167
193
|
workspace,
|
|
168
194
|
agent_id_raw,
|
|
169
|
-
|
|
170
|
-
Some(
|
|
171
|
-
Some(&mcp_config),
|
|
195
|
+
&mcp_config,
|
|
196
|
+
Some(provider),
|
|
172
197
|
)?;
|
|
173
|
-
let
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
198
|
+
let profile_dir = team_dir.join("profiles");
|
|
199
|
+
let profile_launch =
|
|
200
|
+
crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
|
|
201
|
+
workspace,
|
|
202
|
+
agent_id_raw,
|
|
203
|
+
agent,
|
|
204
|
+
Some(&profile_dir),
|
|
205
|
+
Some(&mcp_config),
|
|
206
|
+
)?;
|
|
207
|
+
let command_model = profile_launch.command_overrides.model.as_deref().or(model);
|
|
178
208
|
let mut plan = adapter
|
|
179
209
|
.build_command_plan(crate::provider::ProviderCommandContext {
|
|
180
210
|
auth_mode,
|
|
181
211
|
mcp_config: Some(&mcp_config),
|
|
182
|
-
system_prompt:
|
|
212
|
+
system_prompt: Some(system_prompt.as_str()),
|
|
183
213
|
model: command_model,
|
|
184
|
-
tools: &
|
|
214
|
+
tools: &resolved_tool_refs,
|
|
185
215
|
profile_launch: Some(&profile_launch),
|
|
186
216
|
})
|
|
187
217
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
188
218
|
if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
|
|
189
219
|
point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
|
|
190
220
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
)
|
|
221
|
+
// C-A-4 cr verdict v2 — Copilot BYOK(compatible_api)硬性校验:
|
|
222
|
+
// "A model is required for BYOK"(help-providers 原文)。检查 agent
|
|
223
|
+
// 的 model 来源:角色 spec.model > profile COPILOT_MODEL(经 env_overlay)
|
|
224
|
+
// > --model 旗(本 worker 路径不在 argv 后追加用户 --model)。三者全空 → 报错
|
|
225
|
+
// 含 "model" 字面,失败信息透传给 leader。
|
|
226
|
+
if matches!(provider, Provider::Copilot) && auth_mode == AuthMode::CompatibleApi {
|
|
227
|
+
let has_model = model.is_some_and(|s| !s.is_empty())
|
|
228
|
+
|| profile_launch.command_overrides.model.as_deref().is_some_and(|s| !s.is_empty())
|
|
229
|
+
|| profile_launch
|
|
230
|
+
.env_overlay
|
|
231
|
+
.get("COPILOT_MODEL")
|
|
232
|
+
.is_some_and(|v| !v.is_empty());
|
|
233
|
+
if !has_model {
|
|
234
|
+
return Err(LifecycleError::RequirementUnmet(
|
|
235
|
+
"copilot BYOK profile requires a model (set COPILOT_MODEL, agent.model, or --model)"
|
|
236
|
+
.to_string(),
|
|
237
|
+
));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// §B1 + C-7-1 + C-6-2 + C-3-2 cr verdict v2 — Copilot launch-time argv 注入:
|
|
241
|
+
// -n <agent_id> 会话命名(main-help:104)→ resume-by-name + 人查 双键
|
|
242
|
+
// -C <workspace> 双保险 cwd(main-help:55-56),防 shell 包装意外
|
|
243
|
+
// --log-dir <path> per-worker 定向日志(help-logging)→ 故障期可读 + N18 隔离
|
|
244
|
+
// --log-level info 配套日志级别
|
|
245
|
+
// --disable-mcp-server <n>... C-3-2 残留 MCP server 按名禁(扫 mcp list)
|
|
246
|
+
if matches!(provider, Provider::Copilot) {
|
|
247
|
+
plan.argv.push("-n".to_string());
|
|
248
|
+
plan.argv.push(agent_id_raw.to_string());
|
|
249
|
+
plan.argv.push("-C".to_string());
|
|
250
|
+
plan.argv.push(workspace.to_string_lossy().to_string());
|
|
251
|
+
let log_dir = workspace
|
|
252
|
+
.join(".team")
|
|
253
|
+
.join("logs")
|
|
254
|
+
.join("copilot")
|
|
255
|
+
.join(agent_id_raw);
|
|
256
|
+
std::fs::create_dir_all(&log_dir).map_err(|e| {
|
|
257
|
+
LifecycleError::StatePersist(format!("{}: {e}", log_dir.display()))
|
|
258
|
+
})?;
|
|
259
|
+
plan.argv.push("--log-dir".to_string());
|
|
260
|
+
plan.argv.push(log_dir.to_string_lossy().to_string());
|
|
261
|
+
plan.argv.push("--log-level".to_string());
|
|
262
|
+
plan.argv.push("info".to_string());
|
|
263
|
+
// C-3-2/C-3-3 cr verdict v2 — spawn 前扫 `copilot mcp list` 找用户全局/
|
|
264
|
+
// workspace 的 MCP 残留,对每个非 team_orchestrator server 追加
|
|
265
|
+
// --disable-mcp-server <name>,并落 mcp-residual.txt + event。
|
|
266
|
+
apply_copilot_mcp_residual_disables(
|
|
267
|
+
&workspace,
|
|
268
|
+
agent_id_raw,
|
|
269
|
+
&mut plan.argv,
|
|
270
|
+
&log_dir,
|
|
271
|
+
)?;
|
|
272
|
+
}
|
|
273
|
+
fill_spawn_placeholders_full(&mut plan.argv, workspace, agent_id_raw, Some(&mcp_team_id));
|
|
197
274
|
let window = WindowName::new(agent_id_raw);
|
|
198
|
-
let mut env =
|
|
199
|
-
workspace,
|
|
200
|
-
agent_id_raw,
|
|
201
|
-
Some(&mcp_team_id),
|
|
202
|
-
);
|
|
275
|
+
let mut env =
|
|
276
|
+
inherited_env_with_team_overrides(workspace, agent_id_raw, Some(&mcp_team_id));
|
|
203
277
|
apply_profile_launch_env(&mut env, &profile_launch);
|
|
278
|
+
// Python providers.py:145 + launch/core.py:253 — fresh launch runs the worker
|
|
279
|
+
// with cwd=workspace, same as the RS fork/add and restart paths.
|
|
280
|
+
let env_unset: Vec<String> = profile_launch.env_unset.iter().cloned().collect();
|
|
281
|
+
// BUG / C-1-2 / C-6-1 cr verdict — Copilot system_prompt 走 spawn env overlay +
|
|
282
|
+
// per-worker AGENTS.md(B2 灵魂件降级):写
|
|
283
|
+
// <workspace>/.team/runtime/copilot-instructions/<agent_id>/AGENTS.md
|
|
284
|
+
// 全文 == compile_worker_system_prompt 输出,并通过 spawn env
|
|
285
|
+
// `COPILOT_CUSTOM_INSTRUCTIONS_DIRS=<该目录>` 让 copilot CLI 加载。
|
|
286
|
+
// **禁** silent 写 ~/.copilot/AGENTS.md(C-1-2)+ **禁** -i 作首条消息(C-1-5)。
|
|
287
|
+
if matches!(provider, Provider::Copilot) {
|
|
288
|
+
apply_copilot_instructions_overlay(
|
|
289
|
+
workspace,
|
|
290
|
+
agent_id_raw,
|
|
291
|
+
system_prompt.as_str(),
|
|
292
|
+
&mut env,
|
|
293
|
+
)?;
|
|
294
|
+
// C-A-6 cr verdict v2 — Copilot worker env 全量继承下,用户 shell 的
|
|
295
|
+
// COPILOT_GITHUB_TOKEN / GH_TOKEN / GITHUB_TOKEN 会穿透 + 按 cmd-login 实证
|
|
296
|
+
// **优先于凭据库**(可能静默改变 auth 通道)。一期只观测不剥除(剥除是
|
|
297
|
+
// 行为变更,cr 裁);命中任一就发 warn event 让 user 可见。
|
|
298
|
+
let mut passthrough: Vec<String> = Vec::new();
|
|
299
|
+
for key in ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"] {
|
|
300
|
+
if env.get(key).is_some_and(|v| !v.is_empty()) {
|
|
301
|
+
passthrough.push(key.to_string());
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if !passthrough.is_empty() {
|
|
305
|
+
let event_log = crate::event_log::EventLog::new(workspace);
|
|
306
|
+
let _ = event_log.write(
|
|
307
|
+
"provider.copilot.token_passthrough_warning",
|
|
308
|
+
serde_json::json!({
|
|
309
|
+
"agent_id": agent_id_raw,
|
|
310
|
+
"tokens": passthrough,
|
|
311
|
+
"reason": "user shell GITHUB_TOKEN family takes precedence over copilot credential store (cmd-login)",
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
204
316
|
let spawn = if started.is_empty() {
|
|
205
|
-
transport.
|
|
317
|
+
transport.spawn_first_with_env_unset(
|
|
318
|
+
session_name,
|
|
319
|
+
&window,
|
|
320
|
+
&plan.argv,
|
|
321
|
+
workspace,
|
|
322
|
+
&env,
|
|
323
|
+
&env_unset,
|
|
324
|
+
)
|
|
206
325
|
} else {
|
|
207
|
-
transport.
|
|
326
|
+
transport.spawn_into_with_env_unset(
|
|
327
|
+
session_name,
|
|
328
|
+
&window,
|
|
329
|
+
&plan.argv,
|
|
330
|
+
workspace,
|
|
331
|
+
&env,
|
|
332
|
+
&env_unset,
|
|
333
|
+
)
|
|
208
334
|
}
|
|
209
335
|
.map_err(|e| LifecycleError::Transport(e.to_string()))?;
|
|
210
336
|
let _ = adapter.handle_startup_prompts(
|
|
@@ -213,6 +339,11 @@ fn spawn_agents(
|
|
|
213
339
|
30,
|
|
214
340
|
0.5,
|
|
215
341
|
);
|
|
342
|
+
// Python launch/core.py:235-237 — runtime.fast toggles the provider's fast mode
|
|
343
|
+
// after spawn; provider specifics live behind the adapter (F032).
|
|
344
|
+
if runtime_fast {
|
|
345
|
+
let _ = adapter.enable_fast_mode(transport, &Target::Pane(spawn.pane_id.clone()));
|
|
346
|
+
}
|
|
216
347
|
if matches!(transport.liveness(&spawn.pane_id), Ok(PaneLiveness::Dead)) {
|
|
217
348
|
continue;
|
|
218
349
|
}
|
|
@@ -258,12 +389,7 @@ fn persist_spawn_agent_state(
|
|
|
258
389
|
let team_id = explicit_active_team_key(&state)
|
|
259
390
|
.unwrap_or_else(|| runtime_team_key_for_spec(spec_path, spec, session_name));
|
|
260
391
|
let worker_tmux_socket = launched_worker_tmux_socket(transport, workspace);
|
|
261
|
-
drop_worker_pane_seeded_owner(
|
|
262
|
-
&mut state,
|
|
263
|
-
&team_id,
|
|
264
|
-
started,
|
|
265
|
-
worker_tmux_socket.as_deref(),
|
|
266
|
-
);
|
|
392
|
+
drop_worker_pane_seeded_owner(&mut state, &team_id, started, worker_tmux_socket.as_deref());
|
|
267
393
|
// Only persist running state for agents whose spawn still has a live target.
|
|
268
394
|
let live_windows: BTreeSet<String> = transport
|
|
269
395
|
.list_windows(session_name)
|
|
@@ -276,10 +402,7 @@ fn persist_spawn_agent_state(
|
|
|
276
402
|
.map(|agent| agent.agent_id.as_str().to_string())
|
|
277
403
|
.collect();
|
|
278
404
|
let pane_pids_by_agent = pane_pids_by_started_agent(transport, started);
|
|
279
|
-
let profile_dir = spec_path
|
|
280
|
-
.parent()
|
|
281
|
-
.unwrap_or(workspace)
|
|
282
|
-
.join("profiles");
|
|
405
|
+
let profile_dir = spec_path.parent().unwrap_or(workspace).join("profiles");
|
|
283
406
|
let mut agents = serde_json::Map::new();
|
|
284
407
|
let mut spawn_index = 0_u32;
|
|
285
408
|
for agent in spec_agent_values(spec) {
|
|
@@ -317,9 +440,7 @@ fn persist_spawn_agent_state(
|
|
|
317
440
|
let pane_pid = pane_pids_by_agent.get(id).copied();
|
|
318
441
|
let spawned_at = spawn_timestamp_for_agent(spawn_index);
|
|
319
442
|
spawn_index = spawn_index.saturating_add(1);
|
|
320
|
-
let started_agent = started
|
|
321
|
-
.iter()
|
|
322
|
-
.find(|agent| agent.agent_id.as_str() == id);
|
|
443
|
+
let started_agent = started.iter().find(|agent| agent.agent_id.as_str() == id);
|
|
323
444
|
agents.insert(
|
|
324
445
|
id.to_string(),
|
|
325
446
|
running_agent_state(
|
|
@@ -327,7 +448,7 @@ fn persist_spawn_agent_state(
|
|
|
327
448
|
id,
|
|
328
449
|
provider,
|
|
329
450
|
workspace,
|
|
330
|
-
|
|
451
|
+
workspace,
|
|
331
452
|
&spawned_at,
|
|
332
453
|
&team_id,
|
|
333
454
|
Some(agent_id_to_pane_id(started, id)),
|
|
@@ -373,7 +494,10 @@ fn agent_id_to_pane_id<'a>(started: &'a [StartedAgent], agent_id: &str) -> &'a s
|
|
|
373
494
|
.unwrap_or("")
|
|
374
495
|
}
|
|
375
496
|
|
|
376
|
-
fn save_launched_team_state(
|
|
497
|
+
fn save_launched_team_state(
|
|
498
|
+
workspace: &Path,
|
|
499
|
+
launched: &serde_json::Value,
|
|
500
|
+
) -> Result<(), LifecycleError> {
|
|
377
501
|
save_launched_team_state_for_key(workspace, launched, None)
|
|
378
502
|
}
|
|
379
503
|
|
|
@@ -404,7 +528,8 @@ fn save_launched_team_state_for_key(
|
|
|
404
528
|
};
|
|
405
529
|
let mut projected = crate::state::projection::project_top_level_view(&merged, &launched_key);
|
|
406
530
|
drop_unbound_top_level_owner(&mut projected);
|
|
407
|
-
save_runtime_state(workspace, &projected)
|
|
531
|
+
save_runtime_state(workspace, &projected)
|
|
532
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
408
533
|
}
|
|
409
534
|
|
|
410
535
|
fn drop_bare_worker_seeded_owner(launched: &mut serde_json::Value, launched_key: &str) {
|
|
@@ -506,7 +631,11 @@ fn drop_unbound_top_level_owner(state: &mut serde_json::Value) {
|
|
|
506
631
|
}
|
|
507
632
|
}
|
|
508
633
|
|
|
509
|
-
fn drop_foreign_seeded_owner(
|
|
634
|
+
fn drop_foreign_seeded_owner(
|
|
635
|
+
existing: &serde_json::Value,
|
|
636
|
+
launched_key: &str,
|
|
637
|
+
launched: &mut serde_json::Value,
|
|
638
|
+
) {
|
|
510
639
|
let Some(pane) = launched
|
|
511
640
|
.get("team_owner")
|
|
512
641
|
.and_then(|owner| owner.get("pane_id"))
|
|
@@ -549,8 +678,7 @@ fn drop_worker_pane_seeded_owner(
|
|
|
549
678
|
.ok()
|
|
550
679
|
.filter(|value| !value.is_empty());
|
|
551
680
|
let has_leader_identity_env = has_positive_caller_leader_env();
|
|
552
|
-
let seeded_from_bare_tmux =
|
|
553
|
-
!has_leader_identity_env && tmux_pane.as_deref() == Some(pane);
|
|
681
|
+
let seeded_from_bare_tmux = !has_leader_identity_env && tmux_pane.as_deref() == Some(pane);
|
|
554
682
|
let caller_tmux_socket = crate::tmux_backend::socket_name_from_tmux_env();
|
|
555
683
|
if seeded_from_bare_tmux
|
|
556
684
|
&& (tmux_sockets_match_or_unknown(caller_tmux_socket.as_deref(), worker_tmux_socket)
|
|
@@ -563,19 +691,14 @@ fn drop_worker_pane_seeded_owner(
|
|
|
563
691
|
|
|
564
692
|
fn seeded_pane_looks_like_worker(pane: &str, started: &[StartedAgent]) -> bool {
|
|
565
693
|
pane.ends_with("-first")
|
|
566
|
-
|| started
|
|
567
|
-
.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|| agent.target.starts_with(pane)
|
|
572
|
-
})
|
|
694
|
+
|| started.iter().any(|agent| {
|
|
695
|
+
pane == agent.target
|
|
696
|
+
|| pane.starts_with(agent.target.as_str())
|
|
697
|
+
|| agent.target.starts_with(pane)
|
|
698
|
+
})
|
|
573
699
|
}
|
|
574
700
|
|
|
575
|
-
fn launched_worker_tmux_socket(
|
|
576
|
-
transport: &dyn Transport,
|
|
577
|
-
workspace: &Path,
|
|
578
|
-
) -> Option<String> {
|
|
701
|
+
fn launched_worker_tmux_socket(transport: &dyn Transport, workspace: &Path) -> Option<String> {
|
|
579
702
|
if matches!(transport.kind(), crate::transport::BackendKind::Tmux) {
|
|
580
703
|
Some(crate::tmux_backend::socket_name_for_workspace(workspace))
|
|
581
704
|
} else {
|
|
@@ -583,10 +706,7 @@ fn launched_worker_tmux_socket(
|
|
|
583
706
|
}
|
|
584
707
|
}
|
|
585
708
|
|
|
586
|
-
fn tmux_sockets_match_or_unknown(
|
|
587
|
-
caller_socket: Option<&str>,
|
|
588
|
-
worker_socket: Option<&str>,
|
|
589
|
-
) -> bool {
|
|
709
|
+
fn tmux_sockets_match_or_unknown(caller_socket: Option<&str>, worker_socket: Option<&str>) -> bool {
|
|
590
710
|
match (caller_socket, worker_socket) {
|
|
591
711
|
(Some(caller), Some(worker)) => caller == worker,
|
|
592
712
|
(Some(_), None) => false,
|
|
@@ -595,7 +715,9 @@ fn tmux_sockets_match_or_unknown(
|
|
|
595
715
|
}
|
|
596
716
|
|
|
597
717
|
fn env_nonempty(key: &str) -> bool {
|
|
598
|
-
std::env::var(key)
|
|
718
|
+
std::env::var(key)
|
|
719
|
+
.ok()
|
|
720
|
+
.is_some_and(|value| !value.is_empty())
|
|
599
721
|
}
|
|
600
722
|
|
|
601
723
|
fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &str) {
|
|
@@ -664,7 +786,11 @@ fn unbound_launched_owner(
|
|
|
664
786
|
}))
|
|
665
787
|
}
|
|
666
788
|
|
|
667
|
-
fn owner_pane_belongs_to_other_team(
|
|
789
|
+
fn owner_pane_belongs_to_other_team(
|
|
790
|
+
existing: &serde_json::Value,
|
|
791
|
+
launched_key: &str,
|
|
792
|
+
pane: &str,
|
|
793
|
+
) -> bool {
|
|
668
794
|
existing
|
|
669
795
|
.get("teams")
|
|
670
796
|
.and_then(serde_json::Value::as_object)
|
|
@@ -700,18 +826,25 @@ fn running_agent_state(
|
|
|
700
826
|
.and_then(Value::as_str)
|
|
701
827
|
.and_then(parse_auth_mode)
|
|
702
828
|
.unwrap_or(AuthMode::Subscription);
|
|
703
|
-
let profile = agent
|
|
829
|
+
let profile = agent
|
|
830
|
+
.get("profile")
|
|
831
|
+
.map(yaml_value_to_json)
|
|
832
|
+
.unwrap_or(serde_json::Value::Null);
|
|
704
833
|
let window = agent.get("window").and_then(Value::as_str).unwrap_or(id);
|
|
705
834
|
let mcp_config = crate::provider::get_adapter(provider)
|
|
706
835
|
.mcp_config(auth_mode)
|
|
707
836
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
708
837
|
let mcp_config = resolve_mcp_config(mcp_config, workspace, id, team_id);
|
|
709
|
-
let mcp_config_path =
|
|
838
|
+
let mcp_config_path =
|
|
839
|
+
write_worker_mcp_config_for_provider(workspace, id, &mcp_config, Some(provider))?;
|
|
710
840
|
let mut state = serde_json::Map::new();
|
|
711
841
|
state.insert("status".to_string(), serde_json::json!("running"));
|
|
712
842
|
state.insert("provider".to_string(), serde_json::json!(provider));
|
|
713
843
|
state.insert("agent_id".to_string(), serde_json::json!(id));
|
|
714
|
-
state.insert(
|
|
844
|
+
state.insert(
|
|
845
|
+
"model".to_string(),
|
|
846
|
+
model.map_or(serde_json::Value::Null, |m| serde_json::json!(m)),
|
|
847
|
+
);
|
|
715
848
|
state.insert("auth_mode".to_string(), serde_json::json!(auth_mode));
|
|
716
849
|
state.insert("profile".to_string(), profile);
|
|
717
850
|
if agent.get("profile").is_some() {
|
|
@@ -737,7 +870,10 @@ fn running_agent_state(
|
|
|
737
870
|
state.insert("rollout_path".to_string(), serde_json::Value::Null);
|
|
738
871
|
state.insert("captured_at".to_string(), serde_json::Value::Null);
|
|
739
872
|
state.insert("captured_via".to_string(), serde_json::Value::Null);
|
|
740
|
-
state.insert(
|
|
873
|
+
state.insert(
|
|
874
|
+
"attribution_confidence".to_string(),
|
|
875
|
+
serde_json::Value::Null,
|
|
876
|
+
);
|
|
741
877
|
if let Some(started_agent) = started_agent {
|
|
742
878
|
persist_started_agent_plan_state(&mut state, started_agent);
|
|
743
879
|
}
|
|
@@ -832,6 +968,20 @@ pub(crate) fn write_worker_mcp_config(
|
|
|
832
968
|
workspace: &Path,
|
|
833
969
|
agent_id: &str,
|
|
834
970
|
config: &crate::provider::McpConfig,
|
|
971
|
+
) -> Result<PathBuf, LifecycleError> {
|
|
972
|
+
write_worker_mcp_config_for_provider(workspace, agent_id, config, None)
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/// C-3-4 cr verdict v2 — Copilot 的 mcp config schema 字段名是 `transport`
|
|
976
|
+
/// (实测 cmd-mcp-add 原文取值 stdio|http|sse),不是 canonical 的 `type`。当
|
|
977
|
+
/// provider==Copilot 时写出文件前先做 type→transport 翻译;其它 provider 不动。
|
|
978
|
+
/// 文件路径同 canonical `<ws>/.team/runtime/mcp/<agent_id>.json`,因为 launch
|
|
979
|
+
/// 路径会用 `--additional-mcp-config @<file>` 直指它。
|
|
980
|
+
pub(crate) fn write_worker_mcp_config_for_provider(
|
|
981
|
+
workspace: &Path,
|
|
982
|
+
agent_id: &str,
|
|
983
|
+
config: &crate::provider::McpConfig,
|
|
984
|
+
provider: Option<Provider>,
|
|
835
985
|
) -> Result<PathBuf, LifecycleError> {
|
|
836
986
|
let path = workspace
|
|
837
987
|
.join(".team/runtime/mcp")
|
|
@@ -840,22 +990,70 @@ pub(crate) fn write_worker_mcp_config(
|
|
|
840
990
|
std::fs::create_dir_all(parent)
|
|
841
991
|
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", parent.display())))?;
|
|
842
992
|
}
|
|
843
|
-
let
|
|
993
|
+
let raw = if matches!(provider, Some(Provider::Copilot)) {
|
|
994
|
+
copilot_translate_mcp_servers(&config.raw)
|
|
995
|
+
} else {
|
|
996
|
+
config.raw.clone()
|
|
997
|
+
};
|
|
998
|
+
let body = serde_json::to_string_pretty(&serde_json::json!({"mcpServers": raw}))
|
|
844
999
|
.map_err(|e| LifecycleError::StatePersist(format!("serialize mcp config: {e}")))?;
|
|
845
1000
|
std::fs::write(&path, body)
|
|
846
1001
|
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", path.display())))?;
|
|
847
1002
|
Ok(path)
|
|
848
1003
|
}
|
|
849
1004
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
let Some(
|
|
855
|
-
return;
|
|
1005
|
+
/// C-3-4 cr verdict v2 — McpConfig.raw 是 `{name: {type, command, args, env}}` 形;
|
|
1006
|
+
/// copilot mcp add schema 取 `transport` 替 `type`(stdio|http|sse 同值)。仅
|
|
1007
|
+
/// 字段名变换,其余字段全保留。
|
|
1008
|
+
fn copilot_translate_mcp_servers(raw: &serde_json::Value) -> serde_json::Value {
|
|
1009
|
+
let Some(servers) = raw.as_object() else {
|
|
1010
|
+
return raw.clone();
|
|
856
1011
|
};
|
|
857
|
-
|
|
858
|
-
|
|
1012
|
+
let mut translated = serde_json::Map::new();
|
|
1013
|
+
for (name, server) in servers {
|
|
1014
|
+
let Some(obj) = server.as_object() else {
|
|
1015
|
+
translated.insert(name.clone(), server.clone());
|
|
1016
|
+
continue;
|
|
1017
|
+
};
|
|
1018
|
+
let mut out = serde_json::Map::new();
|
|
1019
|
+
for (key, value) in obj {
|
|
1020
|
+
if key == "type" {
|
|
1021
|
+
out.insert("transport".to_string(), value.clone());
|
|
1022
|
+
} else {
|
|
1023
|
+
out.insert(key.clone(), value.clone());
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
translated.insert(name.clone(), serde_json::Value::Object(out));
|
|
1027
|
+
}
|
|
1028
|
+
serde_json::Value::Object(translated)
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
pub(crate) fn point_native_mcp_config_at_file(
|
|
1032
|
+
argv: &mut [String],
|
|
1033
|
+
provider: Provider,
|
|
1034
|
+
path: &Path,
|
|
1035
|
+
) {
|
|
1036
|
+
match provider {
|
|
1037
|
+
Provider::Claude | Provider::ClaudeCode => {
|
|
1038
|
+
let Some(index) = argv.iter().position(|arg| arg == "--mcp-config") else {
|
|
1039
|
+
return;
|
|
1040
|
+
};
|
|
1041
|
+
if let Some(value) = argv.get_mut(index.saturating_add(1)) {
|
|
1042
|
+
*value = path.to_string_lossy().to_string();
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
// §C1 note: copilot `--additional-mcp-config` 接受 `@file`,直接指向既有
|
|
1046
|
+
// `.team/runtime/mcp/<agent>.json`(launch 路径 write_worker_mcp_config 已写)。
|
|
1047
|
+
// 既避免 inline JSON 包 mcpServers wrapper 的语义错位,也更利于 ps 验法。
|
|
1048
|
+
Provider::Copilot => {
|
|
1049
|
+
let Some(index) = argv.iter().position(|arg| arg == "--additional-mcp-config") else {
|
|
1050
|
+
return;
|
|
1051
|
+
};
|
|
1052
|
+
if let Some(value) = argv.get_mut(index.saturating_add(1)) {
|
|
1053
|
+
*value = format!("@{}", path.to_string_lossy());
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
_ => {}
|
|
859
1057
|
}
|
|
860
1058
|
}
|
|
861
1059
|
|
|
@@ -874,13 +1072,19 @@ fn permissions_json(
|
|
|
874
1072
|
let resolved = permissions::resolve_permissions(&AgentPermissionInput {
|
|
875
1073
|
id: Some(AgentId::new(id)),
|
|
876
1074
|
provider,
|
|
877
|
-
role: agent
|
|
1075
|
+
role: agent
|
|
1076
|
+
.get("role")
|
|
1077
|
+
.and_then(Value::as_str)
|
|
1078
|
+
.map(str::to_string),
|
|
878
1079
|
tools,
|
|
879
1080
|
})?;
|
|
880
1081
|
let mut out = serde_json::Map::new();
|
|
881
1082
|
out.insert("agent_id".to_string(), serde_json::json!(id));
|
|
882
1083
|
out.insert("provider".to_string(), serde_json::json!(provider));
|
|
883
|
-
out.insert(
|
|
1084
|
+
out.insert(
|
|
1085
|
+
"tools".to_string(),
|
|
1086
|
+
serde_json::json!(resolved.sorted_tool_strings()),
|
|
1087
|
+
);
|
|
884
1088
|
out.insert(
|
|
885
1089
|
"resolved_tools".to_string(),
|
|
886
1090
|
serde_json::Value::Array(
|
|
@@ -896,7 +1100,10 @@ fn permissions_json(
|
|
|
896
1100
|
.collect(),
|
|
897
1101
|
),
|
|
898
1102
|
);
|
|
899
|
-
out.insert(
|
|
1103
|
+
out.insert(
|
|
1104
|
+
"has_prompt_only".to_string(),
|
|
1105
|
+
serde_json::json!(resolved.has_prompt_only),
|
|
1106
|
+
);
|
|
900
1107
|
Ok(serde_json::Value::Object(out))
|
|
901
1108
|
}
|
|
902
1109
|
|
|
@@ -920,9 +1127,10 @@ fn spawn_timestamp_for_agent(offset_micros: u32) -> String {
|
|
|
920
1127
|
match std::env::var("TEAM_AGENT_TEST_FIXED_SPAWNED_AT") {
|
|
921
1128
|
Ok(value) => chrono::DateTime::parse_from_rfc3339(&value)
|
|
922
1129
|
.map(|dt| {
|
|
923
|
-
(dt.with_timezone(&chrono::Utc)
|
|
924
|
-
|
|
925
|
-
|
|
1130
|
+
(dt.with_timezone(&chrono::Utc)
|
|
1131
|
+
+ chrono::Duration::microseconds(i64::from(offset_micros)))
|
|
1132
|
+
.format("%Y-%m-%dT%H:%M:%S%.6f+00:00")
|
|
1133
|
+
.to_string()
|
|
926
1134
|
})
|
|
927
1135
|
.unwrap_or(value),
|
|
928
1136
|
Err(_) => spawn_timestamp(),
|
|
@@ -964,6 +1172,10 @@ pub(crate) fn inherited_env_with_team_overrides(
|
|
|
964
1172
|
"TEAM_AGENT_WORKSPACE".to_string(),
|
|
965
1173
|
workspace.to_string_lossy().to_string(),
|
|
966
1174
|
);
|
|
1175
|
+
// Python providers.py:131 — TEAM_AGENT_ID must be the worker ITSELF, overriding any
|
|
1176
|
+
// value inherited from the launching process (an add-agent/fork issued from another
|
|
1177
|
+
// worker's MCP server carries the CALLER's TEAM_AGENT_ID in its environ).
|
|
1178
|
+
env.insert("TEAM_AGENT_ID".to_string(), agent_id.to_string());
|
|
967
1179
|
env.insert("TEAM_AGENT_AGENT_ID".to_string(), agent_id.to_string());
|
|
968
1180
|
if let Some(tid) = team_id.filter(|s| !s.is_empty()) {
|
|
969
1181
|
env.insert("TEAM_AGENT_OWNER_TEAM_ID".to_string(), tid.to_string());
|
|
@@ -971,6 +1183,190 @@ pub(crate) fn inherited_env_with_team_overrides(
|
|
|
971
1183
|
env
|
|
972
1184
|
}
|
|
973
1185
|
|
|
1186
|
+
/// BUG / B2 灵魂件 + C-1-2 + C-6-1 cr verdict — Copilot per-worker AGENTS.md
|
|
1187
|
+
/// 写入 + `COPILOT_CUSTOM_INSTRUCTIONS_DIRS` 注入。
|
|
1188
|
+
///
|
|
1189
|
+
/// 目录布局:`<workspace>/.team/runtime/copilot-instructions/<agent_id>/AGENTS.md`
|
|
1190
|
+
/// * 含 `<agent_id>` segment(C-6-2 per-agent isolation,N18 精神)
|
|
1191
|
+
/// * 文件内容 ≡ `compile_worker_system_prompt` 输出(B2 ps/文件双验法)
|
|
1192
|
+
/// * **禁** silent 写全局 `~/.copilot/AGENTS.md`(C-1-2 grep guard)
|
|
1193
|
+
///
|
|
1194
|
+
/// 失败回 `LifecycleError::StatePersist` 以与既有 state 持久化错误同源,
|
|
1195
|
+
/// 不 silent 吞(MUST-NOT-13 诚实)。
|
|
1196
|
+
pub(crate) fn apply_copilot_instructions_overlay(
|
|
1197
|
+
workspace: &Path,
|
|
1198
|
+
agent_id: &str,
|
|
1199
|
+
system_prompt: &str,
|
|
1200
|
+
env: &mut BTreeMap<String, String>,
|
|
1201
|
+
) -> Result<(), LifecycleError> {
|
|
1202
|
+
let dir = workspace
|
|
1203
|
+
.join(".team")
|
|
1204
|
+
.join("runtime")
|
|
1205
|
+
.join("copilot-instructions")
|
|
1206
|
+
.join(agent_id);
|
|
1207
|
+
std::fs::create_dir_all(&dir)
|
|
1208
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", dir.display())))?;
|
|
1209
|
+
let agents_md = dir.join("AGENTS.md");
|
|
1210
|
+
std::fs::write(&agents_md, system_prompt.as_bytes())
|
|
1211
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", agents_md.display())))?;
|
|
1212
|
+
env.insert(
|
|
1213
|
+
"COPILOT_CUSTOM_INSTRUCTIONS_DIRS".to_string(),
|
|
1214
|
+
dir.to_string_lossy().to_string(),
|
|
1215
|
+
);
|
|
1216
|
+
// ★ C-4 P0(N39 红线 / MUST-12) — copilot config 默认 `updateTerminalTitle=true`
|
|
1217
|
+
// 会改 tmux window 名(help-config 原文)。tmux window 名是框架定位 agent 的
|
|
1218
|
+
// anchor(window==agent_id);copilot 静默改写 → 寻址 / kill / 保护集 三处同源
|
|
1219
|
+
// 派生漂移 → B5 protected_set 误判、MUST-12 pane 身份失锚、N39 同源派生破。
|
|
1220
|
+
// 漏关后果定级为【B5 leader 误杀同级 incident】,绝不允许 silent 跳过。
|
|
1221
|
+
// 主案:env `COPILOT_DISABLE_TERMINAL_TITLE=1`(help-config 原文 "Can also be
|
|
1222
|
+
// disabled via the COPILOT_DISABLE_TERMINAL_TITLE environment variable")。
|
|
1223
|
+
env.insert("COPILOT_DISABLE_TERMINAL_TITLE".to_string(), "1".to_string());
|
|
1224
|
+
Ok(())
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/// C-3-2/C-3-3 cr verdict v2 — Copilot spawn 前调 `copilot mcp list` 扫用户全局
|
|
1228
|
+
/// `~/.copilot/mcp-config.json` 与 workspace `.mcp.json` 的 MCP 残留;对每个非
|
|
1229
|
+
/// `team_orchestrator` server 追加 `--disable-mcp-server <name>`(main-help:72-73)
|
|
1230
|
+
/// 并落 `<log_dir>/mcp-residual.txt` + emit `provider.copilot.mcp_residual_detected`
|
|
1231
|
+
/// event(MUST-NOT-13 诚实记录,非 silent)。
|
|
1232
|
+
///
|
|
1233
|
+
/// 失败回 `LifecycleError::StatePersist`,不 silent 吞;`copilot mcp list` 自身
|
|
1234
|
+
/// 无法运行(命令缺失 / 退出码非零)时,仅记 `mcp-residual.txt` 的 unavailable
|
|
1235
|
+
/// 行,不阻断 spawn(provider 一期 subscription-only,工具链可能未完全就绪)。
|
|
1236
|
+
fn apply_copilot_mcp_residual_disables(
|
|
1237
|
+
workspace: &Path,
|
|
1238
|
+
agent_id: &str,
|
|
1239
|
+
argv: &mut Vec<String>,
|
|
1240
|
+
log_dir: &Path,
|
|
1241
|
+
) -> Result<(), LifecycleError> {
|
|
1242
|
+
let listing = std::process::Command::new("copilot")
|
|
1243
|
+
.arg("mcp")
|
|
1244
|
+
.arg("list")
|
|
1245
|
+
.output();
|
|
1246
|
+
let residual_path = log_dir.join("mcp-residual.txt");
|
|
1247
|
+
match listing {
|
|
1248
|
+
Ok(out) if out.status.success() => {
|
|
1249
|
+
let text = String::from_utf8_lossy(&out.stdout).to_string();
|
|
1250
|
+
std::fs::write(&residual_path, &text).map_err(|e| {
|
|
1251
|
+
LifecycleError::StatePersist(format!("{}: {e}", residual_path.display()))
|
|
1252
|
+
})?;
|
|
1253
|
+
let residual_servers = parse_copilot_mcp_list_server_names(&text);
|
|
1254
|
+
let non_orchestrator: Vec<String> = residual_servers
|
|
1255
|
+
.iter()
|
|
1256
|
+
.filter(|name| name.as_str() != "team_orchestrator")
|
|
1257
|
+
.cloned()
|
|
1258
|
+
.collect();
|
|
1259
|
+
for name in &non_orchestrator {
|
|
1260
|
+
argv.push("--disable-mcp-server".to_string());
|
|
1261
|
+
argv.push(name.clone());
|
|
1262
|
+
}
|
|
1263
|
+
if !non_orchestrator.is_empty() {
|
|
1264
|
+
let event_log = crate::event_log::EventLog::new(workspace);
|
|
1265
|
+
let _ = event_log.write(
|
|
1266
|
+
"provider.copilot.mcp_residual_detected",
|
|
1267
|
+
serde_json::json!({
|
|
1268
|
+
"agent_id": agent_id,
|
|
1269
|
+
"residual_servers": non_orchestrator,
|
|
1270
|
+
"log_path": residual_path.to_string_lossy(),
|
|
1271
|
+
}),
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
Ok(out) => {
|
|
1276
|
+
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
|
1277
|
+
std::fs::write(
|
|
1278
|
+
&residual_path,
|
|
1279
|
+
format!("copilot mcp list exit={:?} stderr={stderr}\n", out.status.code()),
|
|
1280
|
+
)
|
|
1281
|
+
.map_err(|e| {
|
|
1282
|
+
LifecycleError::StatePersist(format!("{}: {e}", residual_path.display()))
|
|
1283
|
+
})?;
|
|
1284
|
+
}
|
|
1285
|
+
Err(e) => {
|
|
1286
|
+
std::fs::write(
|
|
1287
|
+
&residual_path,
|
|
1288
|
+
format!("copilot mcp list unavailable: {e}\n"),
|
|
1289
|
+
)
|
|
1290
|
+
.map_err(|e| {
|
|
1291
|
+
LifecycleError::StatePersist(format!("{}: {e}", residual_path.display()))
|
|
1292
|
+
})?;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
Ok(())
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/// 解析 `copilot mcp list` 输出取 server 名集合(te 真机实证 v2,1.0.59 形态):
|
|
1299
|
+
/// ```text
|
|
1300
|
+
/// User servers:
|
|
1301
|
+
/// foo (local)
|
|
1302
|
+
/// bar (http)
|
|
1303
|
+
/// Builtin servers:
|
|
1304
|
+
/// github-mcp-server (local)
|
|
1305
|
+
/// ```
|
|
1306
|
+
/// 或空集形态(te 真机实证 fake HOME 无 mcp-config.json):
|
|
1307
|
+
/// ```text
|
|
1308
|
+
/// No MCP servers configured.
|
|
1309
|
+
///
|
|
1310
|
+
/// Add a server with:
|
|
1311
|
+
/// copilot mcp add <name> -- <command> [args...]
|
|
1312
|
+
/// copilot mcp add --transport http <name> <url>
|
|
1313
|
+
/// ```
|
|
1314
|
+
///
|
|
1315
|
+
/// 规则:
|
|
1316
|
+
/// 1. 首行含 "No MCP servers configured" → 立即返空(避免把 "Add a server with"
|
|
1317
|
+
/// 段下的 help 行误识为 server)
|
|
1318
|
+
/// 2. 段标题行(非缩进、以 `:` 结尾):只有 *servers:* 后缀的段(User/Builtin/
|
|
1319
|
+
/// Workspace servers:)才进 server-listing 模式;其余段(如 "Add a server with:")
|
|
1320
|
+
/// 进 skip 模式直到下个 servers: 段或文档结束
|
|
1321
|
+
/// 3. servers: 段内的缩进行取首段 token,剥 ` (local)`/` (http)`/` (sse)` 后缀
|
|
1322
|
+
/// 4. 空行 / 不识别行容忍跳过(诚实降级:漏识 = silent 残留,在 mcp-residual.txt
|
|
1323
|
+
/// 全量落盘留证)
|
|
1324
|
+
fn parse_copilot_mcp_list_server_names(text: &str) -> Vec<String> {
|
|
1325
|
+
let mut out: Vec<String> = Vec::new();
|
|
1326
|
+
let mut in_servers_section = false;
|
|
1327
|
+
for line in text.lines() {
|
|
1328
|
+
let trimmed_end = line.trim_end();
|
|
1329
|
+
if trimmed_end.is_empty() {
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
// C-3-2 fix(te 真机实证):空集 sentinel 立即返空。
|
|
1333
|
+
if trimmed_end
|
|
1334
|
+
.trim_start()
|
|
1335
|
+
.starts_with("No MCP servers configured")
|
|
1336
|
+
{
|
|
1337
|
+
return Vec::new();
|
|
1338
|
+
}
|
|
1339
|
+
// 段标题行(非缩进):决定后续缩进行是否取 server 名。"*servers:" 是
|
|
1340
|
+
// listing 段(User/Builtin/Workspace),其它段都 skip(如 "Add a server with:"
|
|
1341
|
+
// 下面的 help 命令缩进行)。
|
|
1342
|
+
if !(line.starts_with(' ') || line.starts_with('\t')) {
|
|
1343
|
+
let lower = trimmed_end.to_ascii_lowercase();
|
|
1344
|
+
in_servers_section = lower.trim_end_matches(':').ends_with("servers");
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
if !in_servers_section {
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
let trimmed = trimmed_end.trim_start();
|
|
1351
|
+
if trimmed.is_empty() {
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
let mut token = trimmed.split_whitespace().next().unwrap_or("").to_string();
|
|
1355
|
+
// 剥常见装饰后缀(实测形如 "(local)"/"(http)"/"(sse)" 是独立 whitespace
|
|
1356
|
+
// 分隔的 token,首段 token 通常不带括号;若实际 copilot 把括号粘连首段
|
|
1357
|
+
// token,这里多做一次后缀剥离守护)。
|
|
1358
|
+
if let Some(idx) = token.find('(') {
|
|
1359
|
+
token.truncate(idx);
|
|
1360
|
+
}
|
|
1361
|
+
token = token.trim_end_matches(':').trim().to_string();
|
|
1362
|
+
if token.is_empty() {
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
out.push(token);
|
|
1366
|
+
}
|
|
1367
|
+
out
|
|
1368
|
+
}
|
|
1369
|
+
|
|
974
1370
|
pub(crate) fn apply_profile_launch_env(
|
|
975
1371
|
env: &mut BTreeMap<String, String>,
|
|
976
1372
|
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
@@ -1078,7 +1474,10 @@ pub(crate) fn fill_spawn_placeholders_full(
|
|
|
1078
1474
|
*arg = workspace_text.clone();
|
|
1079
1475
|
} else if arg == "{agent_id}" {
|
|
1080
1476
|
*arg = agent_id.to_string();
|
|
1081
|
-
} else if arg.contains("{workspace}")
|
|
1477
|
+
} else if arg.contains("{workspace}")
|
|
1478
|
+
|| arg.contains("{agent_id}")
|
|
1479
|
+
|| arg.contains("{team_id}")
|
|
1480
|
+
{
|
|
1082
1481
|
*arg = arg
|
|
1083
1482
|
.replace("{workspace}", &workspace_text)
|
|
1084
1483
|
.replace("{agent_id}", agent_id)
|
|
@@ -1087,30 +1486,12 @@ pub(crate) fn fill_spawn_placeholders_full(
|
|
|
1087
1486
|
}
|
|
1088
1487
|
}
|
|
1089
1488
|
|
|
1090
|
-
fn agent_tool_strings(agent: &Value) -> Vec<String> {
|
|
1091
|
-
agent
|
|
1092
|
-
.get("tools")
|
|
1093
|
-
.and_then(Value::as_list)
|
|
1094
|
-
.map(|items| {
|
|
1095
|
-
items
|
|
1096
|
-
.iter()
|
|
1097
|
-
.filter_map(Value::as_str)
|
|
1098
|
-
.map(str::to_string)
|
|
1099
|
-
.collect()
|
|
1100
|
-
})
|
|
1101
|
-
.unwrap_or_default()
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
1489
|
fn spec_team_id(spec: &Value) -> Option<String> {
|
|
1105
1490
|
spec.get("team")
|
|
1106
1491
|
.and_then(|v| v.get("id").or_else(|| v.get("name")))
|
|
1107
1492
|
.and_then(Value::as_str)
|
|
1108
1493
|
.map(str::to_string)
|
|
1109
|
-
.or_else(||
|
|
1110
|
-
spec.get("name")
|
|
1111
|
-
.and_then(Value::as_str)
|
|
1112
|
-
.map(str::to_string)
|
|
1113
|
-
})
|
|
1494
|
+
.or_else(|| spec.get("name").and_then(Value::as_str).map(str::to_string))
|
|
1114
1495
|
}
|
|
1115
1496
|
|
|
1116
1497
|
fn runtime_active_team_key_for_spawn(
|
|
@@ -1158,6 +1539,7 @@ fn parse_provider(raw: &str) -> Option<Provider> {
|
|
|
1158
1539
|
"claude" => Some(Provider::Claude),
|
|
1159
1540
|
"claude_code" => Some(Provider::ClaudeCode),
|
|
1160
1541
|
"codex" => Some(Provider::Codex),
|
|
1542
|
+
"copilot" => Some(Provider::Copilot),
|
|
1161
1543
|
"gemini_cli" => Some(Provider::GeminiCli),
|
|
1162
1544
|
"fake" => Some(Provider::Fake),
|
|
1163
1545
|
_ => None,
|
|
@@ -1173,7 +1555,10 @@ fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
|
|
|
1173
1555
|
}
|
|
1174
1556
|
}
|
|
1175
1557
|
|
|
1176
|
-
fn quick_start_requested_team_key<'a>(
|
|
1558
|
+
fn quick_start_requested_team_key<'a>(
|
|
1559
|
+
team_id: Option<&'a str>,
|
|
1560
|
+
name: Option<&'a str>,
|
|
1561
|
+
) -> Option<&'a str> {
|
|
1177
1562
|
team_id.or(name).filter(|team| !team.is_empty())
|
|
1178
1563
|
}
|
|
1179
1564
|
|
|
@@ -1239,7 +1624,7 @@ fn quick_start_depth_guard(
|
|
|
1239
1624
|
Ok(QuickStartDepth {
|
|
1240
1625
|
parent_team_key: Some(parent_key),
|
|
1241
1626
|
team_depth,
|
|
1242
|
-
|
|
1627
|
+
})
|
|
1243
1628
|
}
|
|
1244
1629
|
|
|
1245
1630
|
fn infer_parent_team_from_active_state(state: &serde_json::Value) -> Option<String> {
|
|
@@ -1255,9 +1640,7 @@ fn has_live_runtime_teams(state: &serde_json::Value) -> bool {
|
|
|
1255
1640
|
state
|
|
1256
1641
|
.get("teams")
|
|
1257
1642
|
.and_then(serde_json::Value::as_object)
|
|
1258
|
-
.is_some_and(|teams|
|
|
1259
|
-
teams.values().any(team_has_running_agent)
|
|
1260
|
-
})
|
|
1643
|
+
.is_some_and(|teams| teams.values().any(team_has_running_agent))
|
|
1261
1644
|
}
|
|
1262
1645
|
|
|
1263
1646
|
fn team_has_running_agent(team: &serde_json::Value) -> bool {
|
|
@@ -1265,20 +1648,18 @@ fn team_has_running_agent(team: &serde_json::Value) -> bool {
|
|
|
1265
1648
|
.and_then(serde_json::Value::as_object)
|
|
1266
1649
|
.is_some_and(|agents| {
|
|
1267
1650
|
agents.values().any(|agent| {
|
|
1268
|
-
agent
|
|
1269
|
-
.get("status")
|
|
1270
|
-
.and_then(serde_json::Value::as_str)
|
|
1271
|
-
== Some("running")
|
|
1651
|
+
agent.get("status").and_then(serde_json::Value::as_str) == Some("running")
|
|
1272
1652
|
})
|
|
1273
1653
|
})
|
|
1274
1654
|
}
|
|
1275
1655
|
|
|
1276
1656
|
fn looks_ambiguous_child_team_key(team: &str) -> bool {
|
|
1277
1657
|
let team = team.trim().to_ascii_lowercase();
|
|
1278
|
-
team != "child"
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1658
|
+
team != "child"
|
|
1659
|
+
&& (team.starts_with("child-")
|
|
1660
|
+
|| team.starts_with("child_")
|
|
1661
|
+
|| team.starts_with("child.")
|
|
1662
|
+
|| team.starts_with("child"))
|
|
1282
1663
|
}
|
|
1283
1664
|
|
|
1284
1665
|
fn looks_grandchild_team_key(team: &str) -> bool {
|
|
@@ -1290,7 +1671,11 @@ fn looks_grandchild_team_key(team: &str) -> bool {
|
|
|
1290
1671
|
|| team.starts_with("grandchild")
|
|
1291
1672
|
}
|
|
1292
1673
|
|
|
1293
|
-
fn annotate_team_depth(
|
|
1674
|
+
fn annotate_team_depth(
|
|
1675
|
+
state: &mut serde_json::Value,
|
|
1676
|
+
parent_team_key: Option<&str>,
|
|
1677
|
+
team_depth: u64,
|
|
1678
|
+
) {
|
|
1294
1679
|
let Some(obj) = state.as_object_mut() else {
|
|
1295
1680
|
return;
|
|
1296
1681
|
};
|
|
@@ -1337,9 +1722,7 @@ fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) ->
|
|
|
1337
1722
|
|| state
|
|
1338
1723
|
.get("session_name")
|
|
1339
1724
|
.and_then(serde_json::Value::as_str)
|
|
1340
|
-
.is_some_and(|session|
|
|
1341
|
-
session == team || session.strip_prefix("team-") == Some(team)
|
|
1342
|
-
})
|
|
1725
|
+
.is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
|
|
1343
1726
|
}
|
|
1344
1727
|
|
|
1345
1728
|
fn json_team_identity_matches(state: &serde_json::Value, team: &str) -> bool {
|
|
@@ -1412,7 +1795,9 @@ pub fn quick_start_with_transport(
|
|
|
1412
1795
|
transport: &dyn Transport,
|
|
1413
1796
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
1414
1797
|
let workspace = team_workspace(agents_dir);
|
|
1415
|
-
quick_start_with_transport_in_workspace(
|
|
1798
|
+
quick_start_with_transport_in_workspace(
|
|
1799
|
+
&workspace, agents_dir, name, yes, fresh, team_id, transport,
|
|
1800
|
+
)
|
|
1416
1801
|
}
|
|
1417
1802
|
|
|
1418
1803
|
pub fn quick_start_with_transport_in_workspace(
|
|
@@ -1468,11 +1853,12 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1468
1853
|
.map(SessionName::new),
|
|
1469
1854
|
state_path: Some(state_path),
|
|
1470
1855
|
next_actions: vec![
|
|
1471
|
-
"run restart to resume the existing team or pass --fresh to replace it"
|
|
1856
|
+
"run restart to resume the existing team or pass --fresh to replace it"
|
|
1857
|
+
.to_string(),
|
|
1472
1858
|
],
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
}
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1476
1862
|
}
|
|
1477
1863
|
// CR-040/042: repeated quick-start from one template with distinct --team-id/--name
|
|
1478
1864
|
// must NOT collide on the template-derived tmux session. Override the compiled
|
|
@@ -1488,12 +1874,12 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1488
1874
|
runtime_team_key_for_spec(&spec_path, &spec, &session_name)
|
|
1489
1875
|
});
|
|
1490
1876
|
let spec_path = agents_dir.join("team.spec.yaml");
|
|
1491
|
-
std::fs::write(&spec_path, yaml::dumps(&spec))
|
|
1492
|
-
LifecycleError::StatePersist(format!("{}: {e}", spec_path.display()))
|
|
1493
|
-
})?;
|
|
1877
|
+
std::fs::write(&spec_path, yaml::dumps(&spec))
|
|
1878
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", spec_path.display())))?;
|
|
1494
1879
|
let _store = crate::message_store::MessageStore::open(&workspace)
|
|
1495
1880
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1496
|
-
let resolved_spec_path =
|
|
1881
|
+
let resolved_spec_path =
|
|
1882
|
+
std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
|
|
1497
1883
|
let state = initial_runtime_state(&spec, &resolved_spec_path, &workspace, agents_dir);
|
|
1498
1884
|
save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
|
|
1499
1885
|
annotate_persisted_team_depth(
|
|
@@ -1505,14 +1891,16 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1505
1891
|
// FIX (rt-host-a real-machine finding): dry_run=false so launch_with_transport calls spawn_agents
|
|
1506
1892
|
// and really creates the tmux session + worker windows (was hardcoded true → never spawned, which
|
|
1507
1893
|
// also starved the coordinator: no session → first tick TmuxSessionMissing → run_daemon loop exits).
|
|
1508
|
-
let mut launch =
|
|
1894
|
+
let mut launch =
|
|
1895
|
+
launch_with_transport_in_workspace(&workspace, &spec_path, false, yes, true, transport)?;
|
|
1509
1896
|
annotate_persisted_team_depth(
|
|
1510
1897
|
&workspace,
|
|
1511
1898
|
&state_team_key,
|
|
1512
1899
|
team_depth.parent_team_key.as_deref(),
|
|
1513
1900
|
team_depth.team_depth,
|
|
1514
1901
|
)?;
|
|
1515
|
-
launch.leader_receiver_attached =
|
|
1902
|
+
launch.leader_receiver_attached =
|
|
1903
|
+
launched_team_receiver_is_attached(&workspace, &state_team_key);
|
|
1516
1904
|
launch.session_capture_incomplete_agents =
|
|
1517
1905
|
quick_start_session_capture_incomplete_agents(&workspace, &state_team_key);
|
|
1518
1906
|
let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.clone());
|
|
@@ -1532,12 +1920,29 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1532
1920
|
// asynchronously after spawn), so the verdict is PendingToolLoad — never
|
|
1533
1921
|
// bare Ready.
|
|
1534
1922
|
let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
|
|
1923
|
+
let attach_commands = crate::tmux_backend::attach_commands_for_windows(
|
|
1924
|
+
&workspace,
|
|
1925
|
+
&session_name,
|
|
1926
|
+
launch
|
|
1927
|
+
.started
|
|
1928
|
+
.iter()
|
|
1929
|
+
.map(|started| started.agent_id.as_str()),
|
|
1930
|
+
);
|
|
1931
|
+
let mut next_actions = vec![format!(
|
|
1932
|
+
"team compiled; real spawn is behind the transport/provider boundary; {coordinator_action}"
|
|
1933
|
+
)];
|
|
1934
|
+
next_actions.extend(attach_commands.iter().cloned());
|
|
1935
|
+
let display_backend = state
|
|
1936
|
+
.get("display_backend")
|
|
1937
|
+
.and_then(serde_json::Value::as_str)
|
|
1938
|
+
.unwrap_or("none")
|
|
1939
|
+
.to_string();
|
|
1535
1940
|
Ok(QuickStartReport::Ready {
|
|
1536
1941
|
session_name,
|
|
1537
1942
|
launch: Box::new(launch),
|
|
1538
|
-
next_actions
|
|
1539
|
-
|
|
1540
|
-
|
|
1943
|
+
next_actions,
|
|
1944
|
+
attach_commands,
|
|
1945
|
+
display_backend,
|
|
1541
1946
|
worker_readiness,
|
|
1542
1947
|
})
|
|
1543
1948
|
}
|
|
@@ -1556,7 +1961,10 @@ fn quick_start_worker_readiness(workspace: &Path, team_key: &str) -> QuickStartR
|
|
|
1556
1961
|
.and_then(serde_json::Value::as_object)
|
|
1557
1962
|
.and_then(|teams| teams.get(team_key))
|
|
1558
1963
|
.unwrap_or(&state);
|
|
1559
|
-
let Some(agents) = team_state
|
|
1964
|
+
let Some(agents) = team_state
|
|
1965
|
+
.get("agents")
|
|
1966
|
+
.and_then(serde_json::Value::as_object)
|
|
1967
|
+
else {
|
|
1560
1968
|
return QuickStartReadiness::PendingToolLoad;
|
|
1561
1969
|
};
|
|
1562
1970
|
let all_spawned = !agents.is_empty();
|
|
@@ -1575,9 +1983,12 @@ fn quick_start_worker_readiness(workspace: &Path, team_key: &str) -> QuickStartR
|
|
|
1575
1983
|
if !unhealthy.is_empty() {
|
|
1576
1984
|
unhealthy.sort();
|
|
1577
1985
|
unhealthy.dedup();
|
|
1578
|
-
QuickStartReadiness::Degraded {
|
|
1986
|
+
QuickStartReadiness::Degraded {
|
|
1987
|
+
unhealthy_agents: unhealthy,
|
|
1988
|
+
}
|
|
1579
1989
|
} else {
|
|
1580
|
-
let incomplete_agents =
|
|
1990
|
+
let incomplete_agents =
|
|
1991
|
+
crate::session_capture::incomplete_interacted_resumable_agent_ids(team_state);
|
|
1581
1992
|
let all_resumable_have_session = incomplete_agents.is_empty();
|
|
1582
1993
|
let _readiness_ready = all_spawned && all_attached_receiver && all_resumable_have_session;
|
|
1583
1994
|
QuickStartReadiness::PendingToolLoad
|
|
@@ -1621,10 +2032,7 @@ fn team_uses_fake_model_harness(team_state: &serde_json::Value) -> bool {
|
|
|
1621
2032
|
.is_some_and(|agents| {
|
|
1622
2033
|
!agents.is_empty()
|
|
1623
2034
|
&& agents.values().all(|agent| {
|
|
1624
|
-
agent
|
|
1625
|
-
.get("model")
|
|
1626
|
-
.and_then(serde_json::Value::as_str)
|
|
1627
|
-
== Some("fake")
|
|
2035
|
+
agent.get("model").and_then(serde_json::Value::as_str) == Some("fake")
|
|
1628
2036
|
})
|
|
1629
2037
|
})
|
|
1630
2038
|
}
|
|
@@ -1649,9 +2057,11 @@ fn leader_receiver_is_attached(team_state: &serde_json::Value) -> bool {
|
|
|
1649
2057
|
/// `--dangerously-*` flag,产出危险审批继承态。launch 在 inherited=false 且无 --yes 时拒。
|
|
1650
2058
|
pub fn detect_dangerous_approval() -> Result<DangerousApproval, LifecycleError> {
|
|
1651
2059
|
if let Ok(raw) = std::env::var("TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON") {
|
|
1652
|
-
let argv_tokens = serde_json::from_str::<Vec<String>>(&raw)
|
|
1653
|
-
|
|
1654
|
-
|
|
2060
|
+
let argv_tokens = serde_json::from_str::<Vec<String>>(&raw).map_err(|e| {
|
|
2061
|
+
LifecycleError::StatePersist(format!("invalid test ancestry argv: {e}"))
|
|
2062
|
+
})?;
|
|
2063
|
+
return Ok(detect_dangerous_approval_in_argv(&argv_tokens)
|
|
2064
|
+
.unwrap_or_else(disabled_dangerous_approval));
|
|
1655
2065
|
}
|
|
1656
2066
|
for argv_tokens in process_ancestry_argv(std::process::id()) {
|
|
1657
2067
|
if let Some(detected) = detect_dangerous_approval_in_argv(&argv_tokens) {
|
|
@@ -1667,7 +2077,8 @@ fn detect_dangerous_approval_in_argv(argv_tokens: &[String]) -> Option<Dangerous
|
|
|
1667
2077
|
for token in argv_tokens {
|
|
1668
2078
|
for (provider, flag) in dangerous_leader_flags() {
|
|
1669
2079
|
if token == flag {
|
|
1670
|
-
let unexpected_binary =
|
|
2080
|
+
let unexpected_binary =
|
|
2081
|
+
!binary_matches_provider(provider, ancestry_binary_name.as_deref());
|
|
1671
2082
|
return Some(DangerousApproval {
|
|
1672
2083
|
enabled: true,
|
|
1673
2084
|
source: DangerousApprovalSource::LeaderProcess,
|
|
@@ -1857,9 +2268,9 @@ pub fn add_agent(
|
|
|
1857
2268
|
}
|
|
1858
2269
|
Err(error) => return Err(LifecycleError::TeamSelect(error.to_string())),
|
|
1859
2270
|
};
|
|
1860
|
-
let team_dir = selected
|
|
1861
|
-
.
|
|
1862
|
-
|
|
2271
|
+
let team_dir = selected.spec_workspace.ok_or_else(|| {
|
|
2272
|
+
LifecycleError::TeamSelect("active team spec workspace not found".to_string())
|
|
2273
|
+
})?;
|
|
1863
2274
|
add_agent_with_transport_at_paths(
|
|
1864
2275
|
&selected.run_workspace,
|
|
1865
2276
|
&team_dir,
|
|
@@ -1910,8 +2321,9 @@ fn add_agent_with_transport_at_paths(
|
|
|
1910
2321
|
.map(str::to_string)
|
|
1911
2322
|
.or_else(|| explicit_active_team_key(&runtime_state))
|
|
1912
2323
|
.unwrap_or_else(|| crate::state::projection::team_state_key(&runtime_state));
|
|
1913
|
-
let owner_state =
|
|
1914
|
-
|
|
2324
|
+
let owner_state =
|
|
2325
|
+
crate::state::projection::select_runtime_state(run_workspace, Some(&canonical_team_key))
|
|
2326
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
1915
2327
|
ensure_owner_allowed_for_state(&owner_state, Some(agent_id))?;
|
|
1916
2328
|
if !role_file_path.exists() {
|
|
1917
2329
|
return Err(LifecycleError::Compile(format!(
|
|
@@ -1929,9 +2341,8 @@ fn add_agent_with_transport_at_paths(
|
|
|
1929
2341
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1930
2342
|
let safety = effective_runtime_config(&spec)?;
|
|
1931
2343
|
let spec_path = team_dir.join("team.spec.yaml");
|
|
1932
|
-
std::fs::write(&spec_path, yaml::dumps(&spec))
|
|
1933
|
-
LifecycleError::StatePersist(format!("{}: {e}", spec_path.display()))
|
|
1934
|
-
})?;
|
|
2344
|
+
std::fs::write(&spec_path, yaml::dumps(&spec))
|
|
2345
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", spec_path.display())))?;
|
|
1935
2346
|
let (meta, _) = crate::compiler::read_front_matter(&dynamic_role_file)
|
|
1936
2347
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1937
2348
|
upsert_agent_state_from_role(
|
|
@@ -1978,8 +2389,9 @@ fn upsert_agent_state_from_role(
|
|
|
1978
2389
|
dynamic_role_file: &Path,
|
|
1979
2390
|
safety: &DangerousApproval,
|
|
1980
2391
|
) -> Result<(), LifecycleError> {
|
|
1981
|
-
let mut state =
|
|
1982
|
-
|
|
2392
|
+
let mut state =
|
|
2393
|
+
crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
|
|
2394
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
1983
2395
|
if !state.is_object() {
|
|
1984
2396
|
state = serde_json::json!({});
|
|
1985
2397
|
}
|
|
@@ -2027,10 +2439,7 @@ fn upsert_agent_state_from_role(
|
|
|
2027
2439
|
if let Some(profile) = meta.get("profile").and_then(Value::as_str) {
|
|
2028
2440
|
if let Some(obj) = entry.as_object_mut() {
|
|
2029
2441
|
obj.insert("profile".to_string(), serde_json::json!(profile));
|
|
2030
|
-
if let Some(team_dir) = dynamic_role_file
|
|
2031
|
-
.parent()
|
|
2032
|
-
.and_then(Path::parent)
|
|
2033
|
-
{
|
|
2442
|
+
if let Some(team_dir) = dynamic_role_file.parent().and_then(Path::parent) {
|
|
2034
2443
|
obj.insert(
|
|
2035
2444
|
"_profile_dir".to_string(),
|
|
2036
2445
|
serde_json::json!(team_dir.join("profiles").to_string_lossy().to_string()),
|
|
@@ -2077,6 +2486,7 @@ pub fn fork_agent(
|
|
|
2077
2486
|
workspace: &Path,
|
|
2078
2487
|
source_agent_id: &AgentId,
|
|
2079
2488
|
as_agent_id: &AgentId,
|
|
2489
|
+
label: Option<&str>,
|
|
2080
2490
|
open_display: bool,
|
|
2081
2491
|
team: Option<&str>,
|
|
2082
2492
|
) -> Result<ForkAgentReport, LifecycleError> {
|
|
@@ -2090,6 +2500,7 @@ pub fn fork_agent(
|
|
|
2090
2500
|
workspace,
|
|
2091
2501
|
source_agent_id,
|
|
2092
2502
|
as_agent_id,
|
|
2503
|
+
label,
|
|
2093
2504
|
open_display,
|
|
2094
2505
|
team,
|
|
2095
2506
|
&crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace),
|
|
@@ -2100,6 +2511,7 @@ pub fn fork_agent_with_transport(
|
|
|
2100
2511
|
workspace: &Path,
|
|
2101
2512
|
source_agent_id: &AgentId,
|
|
2102
2513
|
as_agent_id: &AgentId,
|
|
2514
|
+
label: Option<&str>,
|
|
2103
2515
|
open_display: bool,
|
|
2104
2516
|
team: Option<&str>,
|
|
2105
2517
|
transport: &dyn Transport,
|
|
@@ -2111,9 +2523,9 @@ pub fn fork_agent_with_transport(
|
|
|
2111
2523
|
crate::state::selector::SelectorMode::RequireSpec,
|
|
2112
2524
|
)
|
|
2113
2525
|
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
2114
|
-
let spec_workspace = selected
|
|
2115
|
-
.
|
|
2116
|
-
|
|
2526
|
+
let spec_workspace = selected.spec_workspace.ok_or_else(|| {
|
|
2527
|
+
LifecycleError::TeamSelect("active team spec workspace not found".to_string())
|
|
2528
|
+
})?;
|
|
2117
2529
|
let workspace = selected.run_workspace;
|
|
2118
2530
|
let state = selected.state;
|
|
2119
2531
|
ensure_owner_allowed_for_state(&state, Some(source_agent_id))?;
|
|
@@ -2126,8 +2538,9 @@ pub fn fork_agent_with_transport(
|
|
|
2126
2538
|
"agent id already exists: {as_agent_id}"
|
|
2127
2539
|
)));
|
|
2128
2540
|
}
|
|
2129
|
-
let source_agent = find_spec_agent(&spec, source_agent_id)
|
|
2130
|
-
|
|
2541
|
+
let source_agent = find_spec_agent(&spec, source_agent_id).ok_or_else(|| {
|
|
2542
|
+
LifecycleError::RequirementUnmet(format!("unknown worker agent id: {source_agent_id}"))
|
|
2543
|
+
})?;
|
|
2131
2544
|
let session_id = state
|
|
2132
2545
|
.get("agents")
|
|
2133
2546
|
.and_then(|v| v.get(source_agent_id.as_str()))
|
|
@@ -2157,13 +2570,14 @@ pub fn fork_agent_with_transport(
|
|
|
2157
2570
|
as_agent_id.as_str()
|
|
2158
2571
|
)));
|
|
2159
2572
|
}
|
|
2160
|
-
let new_spec = append_forked_agent(&spec, source_agent, source_agent_id, as_agent_id)?;
|
|
2573
|
+
let new_spec = append_forked_agent(&spec, source_agent, source_agent_id, as_agent_id, label)?;
|
|
2161
2574
|
crate::model::spec::validate_spec(&new_spec, &spec_workspace)
|
|
2162
2575
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
2163
2576
|
std::fs::write(&spec_path, yaml::dumps(&new_spec))
|
|
2164
2577
|
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", spec_path.display())))?;
|
|
2165
|
-
let new_agent = find_spec_agent(&new_spec, as_agent_id)
|
|
2166
|
-
|
|
2578
|
+
let new_agent = find_spec_agent(&new_spec, as_agent_id).ok_or_else(|| {
|
|
2579
|
+
LifecycleError::RequirementUnmet(format!("unknown worker agent id: {as_agent_id}"))
|
|
2580
|
+
})?;
|
|
2167
2581
|
let provider = new_agent
|
|
2168
2582
|
.get("provider")
|
|
2169
2583
|
.and_then(Value::as_str)
|
|
@@ -2185,46 +2599,56 @@ pub fn fork_agent_with_transport(
|
|
|
2185
2599
|
"{provider_str} does not support native session fork"
|
|
2186
2600
|
)));
|
|
2187
2601
|
}
|
|
2188
|
-
let role = new_agent.get("role").and_then(Value::as_str);
|
|
2189
2602
|
let model = new_agent.get("model").and_then(Value::as_str);
|
|
2190
2603
|
let safety = effective_runtime_config(&new_spec)?;
|
|
2191
|
-
let
|
|
2192
|
-
|
|
2604
|
+
let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
|
|
2605
|
+
new_agent,
|
|
2606
|
+
Some(as_agent_id.as_str()),
|
|
2607
|
+
provider,
|
|
2608
|
+
);
|
|
2609
|
+
let system_prompt =
|
|
2610
|
+
crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
|
|
2611
|
+
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
2612
|
+
&command_agent,
|
|
2613
|
+
provider,
|
|
2614
|
+
&safety,
|
|
2615
|
+
)?;
|
|
2616
|
+
let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
2193
2617
|
let fork_team = crate::messaging::leader_receiver::active_team_key(&workspace, &state);
|
|
2194
|
-
let mcp_config = adapter
|
|
2195
|
-
.
|
|
2196
|
-
.
|
|
2197
|
-
|
|
2198
|
-
LifecycleError::Provider(e.to_string())
|
|
2199
|
-
})?;
|
|
2618
|
+
let mcp_config = adapter.mcp_config(auth_mode).map_err(|e| {
|
|
2619
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
2620
|
+
LifecycleError::Provider(e.to_string())
|
|
2621
|
+
})?;
|
|
2200
2622
|
let mcp_config = resolve_mcp_config(mcp_config, &workspace, as_agent_id.as_str(), &fork_team);
|
|
2201
|
-
let mcp_config_path =
|
|
2202
|
-
.map_err(|e| {
|
|
2203
|
-
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
2204
|
-
e
|
|
2205
|
-
})?;
|
|
2206
|
-
let profile_dir = spec_workspace.join("profiles");
|
|
2207
|
-
let profile_launch = crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
|
|
2623
|
+
let mcp_config_path = write_worker_mcp_config_for_provider(
|
|
2208
2624
|
&workspace,
|
|
2209
2625
|
as_agent_id.as_str(),
|
|
2210
|
-
|
|
2211
|
-
Some(
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2626
|
+
&mcp_config,
|
|
2627
|
+
Some(provider),
|
|
2628
|
+
)
|
|
2629
|
+
.map_err(|e| {
|
|
2630
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
2631
|
+
e
|
|
2632
|
+
})?;
|
|
2633
|
+
let profile_dir = spec_workspace.join("profiles");
|
|
2634
|
+
let profile_launch =
|
|
2635
|
+
crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
|
|
2636
|
+
&workspace,
|
|
2637
|
+
as_agent_id.as_str(),
|
|
2638
|
+
new_agent,
|
|
2639
|
+
Some(&profile_dir),
|
|
2640
|
+
Some(&mcp_config),
|
|
2641
|
+
)?;
|
|
2642
|
+
let command_model = profile_launch.command_overrides.model.as_deref().or(model);
|
|
2219
2643
|
let mut plan = adapter
|
|
2220
2644
|
.fork_plan(
|
|
2221
2645
|
Some(&session_id),
|
|
2222
2646
|
crate::provider::ProviderCommandContext {
|
|
2223
2647
|
auth_mode,
|
|
2224
2648
|
mcp_config: Some(&mcp_config),
|
|
2225
|
-
system_prompt:
|
|
2649
|
+
system_prompt: Some(system_prompt.as_str()),
|
|
2226
2650
|
model: command_model,
|
|
2227
|
-
tools: &
|
|
2651
|
+
tools: &resolved_tool_refs,
|
|
2228
2652
|
profile_launch: Some(&profile_launch),
|
|
2229
2653
|
},
|
|
2230
2654
|
)
|
|
@@ -2235,23 +2659,40 @@ pub fn fork_agent_with_transport(
|
|
|
2235
2659
|
if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
|
|
2236
2660
|
point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
|
|
2237
2661
|
}
|
|
2238
|
-
fill_spawn_placeholders_full(
|
|
2239
|
-
|
|
2240
|
-
// fork inherits the parent agent's owner team via runtime state (`active_team_key`).
|
|
2241
|
-
let mut env = inherited_env_with_team_overrides(
|
|
2662
|
+
fill_spawn_placeholders_full(
|
|
2663
|
+
&mut plan.argv,
|
|
2242
2664
|
&workspace,
|
|
2243
2665
|
as_agent_id.as_str(),
|
|
2244
2666
|
Some(&fork_team),
|
|
2245
2667
|
);
|
|
2668
|
+
let window = WindowName::new(as_agent_id.as_str());
|
|
2669
|
+
// fork inherits the parent agent's owner team via runtime state (`active_team_key`).
|
|
2670
|
+
let mut env =
|
|
2671
|
+
inherited_env_with_team_overrides(&workspace, as_agent_id.as_str(), Some(&fork_team));
|
|
2246
2672
|
apply_profile_launch_env(&mut env, &profile_launch);
|
|
2247
2673
|
// golden operations.py:336 -> _tmux_start_command_for_agent_window (runtime.py:1017-1020): branch on
|
|
2248
2674
|
// _tmux_session_exists — an ABSENT session => new-session (spawn_first), present => new-window
|
|
2249
2675
|
// (spawn_into). The Rust restart seam (restart.rs spawn_agent_window) uses the same branch.
|
|
2250
2676
|
let session_live = transport.has_session(&session_name).unwrap_or(false);
|
|
2677
|
+
let env_unset: Vec<String> = profile_launch.env_unset.iter().cloned().collect();
|
|
2251
2678
|
let spawn_result = if session_live {
|
|
2252
|
-
transport.
|
|
2679
|
+
transport.spawn_into_with_env_unset(
|
|
2680
|
+
&session_name,
|
|
2681
|
+
&window,
|
|
2682
|
+
&plan.argv,
|
|
2683
|
+
&workspace,
|
|
2684
|
+
&env,
|
|
2685
|
+
&env_unset,
|
|
2686
|
+
)
|
|
2253
2687
|
} else {
|
|
2254
|
-
transport.
|
|
2688
|
+
transport.spawn_first_with_env_unset(
|
|
2689
|
+
&session_name,
|
|
2690
|
+
&window,
|
|
2691
|
+
&plan.argv,
|
|
2692
|
+
&workspace,
|
|
2693
|
+
&env,
|
|
2694
|
+
&env_unset,
|
|
2695
|
+
)
|
|
2255
2696
|
};
|
|
2256
2697
|
let spawn = spawn_result.map_err(|e| {
|
|
2257
2698
|
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
@@ -2324,26 +2765,25 @@ pub fn fork_agent_with_transport(
|
|
|
2324
2765
|
);
|
|
2325
2766
|
return Err(e);
|
|
2326
2767
|
}
|
|
2327
|
-
let coordinator_started =
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
})?;
|
|
2768
|
+
let coordinator_started = crate::coordinator::start_coordinator(
|
|
2769
|
+
&crate::coordinator::WorkspacePath::new(workspace.to_path_buf()),
|
|
2770
|
+
)
|
|
2771
|
+
.map(|report| report.ok)
|
|
2772
|
+
.map_err(|e| {
|
|
2773
|
+
rollback_fork_after_spawn(
|
|
2774
|
+
&workspace,
|
|
2775
|
+
&spec_path,
|
|
2776
|
+
&text,
|
|
2777
|
+
&old_state,
|
|
2778
|
+
transport,
|
|
2779
|
+
&session_name,
|
|
2780
|
+
&window,
|
|
2781
|
+
&mcp_config_path,
|
|
2782
|
+
as_agent_id,
|
|
2783
|
+
&profile_launch,
|
|
2784
|
+
);
|
|
2785
|
+
LifecycleError::StatePersist(e.to_string())
|
|
2786
|
+
})?;
|
|
2347
2787
|
Ok(ForkAgentReport {
|
|
2348
2788
|
source_agent_id: source_agent_id.clone(),
|
|
2349
2789
|
new_agent_id: as_agent_id.clone(),
|
|
@@ -2384,8 +2824,7 @@ fn maybe_fail_fork_after_spawn(step: &str) -> Result<(), LifecycleError> {
|
|
|
2384
2824
|
if reason.is_empty() {
|
|
2385
2825
|
return Ok(());
|
|
2386
2826
|
}
|
|
2387
|
-
let should_fail = reason == step
|
|
2388
|
-
|| (step == "start_coordinator" && reason == "coordinator");
|
|
2827
|
+
let should_fail = reason == step || (step == "start_coordinator" && reason == "coordinator");
|
|
2389
2828
|
if !should_fail {
|
|
2390
2829
|
return Ok(());
|
|
2391
2830
|
}
|
|
@@ -2429,16 +2868,13 @@ fn find_spec_agent<'a>(spec: &'a Value, agent_id: &AgentId) -> Option<&'a Value>
|
|
|
2429
2868
|
if leader_is_agent {
|
|
2430
2869
|
return None;
|
|
2431
2870
|
}
|
|
2432
|
-
spec.get("agents")
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
.map(|id| id == agent_id.as_str())
|
|
2440
|
-
.unwrap_or(false)
|
|
2441
|
-
})
|
|
2871
|
+
spec.get("agents")?.as_list()?.iter().find(|agent| {
|
|
2872
|
+
agent
|
|
2873
|
+
.get("id")
|
|
2874
|
+
.and_then(Value::as_str)
|
|
2875
|
+
.map(|id| id == agent_id.as_str())
|
|
2876
|
+
.unwrap_or(false)
|
|
2877
|
+
})
|
|
2442
2878
|
}
|
|
2443
2879
|
|
|
2444
2880
|
fn append_forked_agent(
|
|
@@ -2446,6 +2882,7 @@ fn append_forked_agent(
|
|
|
2446
2882
|
source_agent: &Value,
|
|
2447
2883
|
source_agent_id: &AgentId,
|
|
2448
2884
|
as_agent_id: &AgentId,
|
|
2885
|
+
label: Option<&str>,
|
|
2449
2886
|
) -> Result<Value, LifecycleError> {
|
|
2450
2887
|
let mut new_agent = source_agent.clone();
|
|
2451
2888
|
set_yaml_map_value(
|
|
@@ -2454,11 +2891,16 @@ fn append_forked_agent(
|
|
|
2454
2891
|
Value::Str(as_agent_id.as_str().to_string()),
|
|
2455
2892
|
)?;
|
|
2456
2893
|
// golden operations.py:315 `str(label or new_agent.get("role") or as_agent_id)` — Python `or`
|
|
2457
|
-
// falsiness: an EMPTY-string role is falsy and falls through to
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
.and_then(Value::as_str)
|
|
2894
|
+
// falsiness: an EMPTY-string label/role is falsy and falls through to the next tier.
|
|
2895
|
+
// The label IS the forked agent's new role (it feeds the identity prompt — B2 family).
|
|
2896
|
+
let role = label
|
|
2461
2897
|
.filter(|s| !s.is_empty())
|
|
2898
|
+
.or_else(|| {
|
|
2899
|
+
new_agent
|
|
2900
|
+
.get("role")
|
|
2901
|
+
.and_then(Value::as_str)
|
|
2902
|
+
.filter(|s| !s.is_empty())
|
|
2903
|
+
})
|
|
2462
2904
|
.unwrap_or_else(|| as_agent_id.as_str())
|
|
2463
2905
|
.to_string();
|
|
2464
2906
|
set_yaml_map_value(&mut new_agent, "role", Value::Str(role.clone()))?;
|
|
@@ -2477,12 +2919,17 @@ fn append_forked_agent(
|
|
|
2477
2919
|
)?;
|
|
2478
2920
|
|
|
2479
2921
|
let Value::Map(pairs) = spec else {
|
|
2480
|
-
return Err(LifecycleError::Compile(
|
|
2922
|
+
return Err(LifecycleError::Compile(
|
|
2923
|
+
"spec root is not a map".to_string(),
|
|
2924
|
+
));
|
|
2481
2925
|
};
|
|
2482
2926
|
let mut out = Vec::new();
|
|
2483
2927
|
for (key, value) in pairs {
|
|
2484
2928
|
if key == "agents" {
|
|
2485
|
-
let mut agents = value
|
|
2929
|
+
let mut agents = value
|
|
2930
|
+
.as_list()
|
|
2931
|
+
.map(|items| items.to_vec())
|
|
2932
|
+
.unwrap_or_default();
|
|
2486
2933
|
agents.push(new_agent.clone());
|
|
2487
2934
|
out.push((key.clone(), Value::List(agents)));
|
|
2488
2935
|
} else if key == "runtime" {
|
|
@@ -2496,7 +2943,9 @@ fn append_forked_agent(
|
|
|
2496
2943
|
|
|
2497
2944
|
fn set_yaml_map_value(value: &mut Value, key: &str, next: Value) -> Result<(), LifecycleError> {
|
|
2498
2945
|
let Value::Map(pairs) = value else {
|
|
2499
|
-
return Err(LifecycleError::Compile(
|
|
2946
|
+
return Err(LifecycleError::Compile(
|
|
2947
|
+
"agent entry is not a map".to_string(),
|
|
2948
|
+
));
|
|
2500
2949
|
};
|
|
2501
2950
|
if let Some((_, existing)) = pairs.iter_mut().find(|(k, _)| k == key) {
|
|
2502
2951
|
*existing = next;
|
|
@@ -2515,10 +2964,15 @@ fn runtime_with_startup_agent(runtime: &Value, agent_id: &AgentId) -> Value {
|
|
|
2515
2964
|
for (key, value) in pairs {
|
|
2516
2965
|
if key == "startup_order" {
|
|
2517
2966
|
saw_startup = true;
|
|
2518
|
-
let mut order = value
|
|
2519
|
-
|
|
2520
|
-
.
|
|
2521
|
-
.
|
|
2967
|
+
let mut order = value
|
|
2968
|
+
.as_list()
|
|
2969
|
+
.map(|items| items.to_vec())
|
|
2970
|
+
.unwrap_or_default();
|
|
2971
|
+
let already_present = order.iter().any(|item| {
|
|
2972
|
+
item.as_str()
|
|
2973
|
+
.map(|id| id == agent_id.as_str())
|
|
2974
|
+
.unwrap_or(false)
|
|
2975
|
+
});
|
|
2522
2976
|
if !already_present {
|
|
2523
2977
|
order.push(Value::Str(agent_id.as_str().to_string()));
|
|
2524
2978
|
}
|
|
@@ -2574,18 +3028,37 @@ fn upsert_forked_agent_state(
|
|
|
2574
3028
|
let mut entry = serde_json::Map::new();
|
|
2575
3029
|
entry.insert("status".to_string(), serde_json::json!("running"));
|
|
2576
3030
|
entry.insert("provider".to_string(), serde_json::json!(provider));
|
|
2577
|
-
entry.insert(
|
|
2578
|
-
|
|
2579
|
-
|
|
3031
|
+
entry.insert(
|
|
3032
|
+
"agent_id".to_string(),
|
|
3033
|
+
serde_json::json!(as_agent_id.as_str()),
|
|
3034
|
+
);
|
|
3035
|
+
entry.insert(
|
|
3036
|
+
"window".to_string(),
|
|
3037
|
+
serde_json::json!(as_agent_id.as_str()),
|
|
3038
|
+
);
|
|
3039
|
+
entry.insert(
|
|
3040
|
+
"forked_from".to_string(),
|
|
3041
|
+
serde_json::json!(source_agent_id.as_str()),
|
|
3042
|
+
);
|
|
2580
3043
|
entry.insert(
|
|
2581
3044
|
"spawn_cwd".to_string(),
|
|
2582
3045
|
serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
|
|
2583
3046
|
);
|
|
2584
|
-
entry.insert(
|
|
3047
|
+
entry.insert(
|
|
3048
|
+
"pane_id".to_string(),
|
|
3049
|
+
serde_json::json!(spawn.pane_id.as_str()),
|
|
3050
|
+
);
|
|
2585
3051
|
if let Some(pid) = spawn.child_pid {
|
|
2586
3052
|
entry.insert("pane_pid".to_string(), serde_json::json!(pid));
|
|
2587
3053
|
}
|
|
2588
|
-
for key in [
|
|
3054
|
+
for key in [
|
|
3055
|
+
"auth_mode",
|
|
3056
|
+
"model",
|
|
3057
|
+
"model_source",
|
|
3058
|
+
"profile",
|
|
3059
|
+
"_profile_dir",
|
|
3060
|
+
"role",
|
|
3061
|
+
] {
|
|
2589
3062
|
if let Some(value) = spec_agent.get(key) {
|
|
2590
3063
|
entry.insert(key.to_string(), yaml_value_to_json(value));
|
|
2591
3064
|
}
|
|
@@ -2602,9 +3075,15 @@ fn upsert_forked_agent_state(
|
|
|
2602
3075
|
entry.insert("rollout_path".to_string(), serde_json::Value::Null);
|
|
2603
3076
|
entry.insert("captured_at".to_string(), serde_json::Value::Null);
|
|
2604
3077
|
entry.insert("captured_via".to_string(), serde_json::Value::Null);
|
|
2605
|
-
entry.insert(
|
|
3078
|
+
entry.insert(
|
|
3079
|
+
"attribution_confidence".to_string(),
|
|
3080
|
+
serde_json::Value::Null,
|
|
3081
|
+
);
|
|
2606
3082
|
persist_command_plan_state(&mut entry, plan, profile_launch);
|
|
2607
|
-
agent_map.insert(
|
|
3083
|
+
agent_map.insert(
|
|
3084
|
+
as_agent_id.as_str().to_string(),
|
|
3085
|
+
serde_json::Value::Object(entry),
|
|
3086
|
+
);
|
|
2608
3087
|
if let Some(entry) = agent_map
|
|
2609
3088
|
.get_mut(as_agent_id.as_str())
|
|
2610
3089
|
.and_then(serde_json::Value::as_object_mut)
|
|
@@ -2642,12 +3121,9 @@ pub(crate) fn ensure_owner_allowed_for_state(
|
|
|
2642
3121
|
None,
|
|
2643
3122
|
)
|
|
2644
3123
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
2645
|
-
if let Some(refusal) =
|
|
2646
|
-
state,
|
|
2647
|
-
|
|
2648
|
-
false,
|
|
2649
|
-
&NoopLiveness,
|
|
2650
|
-
) {
|
|
3124
|
+
if let Some(refusal) =
|
|
3125
|
+
crate::state::owner_gate::check_team_owner(state, &caller, false, &NoopLiveness)
|
|
3126
|
+
{
|
|
2651
3127
|
return Err(LifecycleError::OwnerRefused(refusal.to_string()));
|
|
2652
3128
|
}
|
|
2653
3129
|
Ok(())
|
|
@@ -2676,7 +3152,10 @@ fn initial_runtime_state(
|
|
|
2676
3152
|
let Some(id) = agent.get("id").and_then(Value::as_str) else {
|
|
2677
3153
|
continue;
|
|
2678
3154
|
};
|
|
2679
|
-
let provider = agent
|
|
3155
|
+
let provider = agent
|
|
3156
|
+
.get("provider")
|
|
3157
|
+
.and_then(Value::as_str)
|
|
3158
|
+
.unwrap_or("codex");
|
|
2680
3159
|
let role = agent.get("role").and_then(Value::as_str).unwrap_or(id);
|
|
2681
3160
|
let model = agent.get("model").and_then(Value::as_str);
|
|
2682
3161
|
let auth_mode = agent.get("auth_mode").and_then(Value::as_str);
|
|
@@ -2698,7 +3177,9 @@ fn initial_runtime_state(
|
|
|
2698
3177
|
.get("runtime")
|
|
2699
3178
|
.and_then(|runtime| runtime.get("display_backend"))
|
|
2700
3179
|
.and_then(Value::as_str)
|
|
2701
|
-
.and_then(|backend|
|
|
3180
|
+
.and_then(|backend| {
|
|
3181
|
+
serde_json::from_value::<DisplayBackend>(serde_json::json!(backend)).ok()
|
|
3182
|
+
});
|
|
2702
3183
|
let display_backend =
|
|
2703
3184
|
crate::lifecycle::display::resolve_display_backend(requested_display, None).backend;
|
|
2704
3185
|
let mut state = serde_json::Map::new();
|
|
@@ -2720,11 +3201,16 @@ fn initial_runtime_state(
|
|
|
2720
3201
|
);
|
|
2721
3202
|
state.insert(
|
|
2722
3203
|
"leader".to_string(),
|
|
2723
|
-
spec.get("leader")
|
|
3204
|
+
spec.get("leader")
|
|
3205
|
+
.map(yaml_value_to_json)
|
|
3206
|
+
.unwrap_or(serde_json::Value::Null),
|
|
2724
3207
|
);
|
|
2725
3208
|
state.insert("agents".to_string(), serde_json::Value::Object(agents));
|
|
2726
3209
|
state.insert("tasks".to_string(), spec_tasks_json(spec));
|
|
2727
|
-
state.insert(
|
|
3210
|
+
state.insert(
|
|
3211
|
+
"display_backend".to_string(),
|
|
3212
|
+
serde_json::json!(display_backend),
|
|
3213
|
+
);
|
|
2728
3214
|
let mut state = serde_json::Value::Object(state);
|
|
2729
3215
|
if !seed_launched_owner_from_env(&mut state) {
|
|
2730
3216
|
let team_id = crate::state::projection::team_state_key(&state);
|
|
@@ -2800,9 +3286,7 @@ fn has_positive_caller_leader_env() -> bool {
|
|
|
2800
3286
|
fn spec_tasks_json(spec: &Value) -> serde_json::Value {
|
|
2801
3287
|
spec.get("tasks")
|
|
2802
3288
|
.and_then(Value::as_list)
|
|
2803
|
-
.map(|tasks|
|
|
2804
|
-
serde_json::Value::Array(tasks.iter().map(yaml_value_to_json).collect())
|
|
2805
|
-
})
|
|
3289
|
+
.map(|tasks| serde_json::Value::Array(tasks.iter().map(yaml_value_to_json).collect()))
|
|
2806
3290
|
.unwrap_or_else(|| serde_json::json!([]))
|
|
2807
3291
|
}
|
|
2808
3292
|
|
|
@@ -2841,7 +3325,10 @@ fn override_spec_session_name(spec: &mut Value, session_name: &str) {
|
|
|
2841
3325
|
if let Some((_, existing)) = runtime.iter_mut().find(|(k, _)| k == "session_name") {
|
|
2842
3326
|
*existing = Value::Str(session_name.to_string());
|
|
2843
3327
|
} else {
|
|
2844
|
-
runtime.push((
|
|
3328
|
+
runtime.push((
|
|
3329
|
+
"session_name".to_string(),
|
|
3330
|
+
Value::Str(session_name.to_string()),
|
|
3331
|
+
));
|
|
2845
3332
|
}
|
|
2846
3333
|
}
|
|
2847
3334
|
Some(other) => {
|
|
@@ -2863,12 +3350,22 @@ fn override_spec_session_name(spec: &mut Value, session_name: &str) {
|
|
|
2863
3350
|
}
|
|
2864
3351
|
|
|
2865
3352
|
fn spec_session_name(spec: &Value) -> SessionName {
|
|
2866
|
-
let name = spec
|
|
3353
|
+
if let Some(name) = spec
|
|
2867
3354
|
.get("runtime")
|
|
2868
3355
|
.and_then(|v| v.get("session_name"))
|
|
2869
3356
|
.and_then(Value::as_str)
|
|
2870
|
-
.
|
|
2871
|
-
|
|
3357
|
+
.filter(|name| !name.is_empty())
|
|
3358
|
+
{
|
|
3359
|
+
return SessionName::new(name);
|
|
3360
|
+
}
|
|
3361
|
+
// Python launch/core.py:56 — fallback derives from the team name, not a constant.
|
|
3362
|
+
let team_name = spec
|
|
3363
|
+
.get("team")
|
|
3364
|
+
.and_then(|team| team.get("name"))
|
|
3365
|
+
.and_then(Value::as_str)
|
|
3366
|
+
.filter(|name| !name.is_empty())
|
|
3367
|
+
.unwrap_or("agent");
|
|
3368
|
+
SessionName::new(format!("team-{team_name}"))
|
|
2872
3369
|
}
|
|
2873
3370
|
|
|
2874
3371
|
fn spec_agents(spec: &Value) -> Vec<AgentId> {
|
|
@@ -2948,20 +3445,11 @@ fn disabled_dangerous_approval() -> DangerousApproval {
|
|
|
2948
3445
|
}
|
|
2949
3446
|
}
|
|
2950
3447
|
|
|
2951
|
-
pub(crate) fn effective_runtime_config_for_worker_spawn(
|
|
3448
|
+
pub(crate) fn effective_runtime_config_for_worker_spawn(
|
|
3449
|
+
) -> Result<DangerousApproval, LifecycleError> {
|
|
2952
3450
|
detect_dangerous_approval()
|
|
2953
3451
|
}
|
|
2954
3452
|
|
|
2955
|
-
pub(crate) fn worker_tool_refs(
|
|
2956
|
-
mut tools: Vec<String>,
|
|
2957
|
-
safety: &DangerousApproval,
|
|
2958
|
-
) -> Vec<String> {
|
|
2959
|
-
if safety.enabled && !tools.iter().any(|tool| tool == "dangerous_auto_approve") {
|
|
2960
|
-
tools.push("dangerous_auto_approve".to_string());
|
|
2961
|
-
}
|
|
2962
|
-
tools
|
|
2963
|
-
}
|
|
2964
|
-
|
|
2965
3453
|
fn write_launch_permission_audit(
|
|
2966
3454
|
workspace: &Path,
|
|
2967
3455
|
safety: &DangerousApproval,
|
|
@@ -3019,6 +3507,5 @@ fn agent_id_exists_in_team_dir(team_dir: &Path, agent_id: &AgentId) -> bool {
|
|
|
3019
3507
|
.exists()
|
|
3020
3508
|
}
|
|
3021
3509
|
|
|
3022
|
-
|
|
3023
3510
|
mod plan;
|
|
3024
3511
|
pub use plan::{handle_report_result, start_plan};
|