@team-agent/installer 0.5.50 → 0.5.51

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.
Files changed (84) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -3
  4. package/crates/team-agent/src/cli/emit.rs +2 -1
  5. package/crates/team-agent/src/cli/mod.rs +32 -14
  6. package/crates/team-agent/src/cli/named_address.rs +11 -48
  7. package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
  8. package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
  9. package/crates/team-agent/src/cli/send/persist.rs +154 -0
  10. package/crates/team-agent/src/cli/send/presentation.rs +333 -0
  11. package/crates/team-agent/src/cli/send/resolve.rs +361 -0
  12. package/crates/team-agent/src/cli/send.rs +44 -1169
  13. package/crates/team-agent/src/cli/spec.rs +1 -1
  14. package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
  15. package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
  16. package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
  17. package/crates/team-agent/src/cli/status_port/format.rs +145 -0
  18. package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
  19. package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
  20. package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
  21. package/crates/team-agent/src/cli/status_port/store.rs +412 -0
  22. package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
  23. package/crates/team-agent/src/cli/status_port.rs +47 -1548
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
  25. package/crates/team-agent/src/cli/types.rs +1 -0
  26. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  27. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  28. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  29. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  30. package/crates/team-agent/src/db/message_store.rs +154 -44
  31. package/crates/team-agent/src/event_log.rs +73 -0
  32. package/crates/team-agent/src/leader/start.rs +28 -4
  33. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  34. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
  35. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  36. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  37. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  38. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  39. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  40. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  41. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  42. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  43. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  44. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  45. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  46. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  47. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  48. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  49. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  50. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  51. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  52. package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
  53. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  54. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
  55. package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
  56. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  57. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
  58. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
  59. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  60. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  61. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  62. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  63. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
  64. package/crates/team-agent/src/mcp_server/tools.rs +14 -5
  65. package/crates/team-agent/src/messaging/activity.rs +4 -2
  66. package/crates/team-agent/src/messaging/address.rs +86 -0
  67. package/crates/team-agent/src/messaging/delivery.rs +165 -39
  68. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  69. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  70. package/crates/team-agent/src/messaging/mod.rs +10 -2
  71. package/crates/team-agent/src/messaging/persist.rs +309 -0
  72. package/crates/team-agent/src/messaging/results.rs +16 -24
  73. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  74. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  75. package/crates/team-agent/src/messaging/send.rs +101 -49
  76. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  77. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  78. package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
  79. package/crates/team-agent/src/messaging/watchers.rs +13 -3
  80. package/crates/team-agent/src/redaction.rs +72 -2
  81. package/crates/team-agent/src/state/persist.rs +2 -1
  82. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  83. package/crates/team-agent/src/state/repository.rs +59 -16
  84. package/package.json +4 -4
@@ -18,6 +18,28 @@
18
18
  //! sites can reference the phases by name. The phase fns themselves
19
19
  //! remain in launch.rs until the next batch of relocations.
20
20
 
21
+ use std::collections::{BTreeMap, BTreeSet};
22
+ use std::path::{Path, PathBuf};
23
+ use std::process::Command;
24
+
25
+ use crate::lifecycle::*;
26
+ use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
27
+ use crate::model::ids::AgentId;
28
+ use crate::model::permissions::{self, AgentPermissionInput};
29
+ use crate::model::yaml::{self, Value};
30
+ use crate::state::persist::load_runtime_state;
31
+ use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
32
+
33
+ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
34
+
35
+ use super::approval::detect_dangerous_approval;
36
+ use super::identity::spec_display_backend;
37
+ use super::leader_context::{
38
+ attributed_provider_for_pane_across_tmux_sockets, caller_provider_for_seed_with_lookup,
39
+ seed_unbound_launched_owner,
40
+ };
41
+ use super::worker_env::spawn_timestamp;
42
+
21
43
  /// Named launch phases under spec_state. Used in phase labels for logs
22
44
  /// and (future) for the orchestrator's step dispatcher.
23
45
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -49,3 +71,415 @@ mod tests {
49
71
  .starts_with("launch.spec_state."));
50
72
  }
51
73
  }
