@team-agent/installer 0.5.64 → 0.5.66

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 (105) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +5 -0
  4. package/crates/team-agent/src/cli/adapters.rs +6 -1
  5. package/crates/team-agent/src/cli/emit.rs +15 -0
  6. package/crates/team-agent/src/cli/mod.rs +9 -8
  7. package/crates/team-agent/src/cli/send/presentation.rs +1 -0
  8. package/crates/team-agent/src/cli/spec.rs +3 -0
  9. package/crates/team-agent/src/cli/tests/compile.rs +1 -1
  10. package/crates/team-agent/src/cli/tests/mod.rs +33 -2
  11. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
  12. package/crates/team-agent/src/cli/tests/status_send.rs +13 -3
  13. package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
  14. package/crates/team-agent/src/cli/types.rs +7 -0
  15. package/crates/team-agent/src/compiler/tests.rs +23 -20
  16. package/crates/team-agent/src/compiler.rs +25 -5
  17. package/crates/team-agent/src/conpty/backend.rs +16 -0
  18. package/crates/team-agent/src/coordinator/health.rs +17 -0
  19. package/crates/team-agent/src/coordinator/steps/abnormal.rs +55 -2
  20. package/crates/team-agent/src/db/migration.rs +2 -1
  21. package/crates/team-agent/src/db/schema.rs +16 -8
  22. package/crates/team-agent/src/leader/lease.rs +1 -2
  23. package/crates/team-agent/src/leader/start.rs +175 -10
  24. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
  25. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +0 -2
  26. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +7 -2
  27. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +137 -2
  28. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -2
  29. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +5 -0
  30. package/crates/team-agent/src/lifecycle/launch/spawn.rs +39 -2
  31. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +105 -48
  32. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +0 -2
  33. package/crates/team-agent/src/lifecycle/launch.rs +117 -21
  34. package/crates/team-agent/src/lifecycle/restart/agent.rs +89 -3
  35. package/crates/team-agent/src/lifecycle/restart/common.rs +9 -12
  36. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +59 -15
  37. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
  38. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +4 -4
  39. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
  40. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
  41. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
  42. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +24 -32
  43. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
  44. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +13 -11
  45. package/crates/team-agent/src/lifecycle/tests/core.rs +0 -18
  46. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
  47. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
  48. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
  49. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
  50. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
  51. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +2 -2
  52. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +17 -30
  53. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +1 -1
  54. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +4 -4
  55. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
  56. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
  57. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
  58. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
  59. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1 -1
  60. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
  61. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
  62. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
  63. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +11 -7
  64. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
  65. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
  66. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
  67. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
  68. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
  69. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
  70. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
  71. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +1 -1
  72. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
  73. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
  74. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +1 -1
  75. package/crates/team-agent/src/lifecycle/tests.rs +11 -7
  76. package/crates/team-agent/src/lifecycle/types.rs +0 -2
  77. package/crates/team-agent/src/lifecycle/worker_command_context.rs +115 -22
  78. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  79. package/crates/team-agent/src/messaging/delivery.rs +29 -0
  80. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  81. package/crates/team-agent/src/messaging/leader_receiver.rs +12 -0
  82. package/crates/team-agent/src/messaging/mod.rs +2 -0
  83. package/crates/team-agent/src/messaging/results.rs +5 -0
  84. package/crates/team-agent/src/messaging/send.rs +9 -0
  85. package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
  86. package/crates/team-agent/src/messaging/tests/runtime.rs +32 -0
  87. package/crates/team-agent/src/messaging/types.rs +2 -0
  88. package/crates/team-agent/src/messaging/wait.rs +366 -0
  89. package/crates/team-agent/src/messaging/watchers.rs +153 -6
  90. package/crates/team-agent/src/model/spec.rs +129 -6
  91. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
  92. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
  93. package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
  94. package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
  95. package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
  96. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
  97. package/crates/team-agent/src/provider/bypass_flags.rs +80 -0
  98. package/crates/team-agent/src/provider/mod.rs +2 -0
  99. package/crates/team-agent/src/tmux_backend.rs +35 -12
  100. package/crates/team-agent/src/transport/test_support.rs +20 -0
  101. package/crates/team-agent/src/transport.rs +28 -11
  102. package/package.json +4 -4
  103. package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
  104. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
  105. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
@@ -181,6 +181,31 @@ pub(crate) fn start_agent_at_paths(
181
181
  );
182
182
  }
183
183
  let agent_live = agent_live && (!has_collision || noop_pane.is_some());
