@team-agent/installer 0.3.2 → 0.3.4
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 +34 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +196 -19
- package/crates/team-agent/src/cli/diagnose.rs +145 -11
- package/crates/team-agent/src/cli/emit.rs +287 -53
- package/crates/team-agent/src/cli/leader.rs +37 -8
- package/crates/team-agent/src/cli/mod.rs +807 -316
- package/crates/team-agent/src/cli/status_port.rs +25 -2
- package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
- package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +57 -3
- package/crates/team-agent/src/cli/types.rs +17 -0
- package/crates/team-agent/src/compiler/tests.rs +2 -2
- package/crates/team-agent/src/compiler.rs +16 -6
- package/crates/team-agent/src/coordinator/health.rs +89 -20
- package/crates/team-agent/src/coordinator/mod.rs +4 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +4 -2
- package/crates/team-agent/src/coordinator/tick.rs +222 -69
- package/crates/team-agent/src/coordinator/types.rs +15 -3
- package/crates/team-agent/src/db/schema.rs +37 -2
- package/crates/team-agent/src/diagnose/comms.rs +226 -0
- package/crates/team-agent/src/diagnose/mod.rs +45 -0
- package/crates/team-agent/src/diagnose/orphans.rs +658 -0
- package/crates/team-agent/src/fake_worker.rs +146 -3
- package/crates/team-agent/src/leader/start.rs +121 -23
- package/crates/team-agent/src/leader/types.rs +44 -1
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +648 -50
- package/crates/team-agent/src/lifecycle/launch.rs +1048 -264
- package/crates/team-agent/src/lifecycle/mod.rs +3 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +113 -26
- package/crates/team-agent/src/lifecycle/restart/common.rs +189 -102
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +465 -25
- package/crates/team-agent/src/lifecycle/restart/remove.rs +22 -6
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
- package/crates/team-agent/src/lifecycle/restart.rs +4 -1
- package/crates/team-agent/src/lifecycle/tests/core.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +39 -9
- package/crates/team-agent/src/lifecycle/types.rs +23 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +326 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
- package/crates/team-agent/src/mcp_server/mod.rs +3 -74
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
- package/crates/team-agent/src/mcp_server/tools.rs +312 -111
- package/crates/team-agent/src/mcp_server/types.rs +6 -4
- package/crates/team-agent/src/mcp_server/wire.rs +19 -7
- package/crates/team-agent/src/message_store.rs +21 -4
- package/crates/team-agent/src/messaging/delivery.rs +87 -37
- package/crates/team-agent/src/messaging/mod.rs +9 -6
- package/crates/team-agent/src/messaging/results.rs +153 -16
- package/crates/team-agent/src/messaging/selftest.rs +199 -12
- package/crates/team-agent/src/messaging/send.rs +35 -3
- package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
- package/crates/team-agent/src/messaging/types.rs +11 -3
- package/crates/team-agent/src/os_probe.rs +119 -0
- package/crates/team-agent/src/packaging/migrate.rs +10 -2
- package/crates/team-agent/src/packaging/tests.rs +23 -0
- package/crates/team-agent/src/provider/adapter.rs +483 -67
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
- package/crates/team-agent/src/provider/classify.rs +51 -4
- package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
- package/crates/team-agent/src/provider/types.rs +47 -0
- package/crates/team-agent/src/session_capture.rs +616 -0
- package/crates/team-agent/src/state/persist.rs +57 -0
- package/crates/team-agent/src/state/projection.rs +32 -23
- package/crates/team-agent/src/state/selector.rs +5 -2
- package/crates/team-agent/src/tmux_backend.rs +151 -60
- package/crates/team-agent/src/transport/test_support.rs +9 -0
- package/crates/team-agent/src/transport/tests/wire.rs +4 -0
- package/crates/team-agent/src/transport.rs +13 -2
- 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 {
|
|
@@ -113,6 +127,7 @@ pub fn launch_with_transport_in_workspace(
|
|
|
113
127
|
permissions,
|
|
114
128
|
safety,
|
|
115
129
|
leader_receiver_attached: false,
|
|
130
|
+
session_capture_incomplete_agents: Vec::new(),
|
|
116
131
|
})
|
|
117
132
|
}
|
|
118
133
|
|
|
@@ -151,9 +166,19 @@ fn spawn_agents(
|
|
|
151
166
|
// has both the role instruction AND the callable Team Agent MCP capability.
|
|
152
167
|
// probe5 RED proved that `build_command(.., None, None, ..)` left the worker
|
|
153
168
|
// without `report_result`; placeholders are substituted at spawn time.
|
|
154
|
-
let
|
|
155
|
-
|
|
156
|
-
|
|
169
|
+
let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
|
|
170
|
+
agent,
|
|
171
|
+
Some(agent_id_raw),
|
|
172
|
+
provider,
|
|
173
|
+
);
|
|
174
|
+
let system_prompt =
|
|
175
|
+
crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
|
|
176
|
+
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
177
|
+
&command_agent,
|
|
178
|
+
provider,
|
|
179
|
+
safety,
|
|
180
|
+
)?;
|
|
181
|
+
let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
157
182
|
let mcp_team_id =
|
|
158
183
|
runtime_active_team_key_for_spawn(workspace, spec_path, spec, session_name);
|
|
159
184
|
let mcp_config = adapter
|
|
@@ -161,32 +186,38 @@ fn spawn_agents(
|
|
|
161
186
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
162
187
|
let mcp_config = resolve_mcp_config(mcp_config, workspace, agent_id_raw, &mcp_team_id);
|
|
163
188
|
let mcp_config_path = write_worker_mcp_config(workspace, agent_id_raw, &mcp_config)?;
|
|
164
|
-
let
|
|
165
|
-
|
|
166
|
-
|
|
189
|
+
let profile_dir = team_dir.join("profiles");
|
|
190
|
+
let profile_launch =
|
|
191
|
+
crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
|
|
192
|
+
workspace,
|
|
193
|
+
agent_id_raw,
|
|
194
|
+
agent,
|
|
195
|
+
Some(&profile_dir),
|
|
167
196
|
Some(&mcp_config),
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
197
|
+
)?;
|
|
198
|
+
let command_model = profile_launch.command_overrides.model.as_deref().or(model);
|
|
199
|
+
let mut plan = adapter
|
|
200
|
+
.build_command_plan(crate::provider::ProviderCommandContext {
|
|
201
|
+
auth_mode,
|
|
202
|
+
mcp_config: Some(&mcp_config),
|
|
203
|
+
system_prompt: Some(system_prompt.as_str()),
|
|
204
|
+
model: command_model,
|
|
205
|
+
tools: &resolved_tool_refs,
|
|
206
|
+
profile_launch: Some(&profile_launch),
|
|
207
|
+
})
|
|
172
208
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
agent_id_raw,
|
|
178
|
-
Some(&mcp_team_id),
|
|
179
|
-
);
|
|
209
|
+
if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
|
|
210
|
+
point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
|
|
211
|
+
}
|
|
212
|
+
fill_spawn_placeholders_full(&mut plan.argv, workspace, agent_id_raw, Some(&mcp_team_id));
|
|
180
213
|
let window = WindowName::new(agent_id_raw);
|
|
181
|
-
let env =
|
|
182
|
-
workspace,
|
|
183
|
-
|
|
184
|
-
Some(&mcp_team_id),
|
|
185
|
-
);
|
|
214
|
+
let mut env =
|
|
215
|
+
inherited_env_with_team_overrides(workspace, agent_id_raw, Some(&mcp_team_id));
|
|
216
|
+
apply_profile_launch_env(&mut env, &profile_launch);
|
|
186
217
|
let spawn = if started.is_empty() {
|
|
187
|
-
transport.spawn_first(session_name, &window, &argv, team_dir, &env)
|
|
218
|
+
transport.spawn_first(session_name, &window, &plan.argv, team_dir, &env)
|
|
188
219
|
} else {
|
|
189
|
-
transport.spawn_into(session_name, &window, &argv, team_dir, &env)
|
|
220
|
+
transport.spawn_into(session_name, &window, &plan.argv, team_dir, &env)
|
|
190
221
|
}
|
|
191
222
|
.map_err(|e| LifecycleError::Transport(e.to_string()))?;
|
|
192
223
|
let _ = adapter.handle_startup_prompts(
|
|
@@ -204,6 +235,13 @@ fn spawn_agents(
|
|
|
204
235
|
target: spawn.pane_id.as_str().to_string(),
|
|
205
236
|
session_id: None,
|
|
206
237
|
rollout_path: None,
|
|
238
|
+
pending_session_id: plan.expected_session_id.clone(),
|
|
239
|
+
claude_config_dir: profile_launch.claude_config_dir.clone(),
|
|
240
|
+
provider_projects_root: plan
|
|
241
|
+
.provider_projects_root
|
|
242
|
+
.clone()
|
|
243
|
+
.or_else(|| profile_launch.claude_projects_root.clone()),
|
|
244
|
+
managed_mcp_config: plan.managed_mcp_config || profile_launch.managed_mcp_config,
|
|
207
245
|
display: WorkerDisplay::Blocked {
|
|
208
246
|
reason: AdaptiveBlockReason::NotImplementedThisPlatform,
|
|
209
247
|
},
|
|
@@ -219,6 +257,7 @@ fn persist_spawn_agent_state(
|
|
|
219
257
|
session_name: &SessionName,
|
|
220
258
|
transport: &dyn Transport,
|
|
221
259
|
started: &[StartedAgent],
|
|
260
|
+
safety: &DangerousApproval,
|
|
222
261
|
) -> Result<(), LifecycleError> {
|
|
223
262
|
let state_path = crate::state::persist::runtime_state_path(workspace);
|
|
224
263
|
let mut state = if state_path.exists() {
|
|
@@ -232,12 +271,7 @@ fn persist_spawn_agent_state(
|
|
|
232
271
|
let team_id = explicit_active_team_key(&state)
|
|
233
272
|
.unwrap_or_else(|| runtime_team_key_for_spec(spec_path, spec, session_name));
|
|
234
273
|
let worker_tmux_socket = launched_worker_tmux_socket(transport, workspace);
|
|
235
|
-
drop_worker_pane_seeded_owner(
|
|
236
|
-
&mut state,
|
|
237
|
-
&team_id,
|
|
238
|
-
started,
|
|
239
|
-
worker_tmux_socket.as_deref(),
|
|
240
|
-
);
|
|
274
|
+
drop_worker_pane_seeded_owner(&mut state, &team_id, started, worker_tmux_socket.as_deref());
|
|
241
275
|
// Only persist running state for agents whose spawn still has a live target.
|
|
242
276
|
let live_windows: BTreeSet<String> = transport
|
|
243
277
|
.list_windows(session_name)
|
|
@@ -250,8 +284,9 @@ fn persist_spawn_agent_state(
|
|
|
250
284
|
.map(|agent| agent.agent_id.as_str().to_string())
|
|
251
285
|
.collect();
|
|
252
286
|
let pane_pids_by_agent = pane_pids_by_started_agent(transport, started);
|
|
287
|
+
let profile_dir = spec_path.parent().unwrap_or(workspace).join("profiles");
|
|
253
288
|
let mut agents = serde_json::Map::new();
|
|
254
|
-
let
|
|
289
|
+
let mut spawn_index = 0_u32;
|
|
255
290
|
for agent in spec_agent_values(spec) {
|
|
256
291
|
let Some(id) = agent.get("id").and_then(Value::as_str) else {
|
|
257
292
|
continue;
|
|
@@ -285,9 +320,25 @@ fn persist_spawn_agent_state(
|
|
|
285
320
|
continue;
|
|
286
321
|
}
|
|
287
322
|
let pane_pid = pane_pids_by_agent.get(id).copied();
|
|
323
|
+
let spawned_at = spawn_timestamp_for_agent(spawn_index);
|
|
324
|
+
spawn_index = spawn_index.saturating_add(1);
|
|
325
|
+
let started_agent = started.iter().find(|agent| agent.agent_id.as_str() == id);
|
|
288
326
|
agents.insert(
|
|
289
327
|
id.to_string(),
|
|
290
|
-
running_agent_state(
|
|
328
|
+
running_agent_state(
|
|
329
|
+
agent,
|
|
330
|
+
id,
|
|
331
|
+
provider,
|
|
332
|
+
workspace,
|
|
333
|
+
spec_path.parent().unwrap_or(workspace),
|
|
334
|
+
&spawned_at,
|
|
335
|
+
&team_id,
|
|
336
|
+
Some(agent_id_to_pane_id(started, id)),
|
|
337
|
+
pane_pid,
|
|
338
|
+
safety,
|
|
339
|
+
started_agent,
|
|
340
|
+
Some(&profile_dir),
|
|
341
|
+
)?,
|
|
291
342
|
);
|
|
292
343
|
}
|
|
293
344
|
if let Some(obj) = state.as_object_mut() {
|
|
@@ -317,7 +368,18 @@ fn pane_pids_by_started_agent(
|
|
|
317
368
|
.collect()
|
|
318
369
|
}
|
|
319
370
|
|
|
320
|
-
fn
|
|
371
|
+
fn agent_id_to_pane_id<'a>(started: &'a [StartedAgent], agent_id: &str) -> &'a str {
|
|
372
|
+
started
|
|
373
|
+
.iter()
|
|
374
|
+
.find(|agent| agent.agent_id.as_str() == agent_id)
|
|
375
|
+
.map(|agent| agent.target.as_str())
|
|
376
|
+
.unwrap_or("")
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
fn save_launched_team_state(
|
|
380
|
+
workspace: &Path,
|
|
381
|
+
launched: &serde_json::Value,
|
|
382
|
+
) -> Result<(), LifecycleError> {
|
|
321
383
|
save_launched_team_state_for_key(workspace, launched, None)
|
|
322
384
|
}
|
|
323
385
|
|
|
@@ -340,6 +402,7 @@ fn save_launched_team_state_for_key(
|
|
|
340
402
|
}
|
|
341
403
|
promote_launched_binding_from_team_entry(&mut launched, &launched_key);
|
|
342
404
|
drop_foreign_seeded_owner(&existing, &launched_key, &mut launched);
|
|
405
|
+
drop_bare_worker_seeded_owner(&mut launched, &launched_key);
|
|
343
406
|
let merged = if team_key.is_some() {
|
|
344
407
|
merge_workspace_team_state_with_key(&existing, &launched, &launched_key)
|
|
345
408
|
} else {
|
|
@@ -347,7 +410,22 @@ fn save_launched_team_state_for_key(
|
|
|
347
410
|
};
|
|
348
411
|
let mut projected = crate::state::projection::project_top_level_view(&merged, &launched_key);
|
|
349
412
|
drop_unbound_top_level_owner(&mut projected);
|
|
350
|
-
save_runtime_state(workspace, &projected)
|
|
413
|
+
save_runtime_state(workspace, &projected)
|
|
414
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
fn drop_bare_worker_seeded_owner(launched: &mut serde_json::Value, launched_key: &str) {
|
|
418
|
+
if has_positive_caller_leader_env() {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
let pane = launched
|
|
422
|
+
.get("team_owner")
|
|
423
|
+
.and_then(|owner| owner.get("pane_id"))
|
|
424
|
+
.and_then(serde_json::Value::as_str)
|
|
425
|
+
.unwrap_or("");
|
|
426
|
+
if pane.ends_with("-first") {
|
|
427
|
+
seed_unbound_launched_owner(launched, launched_key);
|
|
428
|
+
}
|
|
351
429
|
}
|
|
352
430
|
|
|
353
431
|
fn merge_workspace_team_state_with_key(
|
|
@@ -435,7 +513,11 @@ fn drop_unbound_top_level_owner(state: &mut serde_json::Value) {
|
|
|
435
513
|
}
|
|
436
514
|
}
|
|
437
515
|
|
|
438
|
-
fn drop_foreign_seeded_owner(
|
|
516
|
+
fn drop_foreign_seeded_owner(
|
|
517
|
+
existing: &serde_json::Value,
|
|
518
|
+
launched_key: &str,
|
|
519
|
+
launched: &mut serde_json::Value,
|
|
520
|
+
) {
|
|
439
521
|
let Some(pane) = launched
|
|
440
522
|
.get("team_owner")
|
|
441
523
|
.and_then(|owner| owner.get("pane_id"))
|
|
@@ -445,7 +527,15 @@ fn drop_foreign_seeded_owner(existing: &serde_json::Value, launched_key: &str, l
|
|
|
445
527
|
return;
|
|
446
528
|
};
|
|
447
529
|
if owner_pane_belongs_to_other_team(existing, launched_key, pane) {
|
|
448
|
-
|
|
530
|
+
let replacement = unbound_launched_owner(launched, launched_key);
|
|
531
|
+
if let Some(obj) = launched.as_object_mut() {
|
|
532
|
+
if let Some(owner) = replacement {
|
|
533
|
+
obj.insert("team_owner".to_string(), owner);
|
|
534
|
+
} else {
|
|
535
|
+
obj.remove("team_owner");
|
|
536
|
+
}
|
|
537
|
+
obj.remove("owner_epoch");
|
|
538
|
+
}
|
|
449
539
|
}
|
|
450
540
|
}
|
|
451
541
|
|
|
@@ -469,27 +559,28 @@ fn drop_worker_pane_seeded_owner(
|
|
|
469
559
|
let tmux_pane = std::env::var("TMUX_PANE")
|
|
470
560
|
.ok()
|
|
471
561
|
.filter(|value| !value.is_empty());
|
|
472
|
-
let has_leader_identity_env =
|
|
473
|
-
|
|
474
|
-
|| env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE")
|
|
475
|
-
|| env_nonempty("TEAM_AGENT_LEADER_PROVIDER")
|
|
476
|
-
|| env_nonempty("TEAM_AGENT_ID")
|
|
477
|
-
|| env_nonempty("TEAM_AGENT_TEAM_ID");
|
|
478
|
-
let seeded_from_bare_tmux =
|
|
479
|
-
!has_leader_identity_env && tmux_pane.as_deref() == Some(pane);
|
|
562
|
+
let has_leader_identity_env = has_positive_caller_leader_env();
|
|
563
|
+
let seeded_from_bare_tmux = !has_leader_identity_env && tmux_pane.as_deref() == Some(pane);
|
|
480
564
|
let caller_tmux_socket = crate::tmux_backend::socket_name_from_tmux_env();
|
|
481
565
|
if seeded_from_bare_tmux
|
|
482
|
-
&& tmux_sockets_match_or_unknown(caller_tmux_socket.as_deref(), worker_tmux_socket)
|
|
483
|
-
|
|
566
|
+
&& (tmux_sockets_match_or_unknown(caller_tmux_socket.as_deref(), worker_tmux_socket)
|
|
567
|
+
|| pane.ends_with("-first"))
|
|
568
|
+
&& seeded_pane_looks_like_worker(pane, started)
|
|
484
569
|
{
|
|
485
570
|
seed_unbound_launched_owner(launched, launched_key);
|
|
486
571
|
}
|
|
487
572
|
}
|
|
488
573
|
|
|
489
|
-
fn
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
574
|
+
fn seeded_pane_looks_like_worker(pane: &str, started: &[StartedAgent]) -> bool {
|
|
575
|
+
pane.ends_with("-first")
|
|
576
|
+
|| started.iter().any(|agent| {
|
|
577
|
+
pane == agent.target
|
|
578
|
+
|| pane.starts_with(agent.target.as_str())
|
|
579
|
+
|| agent.target.starts_with(pane)
|
|
580
|
+
})
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
fn launched_worker_tmux_socket(transport: &dyn Transport, workspace: &Path) -> Option<String> {
|
|
493
584
|
if matches!(transport.kind(), crate::transport::BackendKind::Tmux) {
|
|
494
585
|
Some(crate::tmux_backend::socket_name_for_workspace(workspace))
|
|
495
586
|
} else {
|
|
@@ -497,10 +588,7 @@ fn launched_worker_tmux_socket(
|
|
|
497
588
|
}
|
|
498
589
|
}
|
|
499
590
|
|
|
500
|
-
fn tmux_sockets_match_or_unknown(
|
|
501
|
-
caller_socket: Option<&str>,
|
|
502
|
-
worker_socket: Option<&str>,
|
|
503
|
-
) -> bool {
|
|
591
|
+
fn tmux_sockets_match_or_unknown(caller_socket: Option<&str>, worker_socket: Option<&str>) -> bool {
|
|
504
592
|
match (caller_socket, worker_socket) {
|
|
505
593
|
(Some(caller), Some(worker)) => caller == worker,
|
|
506
594
|
(Some(_), None) => false,
|
|
@@ -509,10 +597,41 @@ fn tmux_sockets_match_or_unknown(
|
|
|
509
597
|
}
|
|
510
598
|
|
|
511
599
|
fn env_nonempty(key: &str) -> bool {
|
|
512
|
-
std::env::var(key)
|
|
600
|
+
std::env::var(key)
|
|
601
|
+
.ok()
|
|
602
|
+
.is_some_and(|value| !value.is_empty())
|
|
513
603
|
}
|
|
514
604
|
|
|
515
605
|
fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &str) {
|
|
606
|
+
let Some(owner) = unbound_launched_owner(launched, launched_key) else {
|
|
607
|
+
return;
|
|
608
|
+
};
|
|
609
|
+
let provider = launched
|
|
610
|
+
.get("team_owner")
|
|
611
|
+
.and_then(|owner| owner.get("provider"))
|
|
612
|
+
.and_then(serde_json::Value::as_str)
|
|
613
|
+
.filter(|provider| !provider.is_empty())
|
|
614
|
+
.unwrap_or("codex");
|
|
615
|
+
let owner_epoch = 1u64;
|
|
616
|
+
let receiver = serde_json::json!({
|
|
617
|
+
"mode": "direct_tmux",
|
|
618
|
+
"status": "unbound",
|
|
619
|
+
"provider": provider,
|
|
620
|
+
"leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
|
|
621
|
+
"owner_epoch": owner_epoch,
|
|
622
|
+
"discovery": "quick_start",
|
|
623
|
+
});
|
|
624
|
+
if let Some(obj) = launched.as_object_mut() {
|
|
625
|
+
obj.insert("leader_receiver".to_string(), receiver);
|
|
626
|
+
obj.insert("team_owner".to_string(), owner);
|
|
627
|
+
obj.insert("owner_epoch".to_string(), serde_json::json!(owner_epoch));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
fn unbound_launched_owner(
|
|
632
|
+
launched: &serde_json::Value,
|
|
633
|
+
launched_key: &str,
|
|
634
|
+
) -> Option<serde_json::Value> {
|
|
516
635
|
let provider = launched
|
|
517
636
|
.get("team_owner")
|
|
518
637
|
.and_then(|owner| owner.get("provider"))
|
|
@@ -531,43 +650,29 @@ fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &
|
|
|
531
650
|
let os_user = std::env::var("USER")
|
|
532
651
|
.or_else(|_| std::env::var("USERNAME"))
|
|
533
652
|
.unwrap_or_default();
|
|
534
|
-
let
|
|
653
|
+
let uuid = crate::model::ids::LeaderSessionUuid::derive(
|
|
535
654
|
machine_fingerprint,
|
|
536
655
|
workspace,
|
|
537
656
|
&os_user,
|
|
538
657
|
launched_key,
|
|
539
|
-
)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
let owner_epoch = 1u64;
|
|
543
|
-
let owner = serde_json::json!({
|
|
544
|
-
"pane_id": "__team_agent_unbound__",
|
|
658
|
+
)
|
|
659
|
+
.ok()?;
|
|
660
|
+
Some(serde_json::json!({
|
|
545
661
|
"provider": provider,
|
|
546
662
|
"machine_fingerprint": machine_fingerprint,
|
|
547
663
|
"leader_session_uuid": uuid.as_str(),
|
|
548
|
-
"owner_epoch":
|
|
664
|
+
"owner_epoch": 1u64,
|
|
549
665
|
"claimed_at": spawn_timestamp(),
|
|
550
666
|
"claimed_via": "quick-start",
|
|
551
667
|
"os_user": os_user,
|
|
552
|
-
})
|
|
553
|
-
let receiver = serde_json::json!({
|
|
554
|
-
"mode": "direct_tmux",
|
|
555
|
-
"status": "attached",
|
|
556
|
-
"provider": provider,
|
|
557
|
-
"pane_id": "__team_agent_unbound__",
|
|
558
|
-
"pane": "__team_agent_unbound__",
|
|
559
|
-
"leader_session_uuid": uuid.as_str(),
|
|
560
|
-
"owner_epoch": owner_epoch,
|
|
561
|
-
"discovery": "quick_start",
|
|
562
|
-
});
|
|
563
|
-
if let Some(obj) = launched.as_object_mut() {
|
|
564
|
-
obj.insert("leader_receiver".to_string(), receiver);
|
|
565
|
-
obj.insert("team_owner".to_string(), owner);
|
|
566
|
-
obj.insert("owner_epoch".to_string(), serde_json::json!(owner_epoch));
|
|
567
|
-
}
|
|
668
|
+
}))
|
|
568
669
|
}
|
|
569
670
|
|
|
570
|
-
fn owner_pane_belongs_to_other_team(
|
|
671
|
+
fn owner_pane_belongs_to_other_team(
|
|
672
|
+
existing: &serde_json::Value,
|
|
673
|
+
launched_key: &str,
|
|
674
|
+
pane: &str,
|
|
675
|
+
) -> bool {
|
|
571
676
|
existing
|
|
572
677
|
.get("teams")
|
|
573
678
|
.and_then(serde_json::Value::as_object)
|
|
@@ -588,9 +693,14 @@ fn running_agent_state(
|
|
|
588
693
|
id: &str,
|
|
589
694
|
provider: Provider,
|
|
590
695
|
workspace: &Path,
|
|
696
|
+
spawn_cwd: &Path,
|
|
591
697
|
spawned_at: &str,
|
|
592
698
|
team_id: &str,
|
|
699
|
+
pane_id: Option<&str>,
|
|
593
700
|
pane_pid: Option<u32>,
|
|
701
|
+
safety: &DangerousApproval,
|
|
702
|
+
started_agent: Option<&StartedAgent>,
|
|
703
|
+
profile_dir: Option<&Path>,
|
|
594
704
|
) -> Result<serde_json::Value, LifecycleError> {
|
|
595
705
|
let model = agent.get("model").and_then(Value::as_str);
|
|
596
706
|
let auth_mode = agent
|
|
@@ -598,7 +708,10 @@ fn running_agent_state(
|
|
|
598
708
|
.and_then(Value::as_str)
|
|
599
709
|
.and_then(parse_auth_mode)
|
|
600
710
|
.unwrap_or(AuthMode::Subscription);
|
|
601
|
-
let profile = agent
|
|
711
|
+
let profile = agent
|
|
712
|
+
.get("profile")
|
|
713
|
+
.map(yaml_value_to_json)
|
|
714
|
+
.unwrap_or(serde_json::Value::Null);
|
|
602
715
|
let window = agent.get("window").and_then(Value::as_str).unwrap_or(id);
|
|
603
716
|
let mcp_config = crate::provider::get_adapter(provider)
|
|
604
717
|
.mcp_config(auth_mode)
|
|
@@ -609,9 +722,20 @@ fn running_agent_state(
|
|
|
609
722
|
state.insert("status".to_string(), serde_json::json!("running"));
|
|
610
723
|
state.insert("provider".to_string(), serde_json::json!(provider));
|
|
611
724
|
state.insert("agent_id".to_string(), serde_json::json!(id));
|
|
612
|
-
state.insert(
|
|
725
|
+
state.insert(
|
|
726
|
+
"model".to_string(),
|
|
727
|
+
model.map_or(serde_json::Value::Null, |m| serde_json::json!(m)),
|
|
728
|
+
);
|
|
613
729
|
state.insert("auth_mode".to_string(), serde_json::json!(auth_mode));
|
|
614
730
|
state.insert("profile".to_string(), profile);
|
|
731
|
+
if agent.get("profile").is_some() {
|
|
732
|
+
if let Some(profile_dir) = profile_dir {
|
|
733
|
+
state.insert(
|
|
734
|
+
"_profile_dir".to_string(),
|
|
735
|
+
serde_json::json!(profile_dir.to_string_lossy().to_string()),
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
615
739
|
state.insert("window".to_string(), serde_json::json!(window));
|
|
616
740
|
state.insert(
|
|
617
741
|
"mcp_config".to_string(),
|
|
@@ -622,23 +746,63 @@ fn running_agent_state(
|
|
|
622
746
|
permissions_json(agent, id, provider)
|
|
623
747
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?,
|
|
624
748
|
);
|
|
749
|
+
persist_effective_approval_policy(&mut state, safety);
|
|
625
750
|
state.insert("session_id".to_string(), serde_json::Value::Null);
|
|
626
751
|
state.insert("rollout_path".to_string(), serde_json::Value::Null);
|
|
627
752
|
state.insert("captured_at".to_string(), serde_json::Value::Null);
|
|
628
753
|
state.insert("captured_via".to_string(), serde_json::Value::Null);
|
|
629
|
-
state.insert(
|
|
754
|
+
state.insert(
|
|
755
|
+
"attribution_confidence".to_string(),
|
|
756
|
+
serde_json::Value::Null,
|
|
757
|
+
);
|
|
758
|
+
if let Some(started_agent) = started_agent {
|
|
759
|
+
persist_started_agent_plan_state(&mut state, started_agent);
|
|
760
|
+
}
|
|
630
761
|
state.insert(
|
|
631
762
|
"spawn_cwd".to_string(),
|
|
632
|
-
serde_json::json!(
|
|
763
|
+
serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
|
|
633
764
|
);
|
|
634
765
|
state.insert("spawned_at".to_string(), serde_json::json!(spawned_at));
|
|
766
|
+
if let Some(pane_id) = pane_id.filter(|pane| !pane.is_empty()) {
|
|
767
|
+
state.insert("pane_id".to_string(), serde_json::json!(pane_id));
|
|
768
|
+
}
|
|
635
769
|
if let Some(pane_pid) = pane_pid {
|
|
636
770
|
state.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
|
|
637
771
|
}
|
|
638
772
|
Ok(serde_json::Value::Object(state))
|
|
639
773
|
}
|
|
640
774
|
|
|
641
|
-
fn
|
|
775
|
+
pub(crate) fn effective_approval_policy(safety: &DangerousApproval) -> serde_json::Value {
|
|
776
|
+
serde_json::json!({
|
|
777
|
+
"enabled": safety.enabled,
|
|
778
|
+
"source": dangerous_approval_source_str(safety.source),
|
|
779
|
+
"inherited": safety.inherited,
|
|
780
|
+
"explicit_yes_confirmed": safety.enabled && matches!(safety.source, DangerousApprovalSource::RuntimeConfig),
|
|
781
|
+
"provider": safety.provider,
|
|
782
|
+
"flag": safety.flag,
|
|
783
|
+
"worker_capability_above_leader": safety.worker_capability_above_leader,
|
|
784
|
+
})
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
pub(crate) fn persist_effective_approval_policy(
|
|
788
|
+
agent_state: &mut serde_json::Map<String, serde_json::Value>,
|
|
789
|
+
safety: &DangerousApproval,
|
|
790
|
+
) {
|
|
791
|
+
agent_state.insert(
|
|
792
|
+
"effective_approval_policy".to_string(),
|
|
793
|
+
effective_approval_policy(safety),
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
fn dangerous_approval_source_str(source: DangerousApprovalSource) -> &'static str {
|
|
798
|
+
match source {
|
|
799
|
+
DangerousApprovalSource::RuntimeConfig => "runtime_config",
|
|
800
|
+
DangerousApprovalSource::LeaderProcess => "leader_process",
|
|
801
|
+
DangerousApprovalSource::Disabled => "disabled",
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
pub(crate) fn resolve_mcp_config(
|
|
642
806
|
config: crate::provider::McpConfig,
|
|
643
807
|
workspace: &Path,
|
|
644
808
|
agent_id: &str,
|
|
@@ -681,7 +845,7 @@ fn resolve_mcp_placeholders(
|
|
|
681
845
|
}
|
|
682
846
|
}
|
|
683
847
|
|
|
684
|
-
fn write_worker_mcp_config(
|
|
848
|
+
pub(crate) fn write_worker_mcp_config(
|
|
685
849
|
workspace: &Path,
|
|
686
850
|
agent_id: &str,
|
|
687
851
|
config: &crate::provider::McpConfig,
|
|
@@ -700,7 +864,11 @@ fn write_worker_mcp_config(
|
|
|
700
864
|
Ok(path)
|
|
701
865
|
}
|
|
702
866
|
|
|
703
|
-
fn point_native_mcp_config_at_file(
|
|
867
|
+
pub(crate) fn point_native_mcp_config_at_file(
|
|
868
|
+
argv: &mut [String],
|
|
869
|
+
provider: Provider,
|
|
870
|
+
path: &Path,
|
|
871
|
+
) {
|
|
704
872
|
if !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
|
|
705
873
|
return;
|
|
706
874
|
}
|
|
@@ -727,13 +895,19 @@ fn permissions_json(
|
|
|
727
895
|
let resolved = permissions::resolve_permissions(&AgentPermissionInput {
|
|
728
896
|
id: Some(AgentId::new(id)),
|
|
729
897
|
provider,
|
|
730
|
-
role: agent
|
|
898
|
+
role: agent
|
|
899
|
+
.get("role")
|
|
900
|
+
.and_then(Value::as_str)
|
|
901
|
+
.map(str::to_string),
|
|
731
902
|
tools,
|
|
732
903
|
})?;
|
|
733
904
|
let mut out = serde_json::Map::new();
|
|
734
905
|
out.insert("agent_id".to_string(), serde_json::json!(id));
|
|
735
906
|
out.insert("provider".to_string(), serde_json::json!(provider));
|
|
736
|
-
out.insert(
|
|
907
|
+
out.insert(
|
|
908
|
+
"tools".to_string(),
|
|
909
|
+
serde_json::json!(resolved.sorted_tool_strings()),
|
|
910
|
+
);
|
|
737
911
|
out.insert(
|
|
738
912
|
"resolved_tools".to_string(),
|
|
739
913
|
serde_json::Value::Array(
|
|
@@ -749,7 +923,10 @@ fn permissions_json(
|
|
|
749
923
|
.collect(),
|
|
750
924
|
),
|
|
751
925
|
);
|
|
752
|
-
out.insert(
|
|
926
|
+
out.insert(
|
|
927
|
+
"has_prompt_only".to_string(),
|
|
928
|
+
serde_json::json!(resolved.has_prompt_only),
|
|
929
|
+
);
|
|
753
930
|
Ok(serde_json::Value::Object(out))
|
|
754
931
|
}
|
|
755
932
|
|
|
@@ -766,6 +943,23 @@ fn spawn_timestamp() -> String {
|
|
|
766
943
|
}
|
|
767
944
|
}
|
|
768
945
|
|
|
946
|
+
fn spawn_timestamp_for_agent(offset_micros: u32) -> String {
|
|
947
|
+
if offset_micros == 0 {
|
|
948
|
+
return spawn_timestamp();
|
|
949
|
+
}
|
|
950
|
+
match std::env::var("TEAM_AGENT_TEST_FIXED_SPAWNED_AT") {
|
|
951
|
+
Ok(value) => chrono::DateTime::parse_from_rfc3339(&value)
|
|
952
|
+
.map(|dt| {
|
|
953
|
+
(dt.with_timezone(&chrono::Utc)
|
|
954
|
+
+ chrono::Duration::microseconds(i64::from(offset_micros)))
|
|
955
|
+
.format("%Y-%m-%dT%H:%M:%S%.6f+00:00")
|
|
956
|
+
.to_string()
|
|
957
|
+
})
|
|
958
|
+
.unwrap_or(value),
|
|
959
|
+
Err(_) => spawn_timestamp(),
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
769
963
|
pub(crate) fn fill_spawn_placeholders(argv: &mut [String], workspace: &Path, agent_id: &str) {
|
|
770
964
|
fill_spawn_placeholders_full(argv, workspace, agent_id, None);
|
|
771
965
|
}
|
|
@@ -808,6 +1002,87 @@ pub(crate) fn inherited_env_with_team_overrides(
|
|
|
808
1002
|
env
|
|
809
1003
|
}
|
|
810
1004
|
|
|
1005
|
+
pub(crate) fn apply_profile_launch_env(
|
|
1006
|
+
env: &mut BTreeMap<String, String>,
|
|
1007
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
1008
|
+
) {
|
|
1009
|
+
for key in &profile_launch.env_unset {
|
|
1010
|
+
env.remove(key);
|
|
1011
|
+
}
|
|
1012
|
+
env.extend(profile_launch.env_overlay.clone());
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
fn persist_started_agent_plan_state(
|
|
1016
|
+
state: &mut serde_json::Map<String, serde_json::Value>,
|
|
1017
|
+
started_agent: &StartedAgent,
|
|
1018
|
+
) {
|
|
1019
|
+
if let Some(session_id) = started_agent.pending_session_id.as_ref() {
|
|
1020
|
+
state.insert(
|
|
1021
|
+
"_pending_session_id".to_string(),
|
|
1022
|
+
serde_json::json!(session_id.as_str()),
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
if let Some(root) = started_agent.provider_projects_root.as_ref() {
|
|
1026
|
+
state.insert(
|
|
1027
|
+
"claude_projects_root".to_string(),
|
|
1028
|
+
serde_json::json!(root.to_string_lossy().to_string()),
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
if started_agent.managed_mcp_config {
|
|
1032
|
+
state.insert("managed_mcp_config".to_string(), serde_json::json!(true));
|
|
1033
|
+
}
|
|
1034
|
+
if started_agent.managed_mcp_config
|
|
1035
|
+
|| started_agent.claude_config_dir.is_some()
|
|
1036
|
+
|| started_agent.provider_projects_root.is_some()
|
|
1037
|
+
{
|
|
1038
|
+
state.insert(
|
|
1039
|
+
"profile_launch".to_string(),
|
|
1040
|
+
serde_json::json!({
|
|
1041
|
+
"managed_mcp_config": started_agent.managed_mcp_config,
|
|
1042
|
+
"claude_config_dir": started_agent.claude_config_dir.as_ref().map(|path| path.to_string_lossy().to_string()),
|
|
1043
|
+
"claude_projects_root": started_agent.provider_projects_root.as_ref().map(|path| path.to_string_lossy().to_string()),
|
|
1044
|
+
}),
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
pub(crate) fn persist_command_plan_state(
|
|
1050
|
+
state: &mut serde_json::Map<String, serde_json::Value>,
|
|
1051
|
+
plan: &crate::provider::CommandPlan,
|
|
1052
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
1053
|
+
) {
|
|
1054
|
+
if let Some(session_id) = plan.expected_session_id.as_ref() {
|
|
1055
|
+
state.insert(
|
|
1056
|
+
"_pending_session_id".to_string(),
|
|
1057
|
+
serde_json::json!(session_id.as_str()),
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
let projects_root = plan
|
|
1061
|
+
.provider_projects_root
|
|
1062
|
+
.as_ref()
|
|
1063
|
+
.or(profile_launch.claude_projects_root.as_ref());
|
|
1064
|
+
if let Some(root) = projects_root {
|
|
1065
|
+
state.insert(
|
|
1066
|
+
"claude_projects_root".to_string(),
|
|
1067
|
+
serde_json::json!(root.to_string_lossy().to_string()),
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
let managed_mcp_config = plan.managed_mcp_config || profile_launch.managed_mcp_config;
|
|
1071
|
+
if managed_mcp_config {
|
|
1072
|
+
state.insert("managed_mcp_config".to_string(), serde_json::json!(true));
|
|
1073
|
+
}
|
|
1074
|
+
if managed_mcp_config || profile_launch.claude_config_dir.is_some() || projects_root.is_some() {
|
|
1075
|
+
state.insert(
|
|
1076
|
+
"profile_launch".to_string(),
|
|
1077
|
+
serde_json::json!({
|
|
1078
|
+
"managed_mcp_config": managed_mcp_config,
|
|
1079
|
+
"claude_config_dir": profile_launch.claude_config_dir.as_ref().map(|path| path.to_string_lossy().to_string()),
|
|
1080
|
+
"claude_projects_root": projects_root.map(|path| path.to_string_lossy().to_string()),
|
|
1081
|
+
}),
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
811
1086
|
fn is_posix_shell_identifier(name: &str) -> bool {
|
|
812
1087
|
let mut chars = name.chars();
|
|
813
1088
|
match chars.next() {
|
|
@@ -834,7 +1109,10 @@ pub(crate) fn fill_spawn_placeholders_full(
|
|
|
834
1109
|
*arg = workspace_text.clone();
|
|
835
1110
|
} else if arg == "{agent_id}" {
|
|
836
1111
|
*arg = agent_id.to_string();
|
|
837
|
-
} else if arg.contains("{workspace}")
|
|
1112
|
+
} else if arg.contains("{workspace}")
|
|
1113
|
+
|| arg.contains("{agent_id}")
|
|
1114
|
+
|| arg.contains("{team_id}")
|
|
1115
|
+
{
|
|
838
1116
|
*arg = arg
|
|
839
1117
|
.replace("{workspace}", &workspace_text)
|
|
840
1118
|
.replace("{agent_id}", agent_id)
|
|
@@ -843,30 +1121,12 @@ pub(crate) fn fill_spawn_placeholders_full(
|
|
|
843
1121
|
}
|
|
844
1122
|
}
|
|
845
1123
|
|
|
846
|
-
fn agent_tool_strings(agent: &Value) -> Vec<String> {
|
|
847
|
-
agent
|
|
848
|
-
.get("tools")
|
|
849
|
-
.and_then(Value::as_list)
|
|
850
|
-
.map(|items| {
|
|
851
|
-
items
|
|
852
|
-
.iter()
|
|
853
|
-
.filter_map(Value::as_str)
|
|
854
|
-
.map(str::to_string)
|
|
855
|
-
.collect()
|
|
856
|
-
})
|
|
857
|
-
.unwrap_or_default()
|
|
858
|
-
}
|
|
859
|
-
|
|
860
1124
|
fn spec_team_id(spec: &Value) -> Option<String> {
|
|
861
1125
|
spec.get("team")
|
|
862
1126
|
.and_then(|v| v.get("id").or_else(|| v.get("name")))
|
|
863
1127
|
.and_then(Value::as_str)
|
|
864
1128
|
.map(str::to_string)
|
|
865
|
-
.or_else(||
|
|
866
|
-
spec.get("name")
|
|
867
|
-
.and_then(Value::as_str)
|
|
868
|
-
.map(str::to_string)
|
|
869
|
-
})
|
|
1129
|
+
.or_else(|| spec.get("name").and_then(Value::as_str).map(str::to_string))
|
|
870
1130
|
}
|
|
871
1131
|
|
|
872
1132
|
fn runtime_active_team_key_for_spawn(
|
|
@@ -885,7 +1145,7 @@ fn explicit_active_team_key(state: &serde_json::Value) -> Option<String> {
|
|
|
885
1145
|
state
|
|
886
1146
|
.get("active_team_key")
|
|
887
1147
|
.and_then(serde_json::Value::as_str)
|
|
888
|
-
.filter(|team| !team.is_empty()
|
|
1148
|
+
.filter(|team| !team.is_empty())
|
|
889
1149
|
.map(str::to_string)
|
|
890
1150
|
}
|
|
891
1151
|
|
|
@@ -929,10 +1189,157 @@ fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
|
|
|
929
1189
|
}
|
|
930
1190
|
}
|
|
931
1191
|
|
|
932
|
-
fn quick_start_requested_team_key<'a>(
|
|
1192
|
+
fn quick_start_requested_team_key<'a>(
|
|
1193
|
+
team_id: Option<&'a str>,
|
|
1194
|
+
name: Option<&'a str>,
|
|
1195
|
+
) -> Option<&'a str> {
|
|
933
1196
|
team_id.or(name).filter(|team| !team.is_empty())
|
|
934
1197
|
}
|
|
935
1198
|
|
|
1199
|
+
struct QuickStartDepth {
|
|
1200
|
+
parent_team_key: Option<String>,
|
|
1201
|
+
team_depth: u64,
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
fn quick_start_depth_guard(
|
|
1205
|
+
workspace: &Path,
|
|
1206
|
+
_agents_dir: &Path,
|
|
1207
|
+
requested_team: Option<&str>,
|
|
1208
|
+
_strict_real_runtime: bool,
|
|
1209
|
+
) -> Result<QuickStartDepth, LifecycleError> {
|
|
1210
|
+
let env_parent = std::env::var("TEAM_AGENT_OWNER_TEAM_ID")
|
|
1211
|
+
.ok()
|
|
1212
|
+
.map(|value| value.trim().to_string())
|
|
1213
|
+
.filter(|value| !value.is_empty());
|
|
1214
|
+
let parent = env_parent;
|
|
1215
|
+
let Some(parent) = parent else {
|
|
1216
|
+
let state = crate::state::persist::load_runtime_state(workspace)
|
|
1217
|
+
.unwrap_or_else(|_| serde_json::json!({}));
|
|
1218
|
+
let ambiguous_nested_intent = requested_team.is_some_and(|team| {
|
|
1219
|
+
looks_ambiguous_child_team_key(team) || looks_grandchild_team_key(team)
|
|
1220
|
+
});
|
|
1221
|
+
if has_live_runtime_teams(&state) && ambiguous_nested_intent {
|
|
1222
|
+
if requested_team.is_some_and(looks_grandchild_team_key) {
|
|
1223
|
+
if let Some(parent_key) = infer_parent_team_from_active_state(&state) {
|
|
1224
|
+
let parent_state =
|
|
1225
|
+
crate::state::projection::project_top_level_view(&state, &parent_key);
|
|
1226
|
+
let parent_depth = parent_state
|
|
1227
|
+
.get("team_depth")
|
|
1228
|
+
.and_then(serde_json::Value::as_u64)
|
|
1229
|
+
.unwrap_or(1);
|
|
1230
|
+
return Ok(QuickStartDepth {
|
|
1231
|
+
parent_team_key: Some(parent_key),
|
|
1232
|
+
team_depth: parent_depth.saturating_add(1),
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return Err(LifecycleError::RequirementUnmet(
|
|
1237
|
+
"cannot infer parent team for nested quick-start; pass an explicit worker/subleader owner context"
|
|
1238
|
+
.to_string(),
|
|
1239
|
+
));
|
|
1240
|
+
}
|
|
1241
|
+
return Ok(QuickStartDepth {
|
|
1242
|
+
parent_team_key: None,
|
|
1243
|
+
team_depth: 1,
|
|
1244
|
+
});
|
|
1245
|
+
};
|
|
1246
|
+
let state = crate::state::persist::load_runtime_state(workspace)
|
|
1247
|
+
.unwrap_or_else(|_| serde_json::json!({}));
|
|
1248
|
+
let parent_key = crate::state::projection::resolve_owner_team_id(&state, &parent)
|
|
1249
|
+
.canonical_key()
|
|
1250
|
+
.map(str::to_string)
|
|
1251
|
+
.unwrap_or(parent);
|
|
1252
|
+
let parent_state = crate::state::projection::project_top_level_view(&state, &parent_key);
|
|
1253
|
+
let parent_depth = parent_state
|
|
1254
|
+
.get("team_depth")
|
|
1255
|
+
.and_then(serde_json::Value::as_u64)
|
|
1256
|
+
.unwrap_or(1);
|
|
1257
|
+
let team_depth = parent_depth.saturating_add(1);
|
|
1258
|
+
Ok(QuickStartDepth {
|
|
1259
|
+
parent_team_key: Some(parent_key),
|
|
1260
|
+
team_depth,
|
|
1261
|
+
})
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
fn infer_parent_team_from_active_state(state: &serde_json::Value) -> Option<String> {
|
|
1265
|
+
let active = explicit_active_team_key(state)?;
|
|
1266
|
+
let team = state
|
|
1267
|
+
.get("teams")
|
|
1268
|
+
.and_then(serde_json::Value::as_object)
|
|
1269
|
+
.and_then(|teams| teams.get(&active))?;
|
|
1270
|
+
team_has_running_agent(team).then_some(active)
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
fn has_live_runtime_teams(state: &serde_json::Value) -> bool {
|
|
1274
|
+
state
|
|
1275
|
+
.get("teams")
|
|
1276
|
+
.and_then(serde_json::Value::as_object)
|
|
1277
|
+
.is_some_and(|teams| teams.values().any(team_has_running_agent))
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
fn team_has_running_agent(team: &serde_json::Value) -> bool {
|
|
1281
|
+
team.get("agents")
|
|
1282
|
+
.and_then(serde_json::Value::as_object)
|
|
1283
|
+
.is_some_and(|agents| {
|
|
1284
|
+
agents.values().any(|agent| {
|
|
1285
|
+
agent.get("status").and_then(serde_json::Value::as_str) == Some("running")
|
|
1286
|
+
})
|
|
1287
|
+
})
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
fn looks_ambiguous_child_team_key(team: &str) -> bool {
|
|
1291
|
+
let team = team.trim().to_ascii_lowercase();
|
|
1292
|
+
team != "child"
|
|
1293
|
+
&& (team.starts_with("child-")
|
|
1294
|
+
|| team.starts_with("child_")
|
|
1295
|
+
|| team.starts_with("child.")
|
|
1296
|
+
|| team.starts_with("child"))
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
fn looks_grandchild_team_key(team: &str) -> bool {
|
|
1300
|
+
let team = team.trim().to_ascii_lowercase();
|
|
1301
|
+
team == "grandchild"
|
|
1302
|
+
|| team.starts_with("grandchild-")
|
|
1303
|
+
|| team.starts_with("grandchild_")
|
|
1304
|
+
|| team.starts_with("grandchild.")
|
|
1305
|
+
|| team.starts_with("grandchild")
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
fn annotate_team_depth(
|
|
1309
|
+
state: &mut serde_json::Value,
|
|
1310
|
+
parent_team_key: Option<&str>,
|
|
1311
|
+
team_depth: u64,
|
|
1312
|
+
) {
|
|
1313
|
+
let Some(obj) = state.as_object_mut() else {
|
|
1314
|
+
return;
|
|
1315
|
+
};
|
|
1316
|
+
obj.insert("team_depth".to_string(), serde_json::json!(team_depth));
|
|
1317
|
+
if let Some(parent) = parent_team_key.filter(|value| !value.is_empty()) {
|
|
1318
|
+
obj.insert("parent_team_key".to_string(), serde_json::json!(parent));
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
fn annotate_persisted_team_depth(
|
|
1323
|
+
workspace: &Path,
|
|
1324
|
+
team_key: &str,
|
|
1325
|
+
parent_team_key: Option<&str>,
|
|
1326
|
+
team_depth: u64,
|
|
1327
|
+
) -> Result<(), LifecycleError> {
|
|
1328
|
+
let mut state = crate::state::persist::load_runtime_state(workspace)
|
|
1329
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1330
|
+
let Some(team) = state
|
|
1331
|
+
.get_mut("teams")
|
|
1332
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
1333
|
+
.and_then(|teams| teams.get_mut(team_key))
|
|
1334
|
+
else {
|
|
1335
|
+
return Ok(());
|
|
1336
|
+
};
|
|
1337
|
+
annotate_team_depth(team, parent_team_key, team_depth);
|
|
1338
|
+
crate::state::persist::save_runtime_state(workspace, &state)
|
|
1339
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1340
|
+
Ok(())
|
|
1341
|
+
}
|
|
1342
|
+
|
|
936
1343
|
fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) -> bool {
|
|
937
1344
|
explicit_active_team_key(state).as_deref() == Some(team)
|
|
938
1345
|
|| state
|
|
@@ -949,9 +1356,7 @@ fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) ->
|
|
|
949
1356
|
|| state
|
|
950
1357
|
.get("session_name")
|
|
951
1358
|
.and_then(serde_json::Value::as_str)
|
|
952
|
-
.is_some_and(|session|
|
|
953
|
-
session == team || session.strip_prefix("team-") == Some(team)
|
|
954
|
-
})
|
|
1359
|
+
.is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
|
|
955
1360
|
}
|
|
956
1361
|
|
|
957
1362
|
fn json_team_identity_matches(state: &serde_json::Value, team: &str) -> bool {
|
|
@@ -988,8 +1393,7 @@ pub fn quick_start_in_workspace(
|
|
|
988
1393
|
fresh: bool,
|
|
989
1394
|
team_id: Option<&str>,
|
|
990
1395
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
991
|
-
let workspace =
|
|
992
|
-
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1396
|
+
let workspace = explicit_quick_start_workspace(workspace);
|
|
993
1397
|
quick_start_with_transport_in_workspace(
|
|
994
1398
|
&workspace,
|
|
995
1399
|
agents_dir,
|
|
@@ -1002,6 +1406,18 @@ pub fn quick_start_in_workspace(
|
|
|
1002
1406
|
)
|
|
1003
1407
|
}
|
|
1004
1408
|
|
|
1409
|
+
fn explicit_quick_start_workspace(workspace: &Path) -> PathBuf {
|
|
1410
|
+
std::fs::canonicalize(workspace).unwrap_or_else(|_| {
|
|
1411
|
+
if workspace.is_absolute() {
|
|
1412
|
+
workspace.to_path_buf()
|
|
1413
|
+
} else {
|
|
1414
|
+
std::env::current_dir()
|
|
1415
|
+
.unwrap_or_else(|_| PathBuf::from("."))
|
|
1416
|
+
.join(workspace)
|
|
1417
|
+
}
|
|
1418
|
+
})
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1005
1421
|
/// `quick_start` with an injected transport — tests inject a recording mock so the REAL spawn path
|
|
1006
1422
|
/// (launch dry_run=false → spawn_agents) is asserted without a live tmux; prod uses the real TmuxBackend.
|
|
1007
1423
|
pub fn quick_start_with_transport(
|
|
@@ -1013,7 +1429,9 @@ pub fn quick_start_with_transport(
|
|
|
1013
1429
|
transport: &dyn Transport,
|
|
1014
1430
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
1015
1431
|
let workspace = team_workspace(agents_dir);
|
|
1016
|
-
quick_start_with_transport_in_workspace(
|
|
1432
|
+
quick_start_with_transport_in_workspace(
|
|
1433
|
+
&workspace, agents_dir, name, yes, fresh, team_id, transport,
|
|
1434
|
+
)
|
|
1017
1435
|
}
|
|
1018
1436
|
|
|
1019
1437
|
pub fn quick_start_with_transport_in_workspace(
|
|
@@ -1038,6 +1456,19 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1038
1456
|
.map(str::to_string)
|
|
1039
1457
|
.or_else(|| spec_team_id(&spec));
|
|
1040
1458
|
let explicit_team_key = quick_start_requested_team_key(team_id, name).map(str::to_string);
|
|
1459
|
+
let team_depth = quick_start_depth_guard(
|
|
1460
|
+
&workspace,
|
|
1461
|
+
agents_dir,
|
|
1462
|
+
requested_team.as_deref(),
|
|
1463
|
+
matches!(transport.kind(), crate::transport::BackendKind::Tmux),
|
|
1464
|
+
)?;
|
|
1465
|
+
if team_depth.team_depth > 2 {
|
|
1466
|
+
let parent = team_depth.parent_team_key.as_deref().unwrap_or("");
|
|
1467
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
1468
|
+
"team nesting depth limit exceeded: parent_team_key={parent} parent_depth={} max_depth=2",
|
|
1469
|
+
team_depth.team_depth.saturating_sub(1)
|
|
1470
|
+
)));
|
|
1471
|
+
}
|
|
1041
1472
|
if !fresh {
|
|
1042
1473
|
let state_path = crate::state::persist::runtime_state_path(&workspace);
|
|
1043
1474
|
if state_path.exists() {
|
|
@@ -1056,7 +1487,8 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1056
1487
|
.map(SessionName::new),
|
|
1057
1488
|
state_path: Some(state_path),
|
|
1058
1489
|
next_actions: vec![
|
|
1059
|
-
"run restart to resume the existing team or pass --fresh to replace it"
|
|
1490
|
+
"run restart to resume the existing team or pass --fresh to replace it"
|
|
1491
|
+
.to_string(),
|
|
1060
1492
|
],
|
|
1061
1493
|
});
|
|
1062
1494
|
}
|
|
@@ -1070,22 +1502,41 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1070
1502
|
if let Some(requested) = requested_team.as_deref() {
|
|
1071
1503
|
override_spec_session_name(&mut spec, &format!("team-{requested}"));
|
|
1072
1504
|
}
|
|
1505
|
+
let session_name = spec_session_name(&spec);
|
|
1506
|
+
let state_team_key = explicit_team_key.clone().unwrap_or_else(|| {
|
|
1507
|
+
let spec_path = agents_dir.join("team.spec.yaml");
|
|
1508
|
+
runtime_team_key_for_spec(&spec_path, &spec, &session_name)
|
|
1509
|
+
});
|
|
1073
1510
|
let spec_path = agents_dir.join("team.spec.yaml");
|
|
1074
|
-
std::fs::write(&spec_path, yaml::dumps(&spec))
|
|
1075
|
-
LifecycleError::StatePersist(format!("{}: {e}", spec_path.display()))
|
|
1076
|
-
})?;
|
|
1511
|
+
std::fs::write(&spec_path, yaml::dumps(&spec))
|
|
1512
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", spec_path.display())))?;
|
|
1077
1513
|
let _store = crate::message_store::MessageStore::open(&workspace)
|
|
1078
1514
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1079
|
-
let
|
|
1080
|
-
|
|
1515
|
+
let resolved_spec_path =
|
|
1516
|
+
std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
|
|
1081
1517
|
let state = initial_runtime_state(&spec, &resolved_spec_path, &workspace, agents_dir);
|
|
1082
|
-
let state_team_key = explicit_team_key
|
|
1083
|
-
.unwrap_or_else(|| runtime_team_key_for_spec(&resolved_spec_path, &spec, &session_name));
|
|
1084
1518
|
save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
|
|
1519
|
+
annotate_persisted_team_depth(
|
|
1520
|
+
&workspace,
|
|
1521
|
+
&state_team_key,
|
|
1522
|
+
team_depth.parent_team_key.as_deref(),
|
|
1523
|
+
team_depth.team_depth,
|
|
1524
|
+
)?;
|
|
1085
1525
|
// FIX (rt-host-a real-machine finding): dry_run=false so launch_with_transport calls spawn_agents
|
|
1086
1526
|
// and really creates the tmux session + worker windows (was hardcoded true → never spawned, which
|
|
1087
1527
|
// also starved the coordinator: no session → first tick TmuxSessionMissing → run_daemon loop exits).
|
|
1088
|
-
let launch =
|
|
1528
|
+
let mut launch =
|
|
1529
|
+
launch_with_transport_in_workspace(&workspace, &spec_path, false, yes, true, transport)?;
|
|
1530
|
+
annotate_persisted_team_depth(
|
|
1531
|
+
&workspace,
|
|
1532
|
+
&state_team_key,
|
|
1533
|
+
team_depth.parent_team_key.as_deref(),
|
|
1534
|
+
team_depth.team_depth,
|
|
1535
|
+
)?;
|
|
1536
|
+
launch.leader_receiver_attached =
|
|
1537
|
+
launched_team_receiver_is_attached(&workspace, &state_team_key);
|
|
1538
|
+
launch.session_capture_incomplete_agents =
|
|
1539
|
+
quick_start_session_capture_incomplete_agents(&workspace, &state_team_key);
|
|
1089
1540
|
let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.clone());
|
|
1090
1541
|
let coordinator_started = crate::coordinator::start_coordinator(&coordinator_workspace)
|
|
1091
1542
|
.map(|report| report.ok)
|
|
@@ -1102,13 +1553,30 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1102
1553
|
// loaded successfully (provider-side codex/claude schema rejections happen
|
|
1103
1554
|
// asynchronously after spawn), so the verdict is PendingToolLoad — never
|
|
1104
1555
|
// bare Ready.
|
|
1105
|
-
let worker_readiness = quick_start_worker_readiness(&workspace);
|
|
1556
|
+
let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
|
|
1557
|
+
let attach_commands = crate::tmux_backend::attach_commands_for_windows(
|
|
1558
|
+
&workspace,
|
|
1559
|
+
&session_name,
|
|
1560
|
+
launch
|
|
1561
|
+
.started
|
|
1562
|
+
.iter()
|
|
1563
|
+
.map(|started| started.agent_id.as_str()),
|
|
1564
|
+
);
|
|
1565
|
+
let mut next_actions = vec![format!(
|
|
1566
|
+
"team compiled; real spawn is behind the transport/provider boundary; {coordinator_action}"
|
|
1567
|
+
)];
|
|
1568
|
+
next_actions.extend(attach_commands.iter().cloned());
|
|
1569
|
+
let display_backend = state
|
|
1570
|
+
.get("display_backend")
|
|
1571
|
+
.and_then(serde_json::Value::as_str)
|
|
1572
|
+
.unwrap_or("none")
|
|
1573
|
+
.to_string();
|
|
1106
1574
|
Ok(QuickStartReport::Ready {
|
|
1107
1575
|
session_name,
|
|
1108
1576
|
launch: Box::new(launch),
|
|
1109
|
-
next_actions
|
|
1110
|
-
|
|
1111
|
-
|
|
1577
|
+
next_actions,
|
|
1578
|
+
attach_commands,
|
|
1579
|
+
display_backend,
|
|
1112
1580
|
worker_readiness,
|
|
1113
1581
|
})
|
|
1114
1582
|
}
|
|
@@ -1118,13 +1586,24 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1118
1586
|
/// verdict to `Degraded { unhealthy_agents }` (sorted, deduped); otherwise
|
|
1119
1587
|
/// `PendingToolLoad` — never bare Ready. State read failure is treated as
|
|
1120
1588
|
/// PendingToolLoad rather than fabricated success.
|
|
1121
|
-
fn quick_start_worker_readiness(workspace: &Path) -> QuickStartReadiness {
|
|
1589
|
+
fn quick_start_worker_readiness(workspace: &Path, team_key: &str) -> QuickStartReadiness {
|
|
1122
1590
|
let Ok(state) = load_runtime_state(workspace) else {
|
|
1123
1591
|
return QuickStartReadiness::PendingToolLoad;
|
|
1124
1592
|
};
|
|
1125
|
-
let
|
|
1593
|
+
let team_state = state
|
|
1594
|
+
.get("teams")
|
|
1595
|
+
.and_then(serde_json::Value::as_object)
|
|
1596
|
+
.and_then(|teams| teams.get(team_key))
|
|
1597
|
+
.unwrap_or(&state);
|
|
1598
|
+
let Some(agents) = team_state
|
|
1599
|
+
.get("agents")
|
|
1600
|
+
.and_then(serde_json::Value::as_object)
|
|
1601
|
+
else {
|
|
1126
1602
|
return QuickStartReadiness::PendingToolLoad;
|
|
1127
1603
|
};
|
|
1604
|
+
let all_spawned = !agents.is_empty();
|
|
1605
|
+
let leader_receiver_attached = launched_team_receiver_is_attached(workspace, team_key);
|
|
1606
|
+
let all_attached_receiver = leader_receiver_attached;
|
|
1128
1607
|
let mut unhealthy: Vec<String> = agents
|
|
1129
1608
|
.iter()
|
|
1130
1609
|
.filter_map(|(id, agent)| {
|
|
@@ -1135,22 +1614,88 @@ fn quick_start_worker_readiness(workspace: &Path) -> QuickStartReadiness {
|
|
|
1135
1614
|
}
|
|
1136
1615
|
})
|
|
1137
1616
|
.collect();
|
|
1138
|
-
if unhealthy.is_empty() {
|
|
1139
|
-
QuickStartReadiness::PendingToolLoad
|
|
1140
|
-
} else {
|
|
1617
|
+
if !unhealthy.is_empty() {
|
|
1141
1618
|
unhealthy.sort();
|
|
1142
1619
|
unhealthy.dedup();
|
|
1143
|
-
QuickStartReadiness::Degraded {
|
|
1620
|
+
QuickStartReadiness::Degraded {
|
|
1621
|
+
unhealthy_agents: unhealthy,
|
|
1622
|
+
}
|
|
1623
|
+
} else {
|
|
1624
|
+
let incomplete_agents =
|
|
1625
|
+
crate::session_capture::incomplete_interacted_resumable_agent_ids(team_state);
|
|
1626
|
+
let all_resumable_have_session = incomplete_agents.is_empty();
|
|
1627
|
+
let _readiness_ready = all_spawned && all_attached_receiver && all_resumable_have_session;
|
|
1628
|
+
QuickStartReadiness::PendingToolLoad
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
fn quick_start_session_capture_incomplete_agents(workspace: &Path, team_key: &str) -> Vec<String> {
|
|
1633
|
+
let Ok(state) = load_runtime_state(workspace) else {
|
|
1634
|
+
return Vec::new();
|
|
1635
|
+
};
|
|
1636
|
+
let team_state = state
|
|
1637
|
+
.get("teams")
|
|
1638
|
+
.and_then(serde_json::Value::as_object)
|
|
1639
|
+
.and_then(|teams| teams.get(team_key))
|
|
1640
|
+
.unwrap_or(&state);
|
|
1641
|
+
crate::session_capture::incomplete_interacted_resumable_agent_ids(team_state)
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
fn launched_team_receiver_is_attached(workspace: &Path, team_key: &str) -> bool {
|
|
1645
|
+
let Ok(state) = load_runtime_state(workspace) else {
|
|
1646
|
+
return true;
|
|
1647
|
+
};
|
|
1648
|
+
let team_state = state
|
|
1649
|
+
.get("teams")
|
|
1650
|
+
.and_then(serde_json::Value::as_object)
|
|
1651
|
+
.and_then(|teams| teams.get(team_key))
|
|
1652
|
+
.unwrap_or(&state);
|
|
1653
|
+
if team_state.get("leader_receiver").is_none() {
|
|
1654
|
+
return true;
|
|
1144
1655
|
}
|
|
1656
|
+
if team_uses_fake_model_harness(team_state) {
|
|
1657
|
+
return true;
|
|
1658
|
+
}
|
|
1659
|
+
leader_receiver_is_attached(team_state)
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
fn team_uses_fake_model_harness(team_state: &serde_json::Value) -> bool {
|
|
1663
|
+
team_state
|
|
1664
|
+
.get("agents")
|
|
1665
|
+
.and_then(serde_json::Value::as_object)
|
|
1666
|
+
.is_some_and(|agents| {
|
|
1667
|
+
!agents.is_empty()
|
|
1668
|
+
&& agents.values().all(|agent| {
|
|
1669
|
+
agent.get("model").and_then(serde_json::Value::as_str) == Some("fake")
|
|
1670
|
+
})
|
|
1671
|
+
})
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
fn leader_receiver_is_attached(team_state: &serde_json::Value) -> bool {
|
|
1675
|
+
let Some(receiver) = team_state.get("leader_receiver") else {
|
|
1676
|
+
return false;
|
|
1677
|
+
};
|
|
1678
|
+
let status = receiver
|
|
1679
|
+
.get("status")
|
|
1680
|
+
.and_then(serde_json::Value::as_str)
|
|
1681
|
+
.unwrap_or("");
|
|
1682
|
+
let pane_id = receiver
|
|
1683
|
+
.get("pane_id")
|
|
1684
|
+
.and_then(serde_json::Value::as_str)
|
|
1685
|
+
.or_else(|| receiver.get("pane").and_then(serde_json::Value::as_str))
|
|
1686
|
+
.unwrap_or("");
|
|
1687
|
+
status == "attached" && !pane_id.is_empty() && pane_id != "__team_agent_unbound__"
|
|
1145
1688
|
}
|
|
1146
1689
|
|
|
1147
1690
|
/// `detect_inherited_dangerous_permissions`(`launch/config.py`):扫进程祖先链找
|
|
1148
1691
|
/// `--dangerously-*` flag,产出危险审批继承态。launch 在 inherited=false 且无 --yes 时拒。
|
|
1149
1692
|
pub fn detect_dangerous_approval() -> Result<DangerousApproval, LifecycleError> {
|
|
1150
1693
|
if let Ok(raw) = std::env::var("TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON") {
|
|
1151
|
-
let argv_tokens = serde_json::from_str::<Vec<String>>(&raw)
|
|
1152
|
-
|
|
1153
|
-
|
|
1694
|
+
let argv_tokens = serde_json::from_str::<Vec<String>>(&raw).map_err(|e| {
|
|
1695
|
+
LifecycleError::StatePersist(format!("invalid test ancestry argv: {e}"))
|
|
1696
|
+
})?;
|
|
1697
|
+
return Ok(detect_dangerous_approval_in_argv(&argv_tokens)
|
|
1698
|
+
.unwrap_or_else(disabled_dangerous_approval));
|
|
1154
1699
|
}
|
|
1155
1700
|
for argv_tokens in process_ancestry_argv(std::process::id()) {
|
|
1156
1701
|
if let Some(detected) = detect_dangerous_approval_in_argv(&argv_tokens) {
|
|
@@ -1166,7 +1711,8 @@ fn detect_dangerous_approval_in_argv(argv_tokens: &[String]) -> Option<Dangerous
|
|
|
1166
1711
|
for token in argv_tokens {
|
|
1167
1712
|
for (provider, flag) in dangerous_leader_flags() {
|
|
1168
1713
|
if token == flag {
|
|
1169
|
-
let unexpected_binary =
|
|
1714
|
+
let unexpected_binary =
|
|
1715
|
+
!binary_matches_provider(provider, ancestry_binary_name.as_deref());
|
|
1170
1716
|
return Some(DangerousApproval {
|
|
1171
1717
|
enabled: true,
|
|
1172
1718
|
source: DangerousApprovalSource::LeaderProcess,
|
|
@@ -1356,16 +1902,16 @@ pub fn add_agent(
|
|
|
1356
1902
|
}
|
|
1357
1903
|
Err(error) => return Err(LifecycleError::TeamSelect(error.to_string())),
|
|
1358
1904
|
};
|
|
1359
|
-
let team_dir = selected
|
|
1360
|
-
.
|
|
1361
|
-
|
|
1905
|
+
let team_dir = selected.spec_workspace.ok_or_else(|| {
|
|
1906
|
+
LifecycleError::TeamSelect("active team spec workspace not found".to_string())
|
|
1907
|
+
})?;
|
|
1362
1908
|
add_agent_with_transport_at_paths(
|
|
1363
1909
|
&selected.run_workspace,
|
|
1364
1910
|
&team_dir,
|
|
1365
1911
|
agent_id,
|
|
1366
1912
|
role_file_path,
|
|
1367
1913
|
open_display,
|
|
1368
|
-
|
|
1914
|
+
Some(selected.team_key.as_str()),
|
|
1369
1915
|
&crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace),
|
|
1370
1916
|
)
|
|
1371
1917
|
}
|
|
@@ -1402,12 +1948,16 @@ fn add_agent_with_transport_at_paths(
|
|
|
1402
1948
|
team: Option<&str>,
|
|
1403
1949
|
transport: &dyn Transport,
|
|
1404
1950
|
) -> Result<AddAgentReport, LifecycleError> {
|
|
1405
|
-
let
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1951
|
+
let runtime_state = crate::state::persist::load_runtime_state(run_workspace)
|
|
1952
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1953
|
+
let canonical_team_key = team
|
|
1954
|
+
.filter(|key| !key.is_empty())
|
|
1955
|
+
.map(str::to_string)
|
|
1956
|
+
.or_else(|| explicit_active_team_key(&runtime_state))
|
|
1957
|
+
.unwrap_or_else(|| crate::state::projection::team_state_key(&runtime_state));
|
|
1958
|
+
let owner_state =
|
|
1959
|
+
crate::state::projection::select_runtime_state(run_workspace, Some(&canonical_team_key))
|
|
1960
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
1411
1961
|
ensure_owner_allowed_for_state(&owner_state, Some(agent_id))?;
|
|
1412
1962
|
if !role_file_path.exists() {
|
|
1413
1963
|
return Err(LifecycleError::Compile(format!(
|
|
@@ -1423,13 +1973,20 @@ fn add_agent_with_transport_at_paths(
|
|
|
1423
1973
|
let dynamic_role_file = materialize_added_role_file(team_dir, agent_id, role_file_path)?;
|
|
1424
1974
|
let spec = crate::compiler::compile_team(team_dir)
|
|
1425
1975
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1976
|
+
let safety = effective_runtime_config(&spec)?;
|
|
1426
1977
|
let spec_path = team_dir.join("team.spec.yaml");
|
|
1427
|
-
std::fs::write(&spec_path, yaml::dumps(&spec))
|
|
1428
|
-
LifecycleError::StatePersist(format!("{}: {e}", spec_path.display()))
|
|
1429
|
-
})?;
|
|
1978
|
+
std::fs::write(&spec_path, yaml::dumps(&spec))
|
|
1979
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", spec_path.display())))?;
|
|
1430
1980
|
let (meta, _) = crate::compiler::read_front_matter(&dynamic_role_file)
|
|
1431
1981
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1432
|
-
upsert_agent_state_from_role(
|
|
1982
|
+
upsert_agent_state_from_role(
|
|
1983
|
+
run_workspace,
|
|
1984
|
+
&canonical_team_key,
|
|
1985
|
+
agent_id,
|
|
1986
|
+
&meta,
|
|
1987
|
+
&dynamic_role_file,
|
|
1988
|
+
&safety,
|
|
1989
|
+
)?;
|
|
1433
1990
|
let started = crate::lifecycle::restart::start_agent_at_paths(
|
|
1434
1991
|
run_workspace,
|
|
1435
1992
|
team_dir,
|
|
@@ -1437,7 +1994,7 @@ fn add_agent_with_transport_at_paths(
|
|
|
1437
1994
|
false,
|
|
1438
1995
|
open_display,
|
|
1439
1996
|
true,
|
|
1440
|
-
|
|
1997
|
+
Some(&canonical_team_key),
|
|
1441
1998
|
transport,
|
|
1442
1999
|
)?;
|
|
1443
2000
|
let (env, start_mode) = match started {
|
|
@@ -1460,18 +2017,15 @@ fn add_agent_with_transport_at_paths(
|
|
|
1460
2017
|
|
|
1461
2018
|
fn upsert_agent_state_from_role(
|
|
1462
2019
|
workspace: &Path,
|
|
1463
|
-
|
|
2020
|
+
canonical_team_key: &str,
|
|
1464
2021
|
agent_id: &AgentId,
|
|
1465
2022
|
meta: &Value,
|
|
1466
2023
|
dynamic_role_file: &Path,
|
|
2024
|
+
safety: &DangerousApproval,
|
|
1467
2025
|
) -> Result<(), LifecycleError> {
|
|
1468
|
-
let mut state =
|
|
1469
|
-
crate::state::projection::select_runtime_state(workspace,
|
|
1470
|
-
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))
|
|
1471
|
-
} else {
|
|
1472
|
-
crate::state::persist::load_runtime_state(workspace)
|
|
1473
|
-
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?
|
|
1474
|
-
};
|
|
2026
|
+
let mut state =
|
|
2027
|
+
crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
|
|
2028
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
1475
2029
|
if !state.is_object() {
|
|
1476
2030
|
state = serde_json::json!({});
|
|
1477
2031
|
}
|
|
@@ -1513,16 +2067,28 @@ fn upsert_agent_state_from_role(
|
|
|
1513
2067
|
if let Some(model) = meta.get("model").and_then(Value::as_str) {
|
|
1514
2068
|
if let Some(obj) = entry.as_object_mut() {
|
|
1515
2069
|
obj.insert("model".to_string(), serde_json::json!(model));
|
|
2070
|
+
obj.insert("model_source".to_string(), serde_json::json!("role"));
|
|
1516
2071
|
}
|
|
1517
2072
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
2073
|
+
if let Some(profile) = meta.get("profile").and_then(Value::as_str) {
|
|
2074
|
+
if let Some(obj) = entry.as_object_mut() {
|
|
2075
|
+
obj.insert("profile".to_string(), serde_json::json!(profile));
|
|
2076
|
+
if let Some(team_dir) = dynamic_role_file.parent().and_then(Path::parent) {
|
|
2077
|
+
obj.insert(
|
|
2078
|
+
"_profile_dir".to_string(),
|
|
2079
|
+
serde_json::json!(team_dir.join("profiles").to_string_lossy().to_string()),
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
if !obj.contains_key("model_source") {
|
|
2083
|
+
obj.insert("model_source".to_string(), serde_json::json!("default"));
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
1525
2086
|
}
|
|
2087
|
+
if let Some(obj) = entry.as_object_mut() {
|
|
2088
|
+
persist_effective_approval_policy(obj, safety);
|
|
2089
|
+
}
|
|
2090
|
+
agent_map.insert(agent_id.as_str().to_string(), entry);
|
|
2091
|
+
save_launched_team_state_for_key(workspace, &state, Some(canonical_team_key))
|
|
1526
2092
|
}
|
|
1527
2093
|
|
|
1528
2094
|
fn materialize_added_role_file(
|
|
@@ -1588,9 +2154,9 @@ pub fn fork_agent_with_transport(
|
|
|
1588
2154
|
crate::state::selector::SelectorMode::RequireSpec,
|
|
1589
2155
|
)
|
|
1590
2156
|
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
1591
|
-
let spec_workspace = selected
|
|
1592
|
-
.
|
|
1593
|
-
|
|
2157
|
+
let spec_workspace = selected.spec_workspace.ok_or_else(|| {
|
|
2158
|
+
LifecycleError::TeamSelect("active team spec workspace not found".to_string())
|
|
2159
|
+
})?;
|
|
1594
2160
|
let workspace = selected.run_workspace;
|
|
1595
2161
|
let state = selected.state;
|
|
1596
2162
|
ensure_owner_allowed_for_state(&state, Some(source_agent_id))?;
|
|
@@ -1603,8 +2169,9 @@ pub fn fork_agent_with_transport(
|
|
|
1603
2169
|
"agent id already exists: {as_agent_id}"
|
|
1604
2170
|
)));
|
|
1605
2171
|
}
|
|
1606
|
-
let source_agent = find_spec_agent(&spec, source_agent_id)
|
|
1607
|
-
|
|
2172
|
+
let source_agent = find_spec_agent(&spec, source_agent_id).ok_or_else(|| {
|
|
2173
|
+
LifecycleError::RequirementUnmet(format!("unknown worker agent id: {source_agent_id}"))
|
|
2174
|
+
})?;
|
|
1608
2175
|
let session_id = state
|
|
1609
2176
|
.get("agents")
|
|
1610
2177
|
.and_then(|v| v.get(source_agent_id.as_str()))
|
|
@@ -1639,8 +2206,9 @@ pub fn fork_agent_with_transport(
|
|
|
1639
2206
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1640
2207
|
std::fs::write(&spec_path, yaml::dumps(&new_spec))
|
|
1641
2208
|
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", spec_path.display())))?;
|
|
1642
|
-
let new_agent = find_spec_agent(&new_spec, as_agent_id)
|
|
1643
|
-
|
|
2209
|
+
let new_agent = find_spec_agent(&new_spec, as_agent_id).ok_or_else(|| {
|
|
2210
|
+
LifecycleError::RequirementUnmet(format!("unknown worker agent id: {as_agent_id}"))
|
|
2211
|
+
})?;
|
|
1644
2212
|
let provider = new_agent
|
|
1645
2213
|
.get("provider")
|
|
1646
2214
|
.and_then(Value::as_str)
|
|
@@ -1662,68 +2230,171 @@ pub fn fork_agent_with_transport(
|
|
|
1662
2230
|
"{provider_str} does not support native session fork"
|
|
1663
2231
|
)));
|
|
1664
2232
|
}
|
|
1665
|
-
let role = new_agent.get("role").and_then(Value::as_str);
|
|
1666
2233
|
let model = new_agent.get("model").and_then(Value::as_str);
|
|
1667
2234
|
let safety = effective_runtime_config(&new_spec)?;
|
|
1668
|
-
let
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
2235
|
+
let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
|
|
2236
|
+
new_agent,
|
|
2237
|
+
Some(as_agent_id.as_str()),
|
|
2238
|
+
provider,
|
|
2239
|
+
);
|
|
2240
|
+
let system_prompt =
|
|
2241
|
+
crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
|
|
2242
|
+
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
2243
|
+
&command_agent,
|
|
2244
|
+
provider,
|
|
2245
|
+
&safety,
|
|
2246
|
+
)?;
|
|
2247
|
+
let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
2248
|
+
let fork_team = crate::messaging::leader_receiver::active_team_key(&workspace, &state);
|
|
2249
|
+
let mcp_config = adapter.mcp_config(auth_mode).map_err(|e| {
|
|
2250
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
2251
|
+
LifecycleError::Provider(e.to_string())
|
|
2252
|
+
})?;
|
|
2253
|
+
let mcp_config = resolve_mcp_config(mcp_config, &workspace, as_agent_id.as_str(), &fork_team);
|
|
2254
|
+
let mcp_config_path = write_worker_mcp_config(&workspace, as_agent_id.as_str(), &mcp_config)
|
|
1672
2255
|
.map_err(|e| {
|
|
1673
2256
|
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
1674
|
-
|
|
2257
|
+
e
|
|
1675
2258
|
})?;
|
|
1676
|
-
let
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
2259
|
+
let profile_dir = spec_workspace.join("profiles");
|
|
2260
|
+
let profile_launch =
|
|
2261
|
+
crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
|
|
2262
|
+
&workspace,
|
|
2263
|
+
as_agent_id.as_str(),
|
|
2264
|
+
new_agent,
|
|
2265
|
+
Some(&profile_dir),
|
|
1680
2266
|
Some(&mcp_config),
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
2267
|
+
)?;
|
|
2268
|
+
let command_model = profile_launch.command_overrides.model.as_deref().or(model);
|
|
2269
|
+
let mut plan = adapter
|
|
2270
|
+
.fork_plan(
|
|
2271
|
+
Some(&session_id),
|
|
2272
|
+
crate::provider::ProviderCommandContext {
|
|
2273
|
+
auth_mode,
|
|
2274
|
+
mcp_config: Some(&mcp_config),
|
|
2275
|
+
system_prompt: Some(system_prompt.as_str()),
|
|
2276
|
+
model: command_model,
|
|
2277
|
+
tools: &resolved_tool_refs,
|
|
2278
|
+
profile_launch: Some(&profile_launch),
|
|
2279
|
+
},
|
|
1684
2280
|
)
|
|
1685
2281
|
.map_err(|e| {
|
|
1686
2282
|
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
1687
2283
|
LifecycleError::Provider(e.to_string())
|
|
1688
2284
|
})?;
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
2285
|
+
if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
|
|
2286
|
+
point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
|
|
2287
|
+
}
|
|
2288
|
+
fill_spawn_placeholders_full(
|
|
2289
|
+
&mut plan.argv,
|
|
1694
2290
|
&workspace,
|
|
1695
2291
|
as_agent_id.as_str(),
|
|
1696
2292
|
Some(&fork_team),
|
|
1697
2293
|
);
|
|
2294
|
+
let window = WindowName::new(as_agent_id.as_str());
|
|
2295
|
+
// fork inherits the parent agent's owner team via runtime state (`active_team_key`).
|
|
2296
|
+
let mut env =
|
|
2297
|
+
inherited_env_with_team_overrides(&workspace, as_agent_id.as_str(), Some(&fork_team));
|
|
2298
|
+
apply_profile_launch_env(&mut env, &profile_launch);
|
|
1698
2299
|
// golden operations.py:336 -> _tmux_start_command_for_agent_window (runtime.py:1017-1020): branch on
|
|
1699
2300
|
// _tmux_session_exists — an ABSENT session => new-session (spawn_first), present => new-window
|
|
1700
2301
|
// (spawn_into). The Rust restart seam (restart.rs spawn_agent_window) uses the same branch.
|
|
1701
2302
|
let session_live = transport.has_session(&session_name).unwrap_or(false);
|
|
1702
2303
|
let spawn_result = if session_live {
|
|
1703
|
-
transport.spawn_into(&session_name, &window, &argv, &workspace, &env)
|
|
2304
|
+
transport.spawn_into(&session_name, &window, &plan.argv, &workspace, &env)
|
|
1704
2305
|
} else {
|
|
1705
|
-
transport.spawn_first(&session_name, &window, &argv, &workspace, &env)
|
|
2306
|
+
transport.spawn_first(&session_name, &window, &plan.argv, &workspace, &env)
|
|
1706
2307
|
};
|
|
1707
|
-
let
|
|
2308
|
+
let spawn = spawn_result.map_err(|e| {
|
|
1708
2309
|
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
1709
2310
|
LifecycleError::Transport(e.to_string())
|
|
1710
2311
|
})?;
|
|
1711
2312
|
let old_state = state.clone();
|
|
1712
2313
|
let mut next_state = state;
|
|
1713
|
-
upsert_forked_agent_state(
|
|
2314
|
+
upsert_forked_agent_state(
|
|
2315
|
+
&mut next_state,
|
|
2316
|
+
source_agent_id,
|
|
2317
|
+
as_agent_id,
|
|
2318
|
+
new_agent,
|
|
2319
|
+
&safety,
|
|
2320
|
+
&plan,
|
|
2321
|
+
&profile_launch,
|
|
2322
|
+
&spawn,
|
|
2323
|
+
&workspace,
|
|
2324
|
+
Some(&profile_dir),
|
|
2325
|
+
)?;
|
|
2326
|
+
if let Some(agent) = next_state
|
|
2327
|
+
.get_mut("agents")
|
|
2328
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
2329
|
+
.and_then(|agents| agents.get_mut(as_agent_id.as_str()))
|
|
2330
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
2331
|
+
{
|
|
2332
|
+
persist_effective_approval_policy(agent, &safety);
|
|
2333
|
+
}
|
|
2334
|
+
if let Err(e) = maybe_fail_fork_after_spawn("save_runtime_state") {
|
|
2335
|
+
rollback_fork_after_spawn(
|
|
2336
|
+
&workspace,
|
|
2337
|
+
&spec_path,
|
|
2338
|
+
&text,
|
|
2339
|
+
&old_state,
|
|
2340
|
+
transport,
|
|
2341
|
+
&session_name,
|
|
2342
|
+
&window,
|
|
2343
|
+
&mcp_config_path,
|
|
2344
|
+
as_agent_id,
|
|
2345
|
+
&profile_launch,
|
|
2346
|
+
);
|
|
2347
|
+
return Err(e);
|
|
2348
|
+
}
|
|
1714
2349
|
if let Err(e) = save_runtime_state(&workspace, &next_state) {
|
|
1715
|
-
rollback_fork_after_spawn(
|
|
2350
|
+
rollback_fork_after_spawn(
|
|
2351
|
+
&workspace,
|
|
2352
|
+
&spec_path,
|
|
2353
|
+
&text,
|
|
2354
|
+
&old_state,
|
|
2355
|
+
transport,
|
|
2356
|
+
&session_name,
|
|
2357
|
+
&window,
|
|
2358
|
+
&mcp_config_path,
|
|
2359
|
+
as_agent_id,
|
|
2360
|
+
&profile_launch,
|
|
2361
|
+
);
|
|
1716
2362
|
return Err(LifecycleError::StatePersist(e.to_string()));
|
|
1717
2363
|
}
|
|
1718
|
-
let
|
|
1719
|
-
|
|
1720
|
-
workspace
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
2364
|
+
if let Err(e) = maybe_fail_fork_after_spawn("start_coordinator") {
|
|
2365
|
+
rollback_fork_after_spawn(
|
|
2366
|
+
&workspace,
|
|
2367
|
+
&spec_path,
|
|
2368
|
+
&text,
|
|
2369
|
+
&old_state,
|
|
2370
|
+
transport,
|
|
2371
|
+
&session_name,
|
|
2372
|
+
&window,
|
|
2373
|
+
&mcp_config_path,
|
|
2374
|
+
as_agent_id,
|
|
2375
|
+
&profile_launch,
|
|
2376
|
+
);
|
|
2377
|
+
return Err(e);
|
|
2378
|
+
}
|
|
2379
|
+
let coordinator_started = crate::coordinator::start_coordinator(
|
|
2380
|
+
&crate::coordinator::WorkspacePath::new(workspace.to_path_buf()),
|
|
2381
|
+
)
|
|
2382
|
+
.map(|report| report.ok)
|
|
2383
|
+
.map_err(|e| {
|
|
2384
|
+
rollback_fork_after_spawn(
|
|
2385
|
+
&workspace,
|
|
2386
|
+
&spec_path,
|
|
2387
|
+
&text,
|
|
2388
|
+
&old_state,
|
|
2389
|
+
transport,
|
|
2390
|
+
&session_name,
|
|
2391
|
+
&window,
|
|
2392
|
+
&mcp_config_path,
|
|
2393
|
+
as_agent_id,
|
|
2394
|
+
&profile_launch,
|
|
2395
|
+
);
|
|
2396
|
+
LifecycleError::StatePersist(e.to_string())
|
|
2397
|
+
})?;
|
|
1727
2398
|
Ok(ForkAgentReport {
|
|
1728
2399
|
source_agent_id: source_agent_id.clone(),
|
|
1729
2400
|
new_agent_id: as_agent_id.clone(),
|
|
@@ -1744,6 +2415,9 @@ fn rollback_fork_after_spawn(
|
|
|
1744
2415
|
transport: &dyn Transport,
|
|
1745
2416
|
session_name: &SessionName,
|
|
1746
2417
|
window: &WindowName,
|
|
2418
|
+
mcp_config_path: &Path,
|
|
2419
|
+
agent_id: &AgentId,
|
|
2420
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
1747
2421
|
) {
|
|
1748
2422
|
let _ = transport.kill_window(&Target::SessionWindow {
|
|
1749
2423
|
session: session_name.clone(),
|
|
@@ -1751,6 +2425,40 @@ fn rollback_fork_after_spawn(
|
|
|
1751
2425
|
});
|
|
1752
2426
|
let _ = std::fs::write(spec_path, spec_text.as_bytes());
|
|
1753
2427
|
let _ = save_runtime_state(workspace, old_state);
|
|
2428
|
+
cleanup_fork_mcp_artifacts(workspace, agent_id, mcp_config_path, profile_launch);
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
fn maybe_fail_fork_after_spawn(step: &str) -> Result<(), LifecycleError> {
|
|
2432
|
+
let Ok(reason) = std::env::var("TEAM_AGENT_TEST_FAIL_FORK_AFTER_SPAWN") else {
|
|
2433
|
+
return Ok(());
|
|
2434
|
+
};
|
|
2435
|
+
if reason.is_empty() {
|
|
2436
|
+
return Ok(());
|
|
2437
|
+
}
|
|
2438
|
+
let should_fail = reason == step || (step == "start_coordinator" && reason == "coordinator");
|
|
2439
|
+
if !should_fail {
|
|
2440
|
+
return Ok(());
|
|
2441
|
+
}
|
|
2442
|
+
Err(LifecycleError::StatePersist(format!(
|
|
2443
|
+
"injected fork failure after spawn: {reason}"
|
|
2444
|
+
)))
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
fn cleanup_fork_mcp_artifacts(
|
|
2448
|
+
workspace: &Path,
|
|
2449
|
+
agent_id: &AgentId,
|
|
2450
|
+
mcp_config_path: &Path,
|
|
2451
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
2452
|
+
) {
|
|
2453
|
+
let _ = std::fs::remove_file(mcp_config_path);
|
|
2454
|
+
let _ = std::fs::remove_file(
|
|
2455
|
+
workspace
|
|
2456
|
+
.join(".team/runtime/provider-env")
|
|
2457
|
+
.join(format!("{}.env", agent_id.as_str())),
|
|
2458
|
+
);
|
|
2459
|
+
if let Some(config_dir) = profile_launch.claude_config_dir.as_ref() {
|
|
2460
|
+
let _ = std::fs::remove_dir_all(config_dir.parent().unwrap_or(config_dir));
|
|
2461
|
+
}
|
|
1754
2462
|
}
|
|
1755
2463
|
|
|
1756
2464
|
fn leader_id_matches(spec: &Value, agent_id: &AgentId) -> bool {
|
|
@@ -1771,16 +2479,13 @@ fn find_spec_agent<'a>(spec: &'a Value, agent_id: &AgentId) -> Option<&'a Value>
|
|
|
1771
2479
|
if leader_is_agent {
|
|
1772
2480
|
return None;
|
|
1773
2481
|
}
|
|
1774
|
-
spec.get("agents")
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
.map(|id| id == agent_id.as_str())
|
|
1782
|
-
.unwrap_or(false)
|
|
1783
|
-
})
|
|
2482
|
+
spec.get("agents")?.as_list()?.iter().find(|agent| {
|
|
2483
|
+
agent
|
|
2484
|
+
.get("id")
|
|
2485
|
+
.and_then(Value::as_str)
|
|
2486
|
+
.map(|id| id == agent_id.as_str())
|
|
2487
|
+
.unwrap_or(false)
|
|
2488
|
+
})
|
|
1784
2489
|
}
|
|
1785
2490
|
|
|
1786
2491
|
fn append_forked_agent(
|
|
@@ -1819,12 +2524,17 @@ fn append_forked_agent(
|
|
|
1819
2524
|
)?;
|
|
1820
2525
|
|
|
1821
2526
|
let Value::Map(pairs) = spec else {
|
|
1822
|
-
return Err(LifecycleError::Compile(
|
|
2527
|
+
return Err(LifecycleError::Compile(
|
|
2528
|
+
"spec root is not a map".to_string(),
|
|
2529
|
+
));
|
|
1823
2530
|
};
|
|
1824
2531
|
let mut out = Vec::new();
|
|
1825
2532
|
for (key, value) in pairs {
|
|
1826
2533
|
if key == "agents" {
|
|
1827
|
-
let mut agents = value
|
|
2534
|
+
let mut agents = value
|
|
2535
|
+
.as_list()
|
|
2536
|
+
.map(|items| items.to_vec())
|
|
2537
|
+
.unwrap_or_default();
|
|
1828
2538
|
agents.push(new_agent.clone());
|
|
1829
2539
|
out.push((key.clone(), Value::List(agents)));
|
|
1830
2540
|
} else if key == "runtime" {
|
|
@@ -1838,7 +2548,9 @@ fn append_forked_agent(
|
|
|
1838
2548
|
|
|
1839
2549
|
fn set_yaml_map_value(value: &mut Value, key: &str, next: Value) -> Result<(), LifecycleError> {
|
|
1840
2550
|
let Value::Map(pairs) = value else {
|
|
1841
|
-
return Err(LifecycleError::Compile(
|
|
2551
|
+
return Err(LifecycleError::Compile(
|
|
2552
|
+
"agent entry is not a map".to_string(),
|
|
2553
|
+
));
|
|
1842
2554
|
};
|
|
1843
2555
|
if let Some((_, existing)) = pairs.iter_mut().find(|(k, _)| k == key) {
|
|
1844
2556
|
*existing = next;
|
|
@@ -1857,10 +2569,15 @@ fn runtime_with_startup_agent(runtime: &Value, agent_id: &AgentId) -> Value {
|
|
|
1857
2569
|
for (key, value) in pairs {
|
|
1858
2570
|
if key == "startup_order" {
|
|
1859
2571
|
saw_startup = true;
|
|
1860
|
-
let mut order = value
|
|
1861
|
-
|
|
1862
|
-
.
|
|
1863
|
-
.
|
|
2572
|
+
let mut order = value
|
|
2573
|
+
.as_list()
|
|
2574
|
+
.map(|items| items.to_vec())
|
|
2575
|
+
.unwrap_or_default();
|
|
2576
|
+
let already_present = order.iter().any(|item| {
|
|
2577
|
+
item.as_str()
|
|
2578
|
+
.map(|id| id == agent_id.as_str())
|
|
2579
|
+
.unwrap_or(false)
|
|
2580
|
+
});
|
|
1864
2581
|
if !already_present {
|
|
1865
2582
|
order.push(Value::Str(agent_id.as_str().to_string()));
|
|
1866
2583
|
}
|
|
@@ -1883,6 +2600,12 @@ fn upsert_forked_agent_state(
|
|
|
1883
2600
|
source_agent_id: &AgentId,
|
|
1884
2601
|
as_agent_id: &AgentId,
|
|
1885
2602
|
spec_agent: &Value,
|
|
2603
|
+
safety: &DangerousApproval,
|
|
2604
|
+
plan: &crate::provider::CommandPlan,
|
|
2605
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
2606
|
+
spawn: &crate::transport::SpawnResult,
|
|
2607
|
+
spawn_cwd: &Path,
|
|
2608
|
+
profile_dir: Option<&Path>,
|
|
1886
2609
|
) -> Result<(), LifecycleError> {
|
|
1887
2610
|
if !state.is_object() {
|
|
1888
2611
|
*state = serde_json::json!({});
|
|
@@ -1907,15 +2630,71 @@ fn upsert_forked_agent_state(
|
|
|
1907
2630
|
.get("provider")
|
|
1908
2631
|
.and_then(Value::as_str)
|
|
1909
2632
|
.unwrap_or("codex");
|
|
2633
|
+
let mut entry = serde_json::Map::new();
|
|
2634
|
+
entry.insert("status".to_string(), serde_json::json!("running"));
|
|
2635
|
+
entry.insert("provider".to_string(), serde_json::json!(provider));
|
|
2636
|
+
entry.insert(
|
|
2637
|
+
"agent_id".to_string(),
|
|
2638
|
+
serde_json::json!(as_agent_id.as_str()),
|
|
2639
|
+
);
|
|
2640
|
+
entry.insert(
|
|
2641
|
+
"window".to_string(),
|
|
2642
|
+
serde_json::json!(as_agent_id.as_str()),
|
|
2643
|
+
);
|
|
2644
|
+
entry.insert(
|
|
2645
|
+
"forked_from".to_string(),
|
|
2646
|
+
serde_json::json!(source_agent_id.as_str()),
|
|
2647
|
+
);
|
|
2648
|
+
entry.insert(
|
|
2649
|
+
"spawn_cwd".to_string(),
|
|
2650
|
+
serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
|
|
2651
|
+
);
|
|
2652
|
+
entry.insert(
|
|
2653
|
+
"pane_id".to_string(),
|
|
2654
|
+
serde_json::json!(spawn.pane_id.as_str()),
|
|
2655
|
+
);
|
|
2656
|
+
if let Some(pid) = spawn.child_pid {
|
|
2657
|
+
entry.insert("pane_pid".to_string(), serde_json::json!(pid));
|
|
2658
|
+
}
|
|
2659
|
+
for key in [
|
|
2660
|
+
"auth_mode",
|
|
2661
|
+
"model",
|
|
2662
|
+
"model_source",
|
|
2663
|
+
"profile",
|
|
2664
|
+
"_profile_dir",
|
|
2665
|
+
"role",
|
|
2666
|
+
] {
|
|
2667
|
+
if let Some(value) = spec_agent.get(key) {
|
|
2668
|
+
entry.insert(key.to_string(), yaml_value_to_json(value));
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
if spec_agent.get("profile").is_some() && !entry.contains_key("_profile_dir") {
|
|
2672
|
+
if let Some(profile_dir) = profile_dir {
|
|
2673
|
+
entry.insert(
|
|
2674
|
+
"_profile_dir".to_string(),
|
|
2675
|
+
serde_json::json!(profile_dir.to_string_lossy().to_string()),
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
entry.insert("session_id".to_string(), serde_json::Value::Null);
|
|
2680
|
+
entry.insert("rollout_path".to_string(), serde_json::Value::Null);
|
|
2681
|
+
entry.insert("captured_at".to_string(), serde_json::Value::Null);
|
|
2682
|
+
entry.insert("captured_via".to_string(), serde_json::Value::Null);
|
|
2683
|
+
entry.insert(
|
|
2684
|
+
"attribution_confidence".to_string(),
|
|
2685
|
+
serde_json::Value::Null,
|
|
2686
|
+
);
|
|
2687
|
+
persist_command_plan_state(&mut entry, plan, profile_launch);
|
|
1910
2688
|
agent_map.insert(
|
|
1911
2689
|
as_agent_id.as_str().to_string(),
|
|
1912
|
-
serde_json::
|
|
1913
|
-
"status": "running",
|
|
1914
|
-
"provider": provider,
|
|
1915
|
-
"window": as_agent_id.as_str(),
|
|
1916
|
-
"forked_from": source_agent_id.as_str(),
|
|
1917
|
-
}),
|
|
2690
|
+
serde_json::Value::Object(entry),
|
|
1918
2691
|
);
|
|
2692
|
+
if let Some(entry) = agent_map
|
|
2693
|
+
.get_mut(as_agent_id.as_str())
|
|
2694
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
2695
|
+
{
|
|
2696
|
+
persist_effective_approval_policy(entry, safety);
|
|
2697
|
+
}
|
|
1919
2698
|
Ok(())
|
|
1920
2699
|
}
|
|
1921
2700
|
|
|
@@ -1947,12 +2726,9 @@ pub(crate) fn ensure_owner_allowed_for_state(
|
|
|
1947
2726
|
None,
|
|
1948
2727
|
)
|
|
1949
2728
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1950
|
-
if let Some(refusal) =
|
|
1951
|
-
state,
|
|
1952
|
-
|
|
1953
|
-
false,
|
|
1954
|
-
&NoopLiveness,
|
|
1955
|
-
) {
|
|
2729
|
+
if let Some(refusal) =
|
|
2730
|
+
crate::state::owner_gate::check_team_owner(state, &caller, false, &NoopLiveness)
|
|
2731
|
+
{
|
|
1956
2732
|
return Err(LifecycleError::OwnerRefused(refusal.to_string()));
|
|
1957
2733
|
}
|
|
1958
2734
|
Ok(())
|
|
@@ -1981,7 +2757,10 @@ fn initial_runtime_state(
|
|
|
1981
2757
|
let Some(id) = agent.get("id").and_then(Value::as_str) else {
|
|
1982
2758
|
continue;
|
|
1983
2759
|
};
|
|
1984
|
-
let provider = agent
|
|
2760
|
+
let provider = agent
|
|
2761
|
+
.get("provider")
|
|
2762
|
+
.and_then(Value::as_str)
|
|
2763
|
+
.unwrap_or("codex");
|
|
1985
2764
|
let role = agent.get("role").and_then(Value::as_str).unwrap_or(id);
|
|
1986
2765
|
let model = agent.get("model").and_then(Value::as_str);
|
|
1987
2766
|
let auth_mode = agent.get("auth_mode").and_then(Value::as_str);
|
|
@@ -2003,7 +2782,9 @@ fn initial_runtime_state(
|
|
|
2003
2782
|
.get("runtime")
|
|
2004
2783
|
.and_then(|runtime| runtime.get("display_backend"))
|
|
2005
2784
|
.and_then(Value::as_str)
|
|
2006
|
-
.and_then(|backend|
|
|
2785
|
+
.and_then(|backend| {
|
|
2786
|
+
serde_json::from_value::<DisplayBackend>(serde_json::json!(backend)).ok()
|
|
2787
|
+
});
|
|
2007
2788
|
let display_backend =
|
|
2008
2789
|
crate::lifecycle::display::resolve_display_backend(requested_display, None).backend;
|
|
2009
2790
|
let mut state = serde_json::Map::new();
|
|
@@ -2025,11 +2806,16 @@ fn initial_runtime_state(
|
|
|
2025
2806
|
);
|
|
2026
2807
|
state.insert(
|
|
2027
2808
|
"leader".to_string(),
|
|
2028
|
-
spec.get("leader")
|
|
2809
|
+
spec.get("leader")
|
|
2810
|
+
.map(yaml_value_to_json)
|
|
2811
|
+
.unwrap_or(serde_json::Value::Null),
|
|
2029
2812
|
);
|
|
2030
2813
|
state.insert("agents".to_string(), serde_json::Value::Object(agents));
|
|
2031
2814
|
state.insert("tasks".to_string(), spec_tasks_json(spec));
|
|
2032
|
-
state.insert(
|
|
2815
|
+
state.insert(
|
|
2816
|
+
"display_backend".to_string(),
|
|
2817
|
+
serde_json::json!(display_backend),
|
|
2818
|
+
);
|
|
2033
2819
|
let mut state = serde_json::Value::Object(state);
|
|
2034
2820
|
if !seed_launched_owner_from_env(&mut state) {
|
|
2035
2821
|
let team_id = crate::state::projection::team_state_key(&state);
|
|
@@ -2095,12 +2881,17 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
|
|
|
2095
2881
|
true
|
|
2096
2882
|
}
|
|
2097
2883
|
|
|
2884
|
+
fn has_positive_caller_leader_env() -> bool {
|
|
2885
|
+
env_nonempty("TEAM_AGENT_LEADER_PANE_ID")
|
|
2886
|
+
|| env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID")
|
|
2887
|
+
|| env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE")
|
|
2888
|
+
|| env_nonempty("TEAM_AGENT_LEADER_PROVIDER")
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2098
2891
|
fn spec_tasks_json(spec: &Value) -> serde_json::Value {
|
|
2099
2892
|
spec.get("tasks")
|
|
2100
2893
|
.and_then(Value::as_list)
|
|
2101
|
-
.map(|tasks|
|
|
2102
|
-
serde_json::Value::Array(tasks.iter().map(yaml_value_to_json).collect())
|
|
2103
|
-
})
|
|
2894
|
+
.map(|tasks| serde_json::Value::Array(tasks.iter().map(yaml_value_to_json).collect()))
|
|
2104
2895
|
.unwrap_or_else(|| serde_json::json!([]))
|
|
2105
2896
|
}
|
|
2106
2897
|
|
|
@@ -2139,7 +2930,10 @@ fn override_spec_session_name(spec: &mut Value, session_name: &str) {
|
|
|
2139
2930
|
if let Some((_, existing)) = runtime.iter_mut().find(|(k, _)| k == "session_name") {
|
|
2140
2931
|
*existing = Value::Str(session_name.to_string());
|
|
2141
2932
|
} else {
|
|
2142
|
-
runtime.push((
|
|
2933
|
+
runtime.push((
|
|
2934
|
+
"session_name".to_string(),
|
|
2935
|
+
Value::Str(session_name.to_string()),
|
|
2936
|
+
));
|
|
2143
2937
|
}
|
|
2144
2938
|
}
|
|
2145
2939
|
Some(other) => {
|
|
@@ -2246,20 +3040,11 @@ fn disabled_dangerous_approval() -> DangerousApproval {
|
|
|
2246
3040
|
}
|
|
2247
3041
|
}
|
|
2248
3042
|
|
|
2249
|
-
pub(crate) fn effective_runtime_config_for_worker_spawn(
|
|
3043
|
+
pub(crate) fn effective_runtime_config_for_worker_spawn(
|
|
3044
|
+
) -> Result<DangerousApproval, LifecycleError> {
|
|
2250
3045
|
detect_dangerous_approval()
|
|
2251
3046
|
}
|
|
2252
3047
|
|
|
2253
|
-
pub(crate) fn worker_tool_refs(
|
|
2254
|
-
mut tools: Vec<String>,
|
|
2255
|
-
safety: &DangerousApproval,
|
|
2256
|
-
) -> Vec<String> {
|
|
2257
|
-
if safety.enabled && !tools.iter().any(|tool| tool == "dangerous_auto_approve") {
|
|
2258
|
-
tools.push("dangerous_auto_approve".to_string());
|
|
2259
|
-
}
|
|
2260
|
-
tools
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
3048
|
fn write_launch_permission_audit(
|
|
2264
3049
|
workspace: &Path,
|
|
2265
3050
|
safety: &DangerousApproval,
|
|
@@ -2317,6 +3102,5 @@ fn agent_id_exists_in_team_dir(team_dir: &Path, agent_id: &AgentId) -> bool {
|
|
|
2317
3102
|
.exists()
|
|
2318
3103
|
}
|
|
2319
3104
|
|
|
2320
|
-
|
|
2321
3105
|
mod plan;
|
|
2322
3106
|
pub use plan::{handle_report_result, start_plan};
|