74
+
75
+ pub(super) fn initial_runtime_state(
76
+ spec: &Value,
77
+ spec_path: &Path,
78
+ workspace: &Path,
79
+ team_dir: &Path,
80
+ team_key: &str,
81
+ ) -> serde_json::Value {
82
+ let mut agents = serde_json::Map::new();
83
+ for agent in spec_agent_values(spec) {
84
+ let Some(id) = agent.get("id").and_then(Value::as_str) else {
85
+ continue;
86
+ };
87
+ let provider = agent
88
+ .get("provider")
89
+ .and_then(Value::as_str)
90
+ .unwrap_or("codex");
91
+ let role = agent.get("role").and_then(Value::as_str).unwrap_or(id);
92
+ let model = agent.get("model").and_then(Value::as_str);
93
+ let auth_mode = agent.get("auth_mode").and_then(Value::as_str);
94
+ let mut value = serde_json::json!({
95
+ "provider": provider,
96
+ "role": role,
97
+ });
98
+ if let Some(obj) = value.as_object_mut() {
99
+ if let Some(model) = model {
100
+ obj.insert("model".to_string(), serde_json::json!(model));
101
+ }
102
+ if let Some(auth_mode) = auth_mode {
103
+ obj.insert("auth_mode".to_string(), serde_json::json!(auth_mode));
104
+ }
105
+ }
106
+ agents.insert(id.to_string(), value);
107
+ }
108
+ let display_backend = spec_display_backend(spec);
109
+ let mut state = serde_json::Map::new();
110
+ state.insert(
111
+ "spec_path".to_string(),
112
+ serde_json::json!(spec_path.to_string_lossy().to_string()),
113
+ );
114
+ state.insert(
115
+ "workspace".to_string(),
116
+ serde_json::json!(workspace.to_string_lossy().to_string()),
117
+ );
118
+ state.insert(
119
+ "team_dir".to_string(),
120
+ serde_json::json!(team_dir.to_string_lossy().to_string()),
121
+ );
122
+ state.insert("team_key".to_string(), serde_json::json!(team_key));
123
+ state.insert(
124
+ "session_name".to_string(),
125
+ serde_json::json!(spec_session_name(spec).as_str()),
126
+ );
127
+ state.insert(
128
+ "leader".to_string(),
129
+ spec.get("leader")
130
+ .map(yaml_value_to_json)
131
+ .unwrap_or(serde_json::Value::Null),
132
+ );
133
+ state.insert("agents".to_string(), serde_json::Value::Object(agents));
134
+ state.insert("tasks".to_string(), spec_tasks_json(spec));
135
+ state.insert(
136
+ "display_backend".to_string(),
137
+ serde_json::json!(display_backend),
138
+ );
139
+ state.insert("is_external_leader".to_string(), serde_json::json!(false));
140
+ let mut state = serde_json::Value::Object(state);
141
+ if !seed_launched_owner_from_env(&mut state) {
142
+ let team_id = crate::state::projection::team_state_key(&state);
143
+ seed_unbound_launched_owner(&mut state, &team_id);
144
+ }
145
+ state
146
+ }
147
+
148
+ pub(super) fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
149
+ let team_id = crate::state::projection::team_state_key(state);
150
+ let Ok(caller) = crate::state::identity::caller_identity_from_env(
151
+ Some(state),
152
+ &crate::state::identity::SystemEnv,
153
+ Some(&team_id),
154
+ None,
155
+ ) else {
156
+ return false;
157
+ };
158
+ seed_launched_owner_from_caller_with_provider_lookup(
159
+ state,
160
+ caller,
161
+ attributed_provider_for_pane_across_tmux_sockets,
162
+ )
163
+ }
164
+
165
+ pub(super) fn seed_launched_owner_from_caller_with_provider_lookup(
166
+ state: &mut serde_json::Value,
167
+ caller: crate::state::owner_gate::CallerIdentity,
168
+ lookup_pane_provider: impl Fn(&PaneId) -> Option<Provider>,
169
+ ) -> bool {
170
+ if caller.pane_id.is_empty() {
171
+ return false;
172
+ }
173
+ let provider = caller_provider_for_seed_with_lookup(&caller, lookup_pane_provider);
174
+ let pane_id = caller.pane_id;
175
+ let owner_epoch = 1u64;
176
+ let mut owner = serde_json::json!({
177
+ "pane_id": pane_id,
178
+ "machine_fingerprint": caller.machine_fingerprint,
179
+ "leader_session_uuid": caller.leader_session_uuid,
180
+ "owner_epoch": owner_epoch,
181
+ "claimed_at": spawn_timestamp(),
182
+ "claimed_via": "quick-start",
183
+ "os_user": std::env::var("USER")
184
+ .or_else(|_| std::env::var("USERNAME"))
185
+ .unwrap_or_default(),
186
+ });
187
+ let mut receiver = serde_json::json!({
188
+ "mode": "direct_tmux",
189
+ "status": "attached",
190
+ "pane_id": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
191
+ "pane": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
192
+ "leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
193
+ "owner_epoch": owner_epoch,
194
+ "discovery": "quick_start",
195
+ });
196
+ if let Some(provider) = provider.as_ref() {
197
+ if let Some(owner) = owner.as_object_mut() {
198
+ owner.insert("provider".to_string(), serde_json::json!(provider));
199
+ }
200
+ if let Some(receiver) = receiver.as_object_mut() {
201
+ receiver.insert("provider".to_string(), serde_json::json!(provider));
202
+ }
203
+ }
204
+ if let (Some(receiver), Some(socket)) = (
205
+ receiver.as_object_mut(),
206
+ crate::tmux_backend::socket_name_from_tmux_env(),
207
+ ) {
208
+ receiver.insert("tmux_socket".to_string(), serde_json::json!(socket));
209
+ }
210
+ // Stage 3a (identity-boundary unified plan, architect direction 2026-06-23):
211
+ // route quick-start attached-from-env seed through ownership repository.
212
+ let team_key = crate::state::projection::team_state_key(state);
213
+ let record = crate::state::ownership::OwnershipWrite::new()
214
+ .with_leader_receiver(receiver)
215
+ .with_team_owner(owner)
216
+ .with_owner_epoch(owner_epoch);
217
+ crate::state::ownership::write_owner(state, &team_key, record);
218
+ true
219
+ }
220
+
221
+ pub(super) fn has_positive_caller_leader_env() -> bool {
222
+ env_nonempty("TEAM_AGENT_LEADER_PANE_ID")
223
+ || env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID")
224
+ || env_nonempty("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE")
225
+ || env_nonempty("TEAM_AGENT_LEADER_PROVIDER")
226
+ }
227
+
228
+ pub(super) fn env_nonempty(key: &str) -> bool {
229
+ std::env::var(key)
230
+ .ok()
231
+ .is_some_and(|value| !value.is_empty())
232
+ }
233
+
234
+ pub(super) fn spec_tasks_json(spec: &Value) -> serde_json::Value {
235
+ spec.get("tasks")
236
+ .and_then(Value::as_list)
237
+ .map(|tasks| serde_json::Value::Array(tasks.iter().map(yaml_value_to_json).collect()))
238
+ .unwrap_or_else(|| serde_json::json!([]))
239
+ }
240
+
241
+ pub(super) fn yaml_value_to_json(value: &Value) -> serde_json::Value {
242
+ match value {
243
+ Value::Null => serde_json::Value::Null,
244
+ Value::Bool(v) => serde_json::json!(v),
245
+ Value::Int(v) => serde_json::json!(v),
246
+ Value::Float(v) => serde_json::json!(v),
247
+ Value::Str(v) => serde_json::json!(v),
248
+ Value::List(values) => {
249
+ serde_json::Value::Array(values.iter().map(yaml_value_to_json).collect())
250
+ }
251
+ Value::Map(entries) => {
252
+ let mut out = serde_json::Map::new();
253
+ for (key, item) in entries {
254
+ out.insert(key.clone(), yaml_value_to_json(item));
255
+ }
256
+ serde_json::Value::Object(out)
257
+ }
258
+ }
259
+ }
260
+
261
+ /// Set `runtime.session_name` on the compiled spec to `session_name`, creating the
262
+ /// `runtime` map and/or the `session_name` entry if absent. Used by quick-start to
263
+ /// derive the tmux session from the REQUESTED team identity (CR-040/042) rather
264
+ /// than the template's compiled-in name.
265
+ /// E5 Bug2(atomic 真修):原子写 runtime spec —— 写 `<spec>.tmp-<pid>` 再 rename 覆盖,
266
+ /// 避免崩溃/并发留下半截 spec(plain fs::write 会 in-place truncate 后逐字节写)。
267
+ /// rename 失败时清理 tmp,原 spec(若有)不动。
268
+ pub(crate) fn write_spec_atomic(spec_path: &Path, spec: &Value) -> Result<(), LifecycleError> {
269
+ if let Some(parent) = spec_path.parent() {
270
+ std::fs::create_dir_all(parent)
271
+ .map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", parent.display())))?;
272
+ }
273
+ let tmp = spec_path.with_extension(format!("tmp-{}", std::process::id()));
274
+ std::fs::write(&tmp, yaml::dumps(spec))
275
+ .map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", tmp.display())))?;
276
+ if let Err(e) = std::fs::rename(&tmp, spec_path) {
277
+ let _ = std::fs::remove_file(&tmp);
278
+ return Err(LifecycleError::StatePersist(format!(
279
+ "{}: {e}",
280
+ spec_path.display()
281
+ )));
282
+ }
283
+ Ok(())
284
+ }
285
+
286
+ pub(crate) fn override_spec_session_name(spec: &mut Value, session_name: &str) {
287
+ override_spec_runtime_str(spec, "session_name", session_name);
288
+ }
289
+
290
+ pub(crate) fn override_spec_workspace(spec: &mut Value, workspace: &Path) {
291
+ let workspace_s = workspace.to_string_lossy().to_string();
292
+ let Value::Map(root) = spec else { return };
293
+ if let Some((_, Value::Map(team))) = root.iter_mut().find(|(k, _)| k == "team") {
294
+ if let Some((_, value)) = team.iter_mut().find(|(k, _)| k == "workspace") {
295
+ *value = Value::Str(workspace_s.clone());
296
+ }
297
+ }
298
+ if let Some((_, Value::List(agents))) = root.iter_mut().find(|(k, _)| k == "agents") {
299
+ for agent in agents {
300
+ if let Value::Map(fields) = agent {
301
+ if let Some((_, value)) = fields.iter_mut().find(|(k, _)| k == "working_directory")
302
+ {
303
+ *value = Value::Str(workspace_s.clone());
304
+ }
305
+ }
306
+ }
307
+ }
308
+ }
309
+
310
+ pub(super) fn override_spec_display_backend(spec: &mut Value, display_backend: &str) {
311
+ override_spec_runtime_str(spec, "display_backend", display_backend);
312
+ }
313
+
314
+ pub(super) fn override_spec_runtime_str(spec: &mut Value, key: &str, value: &str) {
315
+ let Value::Map(root) = spec else { return };
316
+ let runtime_slot = root
317
+ .iter_mut()
318
+ .find(|(k, _)| k == "runtime")
319
+ .map(|(_, v)| v);
320
+ match runtime_slot {
321
+ Some(Value::Map(runtime)) => {
322
+ if let Some((_, existing)) = runtime.iter_mut().find(|(k, _)| k == key) {
323
+ *existing = Value::Str(value.to_string());
324
+ } else {
325
+ runtime.push((key.to_string(), Value::Str(value.to_string())));
326
+ }
327
+ }
328
+ Some(other) => {
329
+ *other = Value::Map(vec![(key.to_string(), Value::Str(value.to_string()))]);
330
+ }
331
+ None => {
332
+ root.push((
333
+ "runtime".to_string(),
334
+ Value::Map(vec![(key.to_string(), Value::Str(value.to_string()))]),
335
+ ));
336
+ }
337
+ }
338
+ }
339
+
340
+ pub(super) fn spec_session_name(spec: &Value) -> SessionName {
341
+ if let Some(name) = spec
342
+ .get("runtime")
343
+ .and_then(|v| v.get("session_name"))
344
+ .and_then(Value::as_str)
345
+ .filter(|name| !name.is_empty())
346
+ {
347
+ return SessionName::new(name);
348
+ }
349
+ // Python launch/core.py:56 — fallback derives from the team name, not a constant.
350
+ let team_name = spec
351
+ .get("team")
352
+ .and_then(|team| team.get("name"))
353
+ .and_then(Value::as_str)
354
+ .filter(|name| !name.is_empty())
355
+ .unwrap_or("agent");
356
+ SessionName::new(format!("team-{team_name}"))
357
+ }
358
+
359
+ /// 0.3.28 layout step 1: pub re-export of `spec_session_name` for the new
360
+ /// `layout::sessions::worker_session_name` to delegate to. Single underlying
361
+ /// impl; this just widens visibility without duplicating logic.
362
+ pub fn worker_session_name_pub(spec: &Value) -> SessionName {
363
+ spec_session_name(spec)
364
+ }
365
+
366
+ pub(super) fn spec_agents(spec: &Value) -> Vec<AgentId> {
367
+ spec_agent_values(spec)
368
+ .into_iter()
369
+ .filter_map(|agent| agent.get("id").and_then(Value::as_str).map(AgentId::new))
370
+ .collect()
371
+ }
372
+
373
+ /// Bug 1 (0.4.2): expose spec agent id set so the restart path can filter
374
+ /// state.agents to only the agents currently defined in the rebuilt spec.
375
+ /// Returns a `BTreeSet<String>` for O(log n) membership checks.
376
+ pub(crate) fn spec_agent_id_set(spec: &Value) -> std::collections::BTreeSet<String> {
377
+ spec_agent_values(spec)
378
+ .into_iter()
379
+ .filter_map(|agent| agent.get("id").and_then(Value::as_str).map(str::to_string))
380
+ .collect()
381
+ }
382
+
383
+ pub(super) fn spec_agent_values(spec: &Value) -> Vec<&Value> {
384
+ spec.get("agents")
385
+ .and_then(Value::as_list)
386
+ .map(|agents| agents.iter().collect())
387
+ .unwrap_or_default()
388
+ }
389
+
390
+ pub(super) fn spec_routes(spec: &Value) -> Vec<RoutingDecision> {
391
+ spec.get("tasks")
392
+ .and_then(Value::as_list)
393
+ .map(|tasks| {
394
+ tasks
395
+ .iter()
396
+ .map(|task| {
397
+ let routed = crate::model::routing::route_task(spec, task);
398
+ RoutingDecision {
399
+ task_id: task.get("id").and_then(Value::as_str).map(str::to_string),
400
+ selected_agent: routed.agent_id,
401
+ reason: routed.reason,
402
+ manual_override: false,
403
+ }
404
+ })
405
+ .collect()
406
+ })
407
+ .unwrap_or_default()
408
+ }
409
+
410
+ pub(super) fn spec_default_assignee(spec: &Value) -> Option<AgentId> {
411
+ spec.get("routing")
412
+ .and_then(|v| v.get("default_assignee"))
413
+ .and_then(Value::as_str)
414
+ .map(AgentId::new)
415
+ .or_else(|| spec_agents(spec).into_iter().next())
416
+ }
417
+
418
+ pub(crate) fn effective_runtime_config(spec: &Value) -> Result<DangerousApproval, LifecycleError> {
419
+ let enabled = spec
420
+ .get("runtime")
421
+ .and_then(|v| v.get("dangerous_auto_approve"))
422
+ .is_some_and(Value::is_truthy);
423
+ if enabled {
424
+ let leader = detect_dangerous_approval()?;
425
+ Ok(DangerousApproval {
426
+ enabled: true,
427
+ source: DangerousApprovalSource::RuntimeConfig,
428
+ inherited: false,
429
+ provider: None,
430
+ flag: None,
431
+ worker_capability_above_leader: !leader.enabled,
432
+ ancestry_binary_name: leader.ancestry_binary_name,
433
+ unexpected_binary: false,
434
+ })
435
+ } else {
436
+ Ok(detect_dangerous_approval()?)
437
+ }
438
+ }
439
+
440
+ pub(crate) fn effective_runtime_config_for_worker_spawn(
441
+ ) -> Result<DangerousApproval, LifecycleError> {
442
+ detect_dangerous_approval()
443
+ }
444
+
445
+ pub(super) fn write_launch_permission_audit(
446
+ workspace: &Path,
447
+ safety: &DangerousApproval,
448
+ ) -> Result<(), LifecycleError> {
449
+ crate::event_log::EventLog::new(workspace)
450
+ .write(
451
+ "launch.permissions_resolved",
452
+ serde_json::json!({
453
+ "dangerous_auto_approve": safety.enabled,
454
+ "dangerous_auto_approve_source": safety.source,
455
+ "dangerous_auto_approve_inherited": safety.inherited,
456
+ "dangerous_auto_approve_provider": safety.provider,
457
+ "dangerous_auto_approve_flag": safety.flag,
458
+ "worker_capability_above_leader": safety.worker_capability_above_leader,
459
+ "ancestry_binary_name": safety.ancestry_binary_name,
460
+ }),
461
+ )
462
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
463
+ if safety.unexpected_binary {
464
+ crate::event_log::EventLog::new(workspace)
465
+ .write(
466
+ "dangerous_flag_in_unexpected_binary",
467
+ serde_json::json!({
468
+ "provider": safety.provider,
469
+ "flag": safety.flag,
470
+ "ancestry_binary_name": safety.ancestry_binary_name,
471
+ }),
472
+ )
473
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
474
+ }
475
+ Ok(())
476
+ }
477
+
478
+ pub(super) fn team_workspace(team_dir: &Path) -> PathBuf {
479
+ crate::model::paths::team_workspace(team_dir).unwrap_or_else(|_| {
480
+ team_dir
481
+ .parent()
482
+ .map(Path::to_path_buf)
483
+ .unwrap_or_else(|| team_dir.to_path_buf())
484
+ })
485
+ }