184
+ // 0.5.66 bypass 单源 §2.6 — dangerously_skip_permissions drift 挡板:
185
+ // Noop 路径不 spawn,若声明面(角色 md / spec)与运行面(as-launched)不一致,
186
+ // 拒 Noop 报明,防"改了 md 却以为生效、进程仍带旧 bypass 跑着"的暗门。
187
+ let file_bypass = agent
188
+ .get("dangerously_skip_permissions")
189
+ .and_then(serde_json::Value::as_bool);
190
+ let launched_bypass = raw_agent
191
+ .get("as_launched_dangerously_skip_permissions")
192
+ .and_then(serde_json::Value::as_bool);
193
+ if !force && agent_live && check_bypass_drift(file_bypass, launched_bypass) {
194
+ // 0.5.66 bypass 单源 §2.7:Noop drift 拒也写审计事件。
195
+ let _ = crate::event_log::EventLog::new(workspace).write(
196
+ "worker.spawn_dangerously_skip_permissions_drift_denied",
197
+ serde_json::json!({
198
+ "agent_id": agent_id.as_str(),
199
+ "file_value": file_bypass,
200
+ "launched_value": launched_bypass,
201
+ "action": "noop_denied_require_remove_add_or_force",
202
+ }),
203
+ );
204
+ return Err(LifecycleError::RequirementUnmet(format!(
205
+ "dangerously_skip_permissions drift for agent {agent_id}: file={file_bypass:?} but running process launched with {launched_bypass:?}. \
206
+ Run `remove-agent {agent_id}` then `add-agent {agent_id}` to apply, or `restart --force {agent_id}` to accept fresh spawn."
207
+ )));
208
+ }
184
209
  if !force && agent_live {
185
210
  let old_binding = pane_binding_snapshot(&raw_agent);
186
211
  let refreshed_binding = noop_pane.as_ref().map(pane_binding_from_live);
@@ -257,7 +282,10 @@ pub(crate) fn start_agent_at_paths(
257
282
  };
258
283
  let into_existing_session =
259
284
  session_live_or_default(transport, &session_name, session_name_present(&state));
260
- let safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
285
+ let safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn_json(
286
+ &agent,
287
+ provider,
288
+ )?;
261
289
  let layout_placement = if adaptive_layout {
262
290
  crate::lifecycle::launch::adaptive_existing_placement_for_agent(
263
291
  &state,
@@ -323,6 +351,30 @@ pub(crate) fn start_agent_at_paths(
323
351
  &safety,
324
352
  start_mode,
325
353
  )?;
354
+ // 0.5.66 bypass 单源 §2.7:启动即带 bypass → 审计留痕。
355
+ if safety.enabled {
356
+ let state_agent = state
357
+ .get("agents")
358
+ .and_then(|v| v.get(agent_id.as_str()));
359
+ let _ = crate::event_log::EventLog::new(workspace).write(
360
+ "worker.spawn_dangerously_skip_permissions",
361
+ serde_json::json!({
362
+ "agent_id": agent_id.as_str(),
363
+ "role": state_agent
364
+ .and_then(|a| a.get("role"))
365
+ .and_then(serde_json::Value::as_str)
366
+ .unwrap_or_default(),
367
+ "provider": state_agent
368
+ .and_then(|a| a.get("provider"))
369
+ .and_then(serde_json::Value::as_str)
370
+ .unwrap_or_default(),
371
+ "trigger": "start-agent",
372
+ "spec_source_path": crate::lifecycle::launch::agent_id_spec_source_path(
373
+ workspace, agent_id.as_str(),
374
+ ),
375
+ }),
376
+ );
377
+ }
326
378
  // **0.3.24 add-agent socket drift fix**: keep `state.tmux_endpoint` /
327
379
  // `state.tmux_socket` synchronized with the transport actually used for the
328
380
  // spawn. Without this, add-agent / fork-agent could spawn to a socket that
@@ -1078,6 +1130,12 @@ fn mark_agent_started(
1078
1130
  // lifecycle/topology fields so absence == UNKNOWN until the next
1079
1131
  // coordinator tick or pane fallback produces a post-spawn observation.
1080
1132
  clear_agent_runtime_activity_observation(agent);
1133
+ // 0.5.66 bypass 单源 §2.6:fresh spawn 那一刻记录运行面 bypass 值,
1134
+ // 供下一次 restart 的 Noop drift 挡板比较。
1135
+ agent.insert(
1136
+ "as_launched_dangerously_skip_permissions".to_string(),
1137
+ serde_json::json!(safety.enabled),
1138
+ );
1081
1139
  // S1-CAPTURE-001 (0.4.8, CR M3 provider-agnostic): on a Fresh /
1082
1140
  // FreshAfterMissingRollout start, the prior session's authoritative
1083
1141
  // capture tuple MUST be cleared before persist_command_plan_state
@@ -1609,7 +1667,6 @@ fn write_start_agent_start_event(
1609
1667
  let adapter = crate::provider::get_adapter(provider);
1610
1668
  // Contract C / F6.4: event log must record the same context-aware argv that the
1611
1669
  // actual spawn used — so the role/tools/MCP context appears in `start_agent.agent_start`.
1612
- let safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
1613
1670
  let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
1614
1671
  agent,
1615
1672
  Some(agent_id.as_str()),
@@ -1620,7 +1677,6 @@ fn write_start_agent_start_event(
1620
1677
  let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
1621
1678
  &command_agent,
1622
1679
  provider,
1623
- &safety,
1624
1680
  )?;
1625
1681
  let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
1626
1682
  let mcp_config = adapter
@@ -1831,6 +1887,17 @@ fn write_reset_complete_event(
1831
1887
  Ok(())
1832
1888
  }
1833
1889
 
1890
+ /// 0.5.66 bypass 单源 §2.6:声明面(file)与运行面(as-launched)是否 drift。
1891
+ /// 缺 as-launched(从未 spawn)= 视为未启动,不 drift(首次 Noop 无进程可比)。
1892
+ fn check_bypass_drift(file: Option<bool>, launched: Option<bool>) -> bool {
1893
+ match (file, launched) {
1894
+ (Some(file), Some(launched)) => file != launched,
1895
+ _ => false,
1896
+ }
1897
+ }
1898
+
1899
+ #[cfg(test)]
1900
+
1834
1901
  #[cfg(test)]
1835
1902
  mod tests {
1836
1903
  use super::*;
@@ -1907,4 +1974,23 @@ mod tests {
1907
1974
  "other-agent collision checks must also include tmux_socket"
1908
1975
  );
1909
1976
  }
1977
+
1978
+ // 0.5.66 bypass 单源 §4.1:Noop drift 检测——声明面与运行面不一致 → drift。
1979
+ #[test]
1980
+ fn test_restart_noop_denied_on_drift() {
1981
+ // file=true, launched=false → drift(改了 md 升 bypass 但进程没带)。
1982
+ assert!(check_bypass_drift(Some(true), Some(false)));
1983
+ // file=false, launched=true → drift(改了 md 撤 bypass 但进程还带着)。
1984
+ assert!(check_bypass_drift(Some(false), Some(true)));
1985
+ }
1986
+
1987
+ // 0.5.66 bypass 单源 §4.1:无 drift 或从未 spawn(as-launched 缺)→ 不拒。
1988
+ #[test]
1989
+ fn test_restart_fresh_spawn_after_drift_uses_file_value() {
1990
+ assert!(!check_bypass_drift(Some(true), Some(true)));
1991
+ assert!(!check_bypass_drift(Some(false), Some(false)));
1992
+ // 从未 spawn:as-launched 缺失 → 不 drift(首次 Noop 无进程可比)。
1993
+ assert!(!check_bypass_drift(Some(true), None));
1994
+ assert!(!check_bypass_drift(None, None));
1995
+ }
1910
1996
  }
@@ -213,7 +213,10 @@ pub(super) fn spawn_agent_window(
213
213
  let safety = if let Some(safety) = safety {
214
214
  safety
215
215
  } else {
216
- detected_safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
216
+ detected_safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn_json(
217
+ agent,
218
+ provider,
219
+ )?;
217
220
  &detected_safety
218
221
  };
219
222
  let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
@@ -226,7 +229,6 @@ pub(super) fn spawn_agent_window(
226
229
  let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
227
230
  &command_agent,
228
231
  provider,
229
- safety,
230
232
  )?;
231
233
  let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
232
234
  // owner_team_id resolution priority (Issue 2 fix):
@@ -405,9 +407,9 @@ pub(super) fn spawn_agent_window(
405
407
 
406
408
  // 0.5.39 Slice 2 (tmux-server-death-locate §7 Slice 2): route the
407
409
  // primary worker spawn through the worker shell wrapper so provider
408
- // exit leaves the pane at an interactive shell with an explicit exit
409
- // marker instead of collapsing the pane into `[exited]`. This mirrors
410
- // the leader wrapper's manual-tmux equivalence. The split path stays
410
+ // exit leaves the pane at an inert sh tail with an explicit exit marker
411
+ // instead of collapsing the pane into `[exited]`. The inert tail does
412
+ // not read pane stdin. The split path stays
411
413
  // on plain spawn_split — split panes are display overlays, not the
412
414
  // primary worker process.
413
415
  let provider_label = crate::provider::wire::command_name(provider);
@@ -591,6 +593,8 @@ fn merge_command_context_fields(
591
593
  "effort",
592
594
  "profile",
593
595
  "permission_mode",
596
+ // 0.5.66 bypass 单源:rehydrate 同步合入,restart 的 safety 构造读到新值。
597
+ "dangerously_skip_permissions",
594
598
  ] {
595
599
  if let Some(value) = spec_agent.get(field).and_then(yaml_value_to_json) {
596
600
  obj.insert(field.to_string(), value);
@@ -799,13 +803,6 @@ pub(super) fn lifecycle_worker_tmux_backend_for_state(
799
803
  .backend
800
804
  }
801
805
 
802
- pub(super) fn persist_effective_approval_policy_for_restart(
803
- agent: &mut serde_json::Map<String, serde_json::Value>,
804
- safety: &DangerousApproval,
805
- ) {
806
- crate::lifecycle::launch::persist_effective_approval_policy(agent, safety);
807
- }
808
-
809
806
  pub(super) fn save_restart_projected_state(
810
807
  workspace: &Path,
811
808
  state: &mut serde_json::Value,
@@ -270,7 +270,6 @@ fn restart_with_selected_team_and_transport(
270
270
  let spec_workspace = runtime_spec.parent().ok_or_else(|| {
271
271
  LifecycleError::TeamSelect("active team spec workspace not found".to_string())
272
272
  })?;
273
- let safety = crate::lifecycle::launch::effective_runtime_config(&spec)?;
274
273
  // Bug 1 (0.4.2 P0): the rebuilt spec is the single source of truth for the
275
274
  // active roster. Any agent in state.agents that is NOT in the rebuilt
276
275
  // spec is a stale leftover (role doc was deleted between sessions) and
@@ -526,7 +525,6 @@ fn restart_with_selected_team_and_transport(
526
525
  &selected.team_key,
527
526
  &session_name,
528
527
  transport,
529
- &safety,
530
528
  tmux_endpoint_source,
531
529
  &state,
532
530
  )
@@ -551,12 +549,25 @@ fn restart_with_selected_team_and_transport(
551
549
  if let Some(parallel_result) = parallel_result {
552
550
  match parallel_result {
553
551
  Ok(pspawn) => {
552
+ let raw_agent = state
553
+ .get("agents")
554
+ .and_then(|v| v.get(decision.agent_id.as_str()))
555
+ .cloned()
556
+ .unwrap_or(serde_json::Value::Null);
557
+ let rehydrated = rehydrate_agent_command_context_from_spec(
558
+ spec_workspace,
559
+ &decision.agent_id,
560
+ &raw_agent,
561
+ );
562
+ let launched_bypass = matches!(
563
+ rehydrated.get("dangerously_skip_permissions"),
564
+ Some(serde_json::Value::Bool(true))
565
+ );
554
566
  apply_marked_respawn(
555
567
  &selected.run_workspace,
556
568
  &selected.team_key,
557
569
  &mut state,
558
570
  transport,
559
- &safety,
560
571
  &phase_timer,
561
572
  decision,
562
573
  &pspawn.spawn,
@@ -565,6 +576,7 @@ fn restart_with_selected_team_and_transport(
565
576
  &mut successful_agents,
566
577
  &mut failed_agents,
567
578
  &mut fatal_resume_failure,
579
+ launched_bypass,
568
580
  );
569
581
  }
570
582
  Err(error) => {
@@ -709,7 +721,7 @@ fn restart_with_selected_team_and_transport(
709
721
  session_id,
710
722
  session_live,
711
723
  transport,
712
- Some(&safety),
724
+ None,
713
725
  layout_placement.as_ref(),
714
726
  None,
715
727
  tmux_endpoint_source,
@@ -737,6 +749,10 @@ fn restart_with_selected_team_and_transport(
737
749
  }
738
750
  };
739
751
  let verify_start = std::time::Instant::now();
752
+ let launched_bypass = matches!(
753
+ agent.get("dangerously_skip_permissions"),
754
+ Some(serde_json::Value::Bool(true))
755
+ );
740
756
  if let Err(error) = verify_spawned_agent_live(&decision.agent_id, &spawn, transport)
741
757
  .and_then(|_| {
742
758
  mark_agent_respawned(
@@ -745,7 +761,7 @@ fn restart_with_selected_team_and_transport(
745
761
  decision.restart_mode,
746
762
  &spawn,
747
763
  transport,
748
- &safety,
764
+ launched_bypass,
749
765
  )
750
766
  })
751
767
  {
@@ -787,7 +803,16 @@ fn restart_with_selected_team_and_transport(
787
803
  .and_then(|agents| agents.get_mut(decision.agent_id.as_str()))
788
804
  .and_then(serde_json::Value::as_object_mut)
789
805
  {
790
- persist_effective_approval_policy_for_restart(agent, &safety);
806
+ // 0.5.66 bypass 单源:policy 从 agent 行字段派生。
807
+ let agent_provider = agent
808
+ .get("provider")
809
+ .and_then(serde_json::Value::as_str)
810
+ .and_then(crate::provider::wire::parse_canonical_provider)
811
+ .unwrap_or(crate::model::enums::Provider::Codex);
812
+ crate::lifecycle::launch::persist_effective_approval_policy_from_agent(
813
+ agent,
814
+ agent_provider,
815
+ );
791
816
  }
792
817
  // 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
793
818
  // pair the state-side activity clear with a DB `agent_health` clear so
@@ -1958,7 +1983,7 @@ fn mark_agent_respawned(
1958
1983
  restart_mode: StartMode,
1959
1984
  spawn: &SpawnedAgentWindow,
1960
1985
  transport: &dyn crate::transport::Transport,
1961
- safety: &DangerousApproval,
1986
+ launched_bypass: bool,
1962
1987
  ) -> Result<(), LifecycleError> {
1963
1988
  let Some(agent) = state
1964
1989
  .get_mut("agents")
@@ -2081,7 +2106,18 @@ fn mark_agent_respawned(
2081
2106
  }
2082
2107
  }
2083
2108
  crate::lifecycle::launch::persist_command_plan_state(agent, &spawn.plan, &spawn.profile_launch);
2084
- persist_effective_approval_policy_for_restart(agent, safety);
2109
+ // 0.5.66 bypass 单源:policy 从 agent 行字段派生,不再用全队 `DangerousApproval`。
2110
+ let agent_provider = agent
2111
+ .get("provider")
2112
+ .and_then(serde_json::Value::as_str)
2113
+ .and_then(crate::provider::wire::parse_canonical_provider)
2114
+ .unwrap_or(crate::model::enums::Provider::Codex);
2115
+ crate::lifecycle::launch::persist_effective_approval_policy_from_agent(agent, agent_provider);
2116
+ // 0.5.66 bypass 单源 §2.6:fresh spawn(restart)记录运行面 bypass 值。
2117
+ agent.insert(
2118
+ "as_launched_dangerously_skip_permissions".to_string(),
2119
+ serde_json::json!(launched_bypass),
2120
+ );
2085
2121
  if let Some(placement) = spawn.layout_placement.as_ref() {
2086
2122
  agent.insert(
2087
2123
  "layout_window".to_string(),
@@ -2510,7 +2546,6 @@ fn run_bounded_parallel_worker_spawns(
2510
2546
  team_key: &str,
2511
2547
  session_name: &SessionName,
2512
2548
  transport: &(dyn crate::transport::Transport),
2513
- safety: &DangerousApproval,
2514
2549
  tmux_endpoint_source: Option<&str>,
2515
2550
  state: &serde_json::Value,
2516
2551
  ) -> Vec<Option<Result<ParallelSpawnResult, String>>> {
@@ -2589,7 +2624,7 @@ fn run_bounded_parallel_worker_spawns(
2589
2624
  first_input.session_id.as_ref(),
2590
2625
  first_session_live,
2591
2626
  transport,
2592
- Some(safety),
2627
+ None,
2593
2628
  first_input.layout_placement.as_ref(),
2594
2629
  None,
2595
2630
  tmux_endpoint_source,
@@ -2673,7 +2708,7 @@ fn run_bounded_parallel_worker_spawns(
2673
2708
  input.session_id.as_ref(),
2674
2709
  /* into_existing_session */ true,
2675
2710
  transport,
2676
- Some(safety),
2711
+ None,
2677
2712
  input.layout_placement.as_ref(),
2678
2713
  None,
2679
2714
  tmux_endpoint_source,
@@ -2716,7 +2751,6 @@ fn apply_marked_respawn(
2716
2751
  team_key: &str,
2717
2752
  state: &mut serde_json::Value,
2718
2753
  transport: &(dyn crate::transport::Transport),
2719
- safety: &DangerousApproval,
2720
2754
  phase_timer: &RestartPhaseTimer,
2721
2755
  decision: &RestartedAgent,
2722
2756
  spawn: &SpawnedAgentWindow,
@@ -2725,6 +2759,7 @@ fn apply_marked_respawn(
2725
2759
  successful_agents: &mut Vec<RestartedAgent>,
2726
2760
  failed_agents: &mut Vec<RestartFailedAgent>,
2727
2761
  fatal_resume_failure: &mut bool,
2762
+ launched_bypass: bool,
2728
2763
  ) {
2729
2764
  let verify_start = std::time::Instant::now();
2730
2765
  if let Err(error) =
@@ -2735,7 +2770,7 @@ fn apply_marked_respawn(
2735
2770
  decision.restart_mode,
2736
2771
  spawn,
2737
2772
  transport,
2738
- safety,
2773
+ launched_bypass,
2739
2774
  )
2740
2775
  })
2741
2776
  {
@@ -2776,7 +2811,16 @@ fn apply_marked_respawn(
2776
2811
  .and_then(|agents| agents.get_mut(decision.agent_id.as_str()))
2777
2812
  .and_then(serde_json::Value::as_object_mut)
2778
2813
  {
2779
- persist_effective_approval_policy_for_restart(agent, safety);
2814
+ // 0.5.66 bypass 单源:policy 从 agent 行字段派生。
2815
+ let agent_provider = agent
2816
+ .get("provider")
2817
+ .and_then(serde_json::Value::as_str)
2818
+ .and_then(crate::provider::wire::parse_canonical_provider)
2819
+ .unwrap_or(crate::model::enums::Provider::Codex);
2820
+ crate::lifecycle::launch::persist_effective_approval_policy_from_agent(
2821
+ agent,
2822
+ agent_provider,
2823
+ );
2780
2824
  }
2781
2825
  let _ = crate::db::agent_health_capture::clear_agent_health_observation(
2782
2826
  run_workspace,
@@ -3728,7 +3772,7 @@ tasks:
3728
3772
  StartMode::Resumed,
3729
3773
  &spawn,
3730
3774
  &RespawnEpochTransport,
3731
- &disabled_safety(),
3775
+ false,
3732
3776
  )
3733
3777
  .expect("mark respawned");
3734
3778
 
@@ -393,7 +393,7 @@ fn write_min_team(ws: &Path, name: &str) -> PathBuf {
393
393
  .unwrap();
394
394
  std::fs::write(
395
395
  team.join("agents/worker_a.md"),
396
- "---\nname: worker_a\nrole: Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nWorker.\n",
396
+ "---\nname: worker_a\nrole: Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nWorker.\n",
397
397
  )
398
398
  .unwrap();
399
399
  team
@@ -330,7 +330,7 @@ fn add_agent_joins_w2_into_running_roster_and_existing_session() {
330
330
  let role_file = team_dir.join("w2-role.md");
331
331
  std::fs::write(
332
332
  &role_file,
333
- "---\nname: w2\nrole: Worker Two\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nWorker two.\n",
333
+ "---\nname: w2\nrole: Worker Two\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nWorker two.\n",
334
334
  )
335
335
  .unwrap();
336
336
  let transport = OfflineTransport::new().with_session_present(true);
@@ -518,9 +518,9 @@ if [ "$track" != 1 ]; then
518
518
  fi
519
519
  # 0.5.39 Slice 2: worker spawn now goes through the worker shell
520
520
  # wrapper (tmux-server-death-locate §7 Slice 2), which embeds a
521
- # printf format literal containing "\n" bytes so the pane returns to
522
- # an interactive shell with an explicit exit marker instead of
523
- # collapsing to `[exited]`. Those embedded newlines land inside the
521
+ # printf format literal containing "\n" bytes so the pane retains an
522
+ # inert sh tail with an explicit exit marker instead of collapsing to
523
+ # `[exited]`. Those embedded newlines land inside the
524
524
  # tmux argv payload, so a naive `printf '%s\n' "$*"` writes multiple
525
525
  # lines per tmux invocation and downstream `raw.lines()` scans see
526
526
  # fake "argv" lines that carry only the marker text. Escape newlines
@@ -528,7 +528,7 @@ fn write_team(workspace: &Path, fixture: &TeamFixture<'_>) -> PathBuf {
528
528
  std::fs::write(
529
529
  team.join("agents").join(format!("{}.md", agent.id)),
530
530
  format!(
531
- "---\nname: {}\nrole: {}\nprovider: {}\n{model}auth_mode: {}\n{profile}tools:\n{tools}---\n\n{}\n",
531
+ "---\nname: {}\nrole: {}\nprovider: {}\n{model}auth_mode: {}\n{profile}dangerously_skip_permissions: false\ntools:\n{tools}---\n\n{}\n",
532
532
  agent.id, agent.role, agent.provider, agent.auth_mode, agent.body
533
533
  ),
534
534
  )
@@ -152,14 +152,14 @@ fn write_claude_profile_team(ws: &Path, team_key: &str, agent_id: &str) -> PathB
152
152
  std::fs::write(
153
153
  team.join("TEAM.md"),
154
154
  format!(
155
- "---\nname: {team_key}\nobjective: Claude compatible config contract.\nprovider: claude\nauth_mode: compatible_api\n---\n\nTeam.\n"
155
+ "---\nname: {team_key}\nobjective: Claude compatible config contract.\nprovider: claude\nauth_mode: compatible_api\ndangerously_skip_permissions: false\n---\n\nTeam.\n"
156
156
  ),
157
157
  )
158
158
  .unwrap();
159
159
  std::fs::write(
160
160
  team.join("agents").join(format!("{agent_id}.md")),
161
161
  format!(
162
- "---\nname: {agent_id}\nrole: Claude Worker\nprovider: claude\nauth_mode: compatible_api\nprofile: local\nmodel: null\ntools:\n - mcp_team\n---\n\nWorker.\n"
162
+ "---\nname: {agent_id}\nrole: Claude Worker\nprovider: claude\nauth_mode: compatible_api\ndangerously_skip_permissions: false\nprofile: local\nmodel: null\ntools:\n - mcp_team\n---\n\nWorker.\n"
163
163
  ),
164
164
  )
165
165
  .unwrap();
@@ -218,14 +218,14 @@ fn write_claude_profile_team(ws: &Path, team_key: &str, agent_id: &str) -> PathB
218
218
  std::fs::write(
219
219
  team.join("TEAM.md"),
220
220
  format!(
221
- "---\nname: {team_key}\nobjective: Claude compatible profile launch contract.\nprovider: claude\nauth_mode: compatible_api\n---\n\nTeam.\n"
221
+ "---\nname: {team_key}\nobjective: Claude compatible profile launch contract.\nprovider: claude\nauth_mode: compatible_api\ndangerously_skip_permissions: false\n---\n\nTeam.\n"
222
222
  ),
223
223
  )
224
224
  .unwrap();
225
225
  std::fs::write(
226
226
  team.join("agents").join(format!("{agent_id}.md")),
227
227
  format!(
228
- "---\nname: {agent_id}\nrole: Claude Worker\nprovider: claude\nauth_mode: compatible_api\nprofile: local\nmodel: null\ntools:\n - mcp_team\n---\n\nWorker.\n"
228
+ "---\nname: {agent_id}\nrole: Claude Worker\nprovider: claude\nauth_mode: compatible_api\ndangerously_skip_permissions: false\nprofile: local\nmodel: null\ntools:\n - mcp_team\n---\n\nWorker.\n"
229
229
  ),
230
230
  )
231
231
  .unwrap();
@@ -247,14 +247,14 @@ fn write_claude_subscription_profile_team(ws: &Path, team_key: &str, agent_id: &
247
247
  std::fs::write(
248
248
  team.join("TEAM.md"),
249
249
  format!(
250
- "---\nname: {team_key}\nobjective: Claude subscription profile launch contract.\nprovider: claude\nauth_mode: subscription\n---\n\nTeam.\n"
250
+ "---\nname: {team_key}\nobjective: Claude subscription profile launch contract.\nprovider: claude\nauth_mode: subscription\ndangerously_skip_permissions: false\n---\n\nTeam.\n"
251
251
  ),
252
252
  )
253
253
  .unwrap();
254
254
  std::fs::write(
255
255
  team.join("agents").join(format!("{agent_id}.md")),
256
256
  format!(
257
- "---\nname: {agent_id}\nrole: Claude Worker\nprovider: claude\nauth_mode: subscription\nprofile: local\nmodel: null\ntools:\n - mcp_team\n---\n\nWorker.\n"
257
+ "---\nname: {agent_id}\nrole: Claude Worker\nprovider: claude\nauth_mode: subscription\ndangerously_skip_permissions: false\nprofile: local\nmodel: null\ntools:\n - mcp_team\n---\n\nWorker.\n"
258
258
  ),
259
259
  )
260
260
  .unwrap();
@@ -304,7 +304,7 @@ fn write_team_docs(workspace: &Path, provider: &str) {
304
304
  std::fs::write(
305
305
  workspace.join("agents").join(format!("{SOURCE}.md")),
306
306
  format!(
307
- "---\nname: {SOURCE}\nrole: {SOURCE}\nprovider: {provider}\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{SOURCE} body.\n"
307
+ "---\nname: {SOURCE}\nrole: {SOURCE}\nprovider: {provider}\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\n{SOURCE} body.\n"
308
308
  ),
309
309
  )
310
310
  .expect("write source role doc");
@@ -323,7 +323,7 @@ fn write_copilot_team_docs(workspace: &Path) -> PathBuf {
323
323
  std::fs::write(
324
324
  team_dir.join("agents").join(format!("{SOURCE}.md")),
325
325
  format!(
326
- "---\nname: {SOURCE}\nrole: Copilot Worker\nprovider: copilot\nmodel: gpt-5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nCopilot source body.\n"
326
+ "---\nname: {SOURCE}\nrole: Copilot Worker\nprovider: copilot\nmodel: gpt-5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nCopilot source body.\n"
327
327
  ),
328
328
  )
329
329
  .expect("write source role doc");
@@ -502,20 +502,16 @@ fn r8_copilot_fork_is_no_longer_capability_refused() {
502
502
  }
503
503
 
504
504
  /// R7 permission-clamp GUARDRAIL — a fork's effective approval policy is derived
505
- /// from the LEADER and never escalates above it (MUST-16). Naturally green at
506
- /// baseline (the role model forces permission_mode=restricted and inherits
507
- /// approval from the leader); its teeth engage after implement recompiles the
508
- /// role via RoleSourceResolver any raw clone of a compiled permission above the
509
- /// leader trips it. The true escalation scenario (a source declaring capability
510
- /// above the leader) is a real-machine concern.
505
+ /// from the LEADER and never escalates above it (MUST-16). 0.5.66 bypass 单源:
506
+ /// fork approval policy 由源角色的 `dangerously_skip_permissions` 派生
507
+ /// (本 fixture = false `source=disabled`),不再依赖 leader argv 继承。任何
508
+ /// fork 都不得出现 `source=leader_process`(旧模型已删)或 capability 高于源。
511
509
  #[test]
512
510
  fn r7_fork_effective_permissions_are_clamped_to_leader_guardrail() {
513
511
  // Claude source + success-path backing shim so the fork COMPLETES and a NEW
514
- // row exists to inspect — no vacuous pass on a refused fork. The dangerous
515
- // leader is established by the PINNED ancestry seam (see ANCESTRY_ENV note):
516
- // `source=leader_process` is only correct when a dangerous ancestor is
517
- // actually detected, so the fixture must supply one instead of borrowing
518
- // the host's.
512
+ // row exists to inspect — no vacuous pass on a refused fork. The source role
513
+ // declares `dangerously_skip_permissions: false`, so the fork's policy must
514
+ // resolve to `disabled` (fail-closed) the new single-source model.
519
515
  let case = Case::start("cf-r7");
520
516
  let fork = case.fork(NEW);
521
517
  assert_eq!(
@@ -538,11 +534,17 @@ fn r7_fork_effective_permissions_are_clamped_to_leader_guardrail() {
538
534
  let above = policy
539
535
  .get("worker_capability_above_leader")
540
536
  .and_then(Value::as_bool);
541
- assert_eq!(
537
+ assert_ne!(
542
538
  source,
543
539
  Some("leader_process"),
544
- "guardrail: a fork's effective approval policy must be derived from the leader \
545
- (source=leader_process), not raw-cloned from the source; MUST-16. policy={policy}"
540
+ "0.5.66: fork policy must never be leader_process (old ancestry model removed); \
541
+ policy={policy}"
542
+ );
543
+ assert_eq!(
544
+ source,
545
+ Some("disabled"),
546
+ "guardrail: a fork's approval policy derives from the role's dangerously_skip_permissions \
547
+ (false → disabled), never raw-cloned escalation; MUST-16. policy={policy}"
546
548
  );
547
549
  assert_ne!(
548
550
  above,
@@ -552,18 +554,9 @@ fn r7_fork_effective_permissions_are_clamped_to_leader_guardrail() {
552
554
  );
553
555
  }
554
556
 
555
- /// R7b reverse positive control — with a pinned RESTRICTED leader ancestry the
556
- /// fork's policy must resolve to `source=disabled` (fail-closed), never
557
- /// `leader_process`, and still never escalate. This is the CI-truth face of the
558
- /// clamp (a clean runner == no dangerous ancestor) and proves r7 does not pass
559
- /// by constant behavior: the same fixture flips leader_process↔disabled purely
560
- /// on the pinned seam. Mirrors the deterministic product contract
561
- /// `lifecycle/tests/core.rs` (clean mocked ancestry → Disabled).
562
- ///
563
- /// NOTE deliberately NOT asserted with the seam UNSET: unpinned, the policy is
564
- /// a function of the host process tree (the exact non-hermeticity this
565
- /// revision removes), so an unset-seam assertion would be red on any host
566
- /// whose own leader runs dangerously.
557
+ /// R7b reverse positive control — same source role (dangerously_skip_permissions:
558
+ /// false) must also resolve to `source=disabled` under any ancestry seam. 0.5.66
559
+ /// policy leader argv 无关:source `disabled`,fork 恒不升级(MUST-16)。
567
560
  #[test]
568
561
  fn r7b_fork_policy_under_restricted_leader_ancestry_is_disabled() {
569
562
  let case = Case::start_with_ancestry("cf-r7b", RESTRICTED_LEADER_ARGV);
@@ -587,16 +580,15 @@ fn r7b_fork_policy_under_restricted_leader_ancestry_is_disabled() {
587
580
  assert_eq!(
588
581
  policy.get("source").and_then(Value::as_str),
589
582
  Some("disabled"),
590
- "reverse control: a restricted/clean leader ancestry must resolve to source=disabled \
591
- (fail-closed), never leader_process. policy={policy}"
583
+ "fail-closed: source role declares dangerously_skip_permissions=false fork policy must \
584
+ be disabled; policy={policy}"
592
585
  );
593
586
  assert_ne!(
594
587
  policy
595
588
  .get("worker_capability_above_leader")
596
589
  .and_then(Value::as_bool),
597
590
  Some(true),
598
- "reverse control: a restricted leader must never yield a fork above the leader; MUST-16. \
599
- policy={policy}"
591
+ "a fork must never yield a capability above the source; MUST-16. policy={policy}"
600
592
  );
601
593
  }
602
594
 
@@ -246,7 +246,7 @@ fn fixture(tag: &str, team_mode: Option<&str>) -> PathBuf {
246
246
  std::fs::write(
247
247
  root.join("agents").join(format!("worker_{id}.md")),
248
248
  format!(
249
- "---\nname: worker_{id}\nrole: Communication Worker\nprovider: claude\ncommunication_mode: {id}\ntools:\n - mcp_team\n---\n\nROLE BODY SENTINEL {id}\n"
249
+ "---\nname: worker_{id}\nrole: Communication Worker\nprovider: claude\ndangerously_skip_permissions: false\ncommunication_mode: {id}\ntools:\n - mcp_team\n---\n\nROLE BODY SENTINEL {id}\n"
250
250
  ),
251
251
  )
252
252
  .unwrap();
@@ -265,7 +265,7 @@ fn default_fixture(tag: &str) -> PathBuf {
265
265
  }
266
266
  std::fs::write(
267
267
  root.join("agents").join("worker_default.md"),
268
- "---\nname: worker_default\nrole: Communication Worker\nprovider: claude\ntools:\n - mcp_team\n---\n\nROLE BODY SENTINEL default\n",
268
+ "---\nname: worker_default\nrole: Communication Worker\nprovider: claude\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nROLE BODY SENTINEL default\n",
269
269
  )
270
270
  .unwrap();
271
271
  root