@team-agent/installer 0.5.65 → 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 (83) 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 +1 -1
  5. package/crates/team-agent/src/cli/tests/compile.rs +1 -1
  6. package/crates/team-agent/src/cli/tests/mod.rs +2 -2
  7. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
  8. package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
  9. package/crates/team-agent/src/compiler/tests.rs +23 -20
  10. package/crates/team-agent/src/compiler.rs +25 -5
  11. package/crates/team-agent/src/conpty/backend.rs +16 -0
  12. package/crates/team-agent/src/leader/start.rs +167 -2
  13. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
  14. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +0 -2
  15. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +7 -2
  16. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +137 -2
  17. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -2
  18. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +5 -0
  19. package/crates/team-agent/src/lifecycle/launch/spawn.rs +39 -2
  20. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +105 -48
  21. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +0 -2
  22. package/crates/team-agent/src/lifecycle/launch.rs +117 -21
  23. package/crates/team-agent/src/lifecycle/restart/agent.rs +89 -3
  24. package/crates/team-agent/src/lifecycle/restart/common.rs +6 -9
  25. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +59 -15
  26. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
  27. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +1 -1
  28. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
  29. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
  30. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
  31. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +24 -32
  32. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
  33. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +13 -11
  34. package/crates/team-agent/src/lifecycle/tests/core.rs +0 -18
  35. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
  36. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
  37. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
  38. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
  39. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
  40. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +2 -2
  41. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +17 -30
  42. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +1 -1
  43. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +4 -4
  44. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
  45. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
  46. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
  47. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
  48. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1 -1
  49. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
  50. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
  51. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
  52. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -1
  53. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
  54. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
  55. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
  56. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
  57. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
  58. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
  59. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
  60. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +1 -1
  61. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
  62. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
  63. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +1 -1
  64. package/crates/team-agent/src/lifecycle/tests.rs +0 -2
  65. package/crates/team-agent/src/lifecycle/types.rs +0 -2
  66. package/crates/team-agent/src/lifecycle/worker_command_context.rs +115 -22
  67. package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
  68. package/crates/team-agent/src/model/spec.rs +129 -6
  69. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
  70. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
  71. package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
  72. package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
  73. package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
  74. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
  75. package/crates/team-agent/src/provider/bypass_flags.rs +80 -0
  76. package/crates/team-agent/src/provider/mod.rs +2 -0
  77. package/crates/team-agent/src/tmux_backend.rs +19 -0
  78. package/crates/team-agent/src/transport/test_support.rs +20 -0
  79. package/crates/team-agent/src/transport.rs +17 -0
  80. package/package.json +4 -4
  81. package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
  82. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
  83. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
@@ -33,7 +33,6 @@ use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
33
33
 
34
34
  use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
35
35
 
36
- use super::approval::detect_dangerous_approval;
37
36
  use super::identity::spec_display_backend;
38
37
  use super::leader_context::{
39
38
  attributed_provider_for_pane_across_tmux_sockets, caller_provider_for_seed_with_lookup,
@@ -387,64 +386,86 @@ pub(super) fn spec_default_assignee(spec: &Value) -> Option<AgentId> {
387
386
  .or_else(|| spec_agents(spec).into_iter().next())
388
387
  }
389
388
 
390
- pub(crate) fn effective_runtime_config(spec: &Value) -> Result<DangerousApproval, LifecycleError> {
391
- let enabled = spec
392
- .get("runtime")
393
- .and_then(|v| v.get("dangerous_auto_approve"))
394
- .is_some_and(Value::is_truthy);
395
- if enabled {
396
- let leader = detect_dangerous_approval()?;
397
- Ok(DangerousApproval {
398
- enabled: true,
399
- source: DangerousApprovalSource::RuntimeConfig,
389
+ /// 0.5.66 bypass 单源:从**单个 agent**(yaml spec) `dangerously_skip_permissions` 构造
390
+ /// `DangerousApproval`(取代旧"runtime config + leader argv"全队一份)。
391
+ /// true → source=RuntimeConfig(角色声明即用户明确同意,coordinator 的
392
+ /// `explicit_yes_confirmed` 自动放行);false → Disabled。
393
+ pub(crate) fn effective_runtime_config_for_worker_spawn(
394
+ agent: &Value,
395
+ provider: Provider,
396
+ ) -> Result<DangerousApproval, LifecycleError> {
397
+ let enabled = matches!(
398
+ agent.get("dangerously_skip_permissions"),
399
+ Some(Value::Bool(true))
400
+ );
401
+ if !enabled {
402
+ return Ok(DangerousApproval {
403
+ enabled: false,
404
+ source: DangerousApprovalSource::Disabled,
400
405
  inherited: false,
401
406
  provider: None,
402
407
  flag: None,
403
- worker_capability_above_leader: !leader.enabled,
404
- ancestry_binary_name: leader.ancestry_binary_name,
408
+ worker_capability_above_leader: false,
409
+ ancestry_binary_name: None,
405
410
  unexpected_binary: false,
406
- })
407
- } else {
408
- Ok(detect_dangerous_approval()?)
411
+ });
409
412
  }
413
+ let flag = crate::provider::bypass_flags::provider_bypass_flag(provider);
414
+ Ok(DangerousApproval {
415
+ enabled: true,
416
+ source: DangerousApprovalSource::RuntimeConfig,
417
+ inherited: false,
418
+ provider: Some(provider_display_str(provider).to_string()),
419
+ flag: flag.map(str::to_string),
420
+ worker_capability_above_leader: false,
421
+ ancestry_binary_name: None,
422
+ unexpected_binary: false,
423
+ })
410
424
  }
411
425
 
412
- pub(crate) fn effective_runtime_config_for_worker_spawn(
426
+ /// 0.5.66 bypass 单源:restart/state 路径的 serde_json 版(agent 来自 state JSON)。
427
+ pub(crate) fn effective_runtime_config_for_worker_spawn_json(
428
+ agent: &serde_json::Value,
429
+ provider: Provider,
413
430
  ) -> Result<DangerousApproval, LifecycleError> {
414
- detect_dangerous_approval()
431
+ let enabled = agent
432
+ .get("dangerously_skip_permissions")
433
+ .and_then(serde_json::Value::as_bool)
434
+ .unwrap_or(false);
435
+ if !enabled {
436
+ return Ok(DangerousApproval {
437
+ enabled: false,
438
+ source: DangerousApprovalSource::Disabled,
439
+ inherited: false,
440
+ provider: None,
441
+ flag: None,
442
+ worker_capability_above_leader: false,
443
+ ancestry_binary_name: None,
444
+ unexpected_binary: false,
445
+ });
446
+ }
447
+ let flag = crate::provider::bypass_flags::provider_bypass_flag(provider);
448
+ Ok(DangerousApproval {
449
+ enabled: true,
450
+ source: DangerousApprovalSource::RuntimeConfig,
451
+ inherited: false,
452
+ provider: Some(provider_display_str(provider).to_string()),
453
+ flag: flag.map(str::to_string),
454
+ worker_capability_above_leader: false,
455
+ ancestry_binary_name: None,
456
+ unexpected_binary: false,
457
+ })
415
458
  }
416
459
 
417
- pub(super) fn write_launch_permission_audit(
418
- workspace: &Path,
419
- safety: &DangerousApproval,
420
- ) -> Result<(), LifecycleError> {
421
- crate::event_log::EventLog::new(workspace)
422
- .write(
423
- "launch.permissions_resolved",
424
- serde_json::json!({
425
- "dangerous_auto_approve": safety.enabled,
426
- "dangerous_auto_approve_source": safety.source,
427
- "dangerous_auto_approve_inherited": safety.inherited,
428
- "dangerous_auto_approve_provider": safety.provider,
429
- "dangerous_auto_approve_flag": safety.flag,
430
- "worker_capability_above_leader": safety.worker_capability_above_leader,
431
- "ancestry_binary_name": safety.ancestry_binary_name,
432
- }),
433
- )
434
- .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
435
- if safety.unexpected_binary {
436
- crate::event_log::EventLog::new(workspace)
437
- .write(
438
- "dangerous_flag_in_unexpected_binary",
439
- serde_json::json!({
440
- "provider": safety.provider,
441
- "flag": safety.flag,
442
- "ancestry_binary_name": safety.ancestry_binary_name,
443
- }),
444
- )
445
- .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
460
+ pub(crate) fn provider_display_str(provider: Provider) -> &'static str {
461
+ match provider {
462
+ Provider::Claude => "claude",
463
+ Provider::ClaudeCode => "claude_code",
464
+ Provider::Codex => "codex",
465
+ Provider::Copilot => "copilot",
466
+ Provider::GeminiCli => "gemini_cli",
467
+ Provider::Fake => "fake",
446
468
  }
447
- Ok(())
448
469
  }
449
470
 
450
471
  pub(super) fn team_workspace(team_dir: &Path) -> PathBuf {
@@ -455,3 +476,39 @@ pub(super) fn team_workspace(team_dir: &Path) -> PathBuf {
455
476
  .unwrap_or_else(|| team_dir.to_path_buf())
456
477
  })
457
478
  }
479
+
480
+ #[cfg(test)]
481
+ mod tests {
482
+ #![allow(clippy::unwrap_used)]
483
+ use super::*;
484
+ use crate::model::enums::Provider;
485
+
486
+ fn yaml_agent(bypass: bool) -> Value {
487
+ crate::model::yaml::loads(&format!(
488
+ "id: a\nrole: r\nprovider: codex\ndangerously_skip_permissions: {bypass}\n"
489
+ ))
490
+ .unwrap()
491
+ }
492
+
493
+ // 0.5.66 bypass 单源 §4.1:true → enabled + source=runtime_config(角色声明即明确同意)。
494
+ #[test]
495
+ fn test_effective_safety_from_true_field() {
496
+ let safety =
497
+ effective_runtime_config_for_worker_spawn(&yaml_agent(true), Provider::Codex).unwrap();
498
+ assert!(safety.enabled);
499
+ assert_eq!(safety.source, DangerousApprovalSource::RuntimeConfig);
500
+ assert_eq!(
501
+ safety.flag.as_deref(),
502
+ Some("--dangerously-bypass-approvals-and-sandbox")
503
+ );
504
+ }
505
+
506
+ // 0.5.66 bypass 单源 §4.1:false → Disabled。
507
+ #[test]
508
+ fn test_effective_safety_from_false_field() {
509
+ let safety =
510
+ effective_runtime_config_for_worker_spawn(&yaml_agent(false), Provider::Codex).unwrap();
511
+ assert!(!safety.enabled);
512
+ assert_eq!(safety.source, DangerousApprovalSource::Disabled);
513
+ }
514
+ }
@@ -28,7 +28,6 @@ pub(super) fn persist_spawn_agent_state(
28
28
  session_name: &SessionName,
29
29
  transport: &dyn Transport,
30
30
  started: &[StartedAgent],
31
- safety: &DangerousApproval,
32
31
  ) -> Result<(), LifecycleError> {
33
32
  let state_path = crate::state::persist::runtime_state_path(workspace);
34
33
  let mut state = match crate::state::repository::StateRepository::new(workspace)
@@ -134,7 +133,6 @@ pub(super) fn persist_spawn_agent_state(
134
133
  &team_id,
135
134
  Some(agent_id_to_pane_id(started, id)),
136
135
  pane_pid,
137
- safety,
138
136
  started_agent,
139
137
  Some(&profile_dir),
140
138
  )?,
@@ -70,6 +70,7 @@ pub fn launch_with_transport_in_workspace(
70
70
  transport: &dyn Transport,
71
71
  ) -> Result<LaunchReport, LifecycleError> {
72
72
  let _ = skip_profile_smoke;
73
+ let _ = auto_approve; // 0.5.66: bypass 单源后无 runtime.dangerous_auto_approve,`--yes` 为 no-op。
73
74
  // 0.5.38 (`.team/artifacts/startup-latency-locate.md` §5): launch.phase
74
75
  // timer with monotonic `elapsed_ms` for latency triage.
75
76
  let phase_timer = crate::lifecycle::restart::RestartPhaseTimer::start();
@@ -82,14 +83,12 @@ pub fn launch_with_transport_in_workspace(
82
83
  let text = std::fs::read_to_string(spec_path)
83
84
  .map_err(|e| LifecycleError::Compile(format!("{}: {e}", spec_path.display())))?;
84
85
  let spec = yaml::loads(&text).map_err(|e| LifecycleError::Compile(e.to_string()))?;
86
+ // 0.5.66 §3.2 跨 workspace 兼容警示:leader argv 带 bypass flag 但当前团队
87
+ // 所有角色都未声明 dangerously_skip_permissions=true → 写 warning 事件不阻塞。
88
+ // TODO(0.6.0): remove(0.6.0 彻底删源头 A 后此警示无意义)。
89
+ emit_dangerous_flag_no_role_declared_warning(workspace, &spec);
85
90
  phase_timer.emit(workspace, "launch.phase", "compile_spec");
86
91
  let session_name = spec_session_name(&spec);
87
- let safety = effective_runtime_config(&spec)?;
88
- if safety.enabled && !safety.inherited && !auto_approve && !dry_run {
89
- return Err(LifecycleError::DangerousApprovalRequired(
90
- "runtime dangerous_auto_approve is enabled".to_string(),
91
- ));
92
- }
93
92
  if !dry_run && transport_has_session(transport, &session_name) {
94
93
  return Err(LifecycleError::SessionConflict(format!(
95
94
  "tmux session already exists: {}",
@@ -103,7 +102,6 @@ pub fn launch_with_transport_in_workspace(
103
102
  raw: serde_json::json!({"source": "compiled_spec"}),
104
103
  })
105
104
  .collect::<Vec<_>>();
106
- write_launch_permission_audit(workspace, &safety)?;
107
105
  let routes = spec_routes(&spec);
108
106
  let started = if dry_run {
109
107
  Vec::new()
@@ -114,7 +112,6 @@ pub fn launch_with_transport_in_workspace(
114
112
  spec_path,
115
113
  &spec,
116
114
  &session_name,
117
- &safety,
118
115
  transport,
119
116
  )?;
120
117
  persist_spawn_agent_state(
@@ -124,7 +121,6 @@ pub fn launch_with_transport_in_workspace(
124
121
  &session_name,
125
122
  transport,
126
123
  &started,
127
- &safety,
128
124
  )?;
129
125
  // 0.5.38: per-worker timing tags (source="launch") so operators can
130
126
  // trace which worker's spawn dominates wall time. Zeros for now on
@@ -181,17 +177,55 @@ pub fn launch_with_transport_in_workspace(
181
177
  tmux_endpoint: transport.tmux_endpoint(),
182
178
  routes,
183
179
  permissions,
184
- safety,
185
180
  leader_receiver_attached: false,
186
181
  session_capture_incomplete_agents: Vec::new(),
187
182
  })
188
183
  }
189
184
 
185
+ /// 0.5.66 §3.2 跨 workspace 升级警示(仅警示,不做行为决策)。
186
+ /// leader argv 含 bypass flag、但当前 spec 所有 agent 都未声明
187
+ /// `dangerously_skip_permissions=true` → 写 `dangerously_flag_present_but_no_role_declared`。
188
+ ///
189
+ /// TODO(0.6.0): remove(0.6.0 删源头 A 后此警示无意义)。
190
+ fn emit_dangerous_flag_no_role_declared_warning(workspace: &Path, spec: &Value) {
191
+ let argv: Vec<String> = std::env::args().collect();
192
+ emit_dangerous_flag_no_role_declared_warning_with_argv(workspace, spec, &argv);
193
+ }
194
+
195
+ /// 带显式 argv 的可测版。
196
+ fn emit_dangerous_flag_no_role_declared_warning_with_argv(
197
+ workspace: &Path,
198
+ spec: &Value,
199
+ argv: &[String],
200
+ ) {
201
+ if crate::provider::bypass_flags::detect_bypass_flag_in_argv(argv).is_none() {
202
+ return;
203
+ }
204
+ let any_role_declared = spec_agent_values(spec).iter().any(|agent| {
205
+ matches!(
206
+ agent.get("dangerously_skip_permissions"),
207
+ Some(Value::Bool(true))
208
+ )
209
+ });
210
+ if any_role_declared {
211
+ return;
212
+ }
213
+ let _ = crate::event_log::EventLog::new(workspace).write(
214
+ "dangerously_flag_present_but_no_role_declared",
215
+ serde_json::json!({
216
+ "message": "leader process carries a bypass flag but no role in this team declares \
217
+ dangerously_skip_permissions=true; bypass no longer inherits to workers \
218
+ (0.5.66 single-source). Declare the field per role to opt in.",
219
+ }),
220
+ );
221
+ }
222
+
190
223
  mod plan;
191
224
  pub(crate) use plan::{handle_report_result, start_plan};
192
225
 
193
226
  pub mod spawn;
194
227
  pub(super) use spawn::*;
228
+ pub(crate) use spawn::agent_id_spec_source_path;
195
229
 
196
230
  mod layout;
197
231
  pub(super) use layout::*;
@@ -221,7 +255,10 @@ pub(crate) use leader_context::{
221
255
 
222
256
  mod agent_state;
223
257
  pub(super) use agent_state::*;
224
- pub(crate) use agent_state::{effective_approval_policy, persist_effective_approval_policy};
258
+ pub(crate) use agent_state::{
259
+ effective_approval_policy, persist_effective_approval_policy,
260
+ persist_effective_approval_policy_from_agent,
261
+ };
225
262
 
226
263
  mod mcp_config;
227
264
  pub(super) use mcp_config::*;
@@ -266,14 +303,6 @@ pub mod readiness;
266
303
  pub(crate) use readiness::launched_team_receiver_is_attached;
267
304
  pub(super) use readiness::*;
268
305
 
269
- mod approval;
270
- pub use approval::detect_dangerous_approval;
271
- use approval::{
272
- binary_matches_provider, binary_name, dangerous_leader_flags,
273
- detect_dangerous_approval_in_argv, disabled_dangerous_approval, process_ancestry_argv,
274
- process_argv_tokens, process_parent_pid,
275
- };
276
-
277
306
  mod add_agent;
278
307
  pub(super) use add_agent::*;
279
308
  pub use add_agent::{add_agent, add_agent_force};
@@ -309,14 +338,81 @@ pub(crate) use ownership::{ensure_owner_allowed, ensure_owner_allowed_for_state,
309
338
 
310
339
  pub mod spec_state;
311
340
  pub(crate) use spec_state::{
312
- effective_runtime_config, effective_runtime_config_for_worker_spawn,
313
341
  override_spec_session_name, override_spec_workspace, spec_agent_id_set, write_spec_atomic,
314
342
  };
343
+ pub(crate) use spec_state::{
344
+ effective_runtime_config_for_worker_spawn, effective_runtime_config_for_worker_spawn_json,
345
+ };
315
346
  use spec_state::{
316
347
  env_nonempty, has_positive_caller_leader_env, initial_runtime_state,
317
348
  override_spec_display_backend, override_spec_runtime_str,
318
349
  seed_launched_owner_from_caller_with_provider_lookup, seed_launched_owner_from_env,
319
350
  spec_agent_values, spec_agents, spec_default_assignee, spec_routes, spec_session_name,
320
- spec_tasks_json, team_workspace, write_launch_permission_audit, yaml_value_to_json,
351
+ spec_tasks_json, team_workspace, yaml_value_to_json,
321
352
  };
322
353
  pub use spec_state::worker_session_name_pub;
354
+
355
+ #[cfg(test)]
356
+ mod tests {
357
+ #![allow(clippy::unwrap_used)]
358
+ use super::*;
359
+
360
+ // 0.5.66 §4.1 test_leader_flag_present_but_no_role_true_writes_warning_event:
361
+ // leader argv 带 bypass flag + 无角色声明 true → 写 warning 事件。
362
+ #[test]
363
+ fn test_leader_flag_present_but_no_role_true_writes_warning_event() {
364
+ let ws = std::env::temp_dir().join(format!(
365
+ "ta-bypass-warning-test-{}",
366
+ std::process::id()
367
+ ));
368
+ let _ = std::fs::remove_dir_all(&ws);
369
+ std::fs::create_dir_all(&ws).unwrap();
370
+ let spec = yaml::loads(
371
+ "version: 1\nteam: {}\nleader: {}\nagents:\n - id: a\n role: r\n provider: fake\n dangerously_skip_permissions: false\n",
372
+ )
373
+ .unwrap();
374
+ let argv = vec![
375
+ "team-agent".to_string(),
376
+ "--dangerously-skip-permissions".to_string(),
377
+ ];
378
+ emit_dangerous_flag_no_role_declared_warning_with_argv(&ws, &spec, &argv);
379
+ let events = crate::event_log::EventLog::new(&ws).tail(0).unwrap();
380
+ assert!(
381
+ events.iter().any(|event| {
382
+ event.get("event").and_then(serde_json::Value::as_str)
383
+ == Some("dangerously_flag_present_but_no_role_declared")
384
+ }),
385
+ "leader flag + no role declared must write the warning event; events={events:?}"
386
+ );
387
+ let _ = std::fs::remove_dir_all(&ws);
388
+ }
389
+
390
+ // 0.5.66 §4.1 同测试的反向:有角色声明 true → 不写 warning。
391
+ #[test]
392
+ fn test_leader_flag_present_but_role_declared_no_warning() {
393
+ let ws = std::env::temp_dir().join(format!(
394
+ "ta-bypass-warning-none-test-{}",
395
+ std::process::id()
396
+ ));
397
+ let _ = std::fs::remove_dir_all(&ws);
398
+ std::fs::create_dir_all(&ws).unwrap();
399
+ let spec = yaml::loads(
400
+ "version: 1\nteam: {}\nleader: {}\nagents:\n - id: a\n role: r\n provider: fake\n dangerously_skip_permissions: true\n",
401
+ )
402
+ .unwrap();
403
+ let argv = vec![
404
+ "team-agent".to_string(),
405
+ "--dangerously-skip-permissions".to_string(),
406
+ ];
407
+ emit_dangerous_flag_no_role_declared_warning_with_argv(&ws, &spec, &argv);
408
+ let events = crate::event_log::EventLog::new(&ws).tail(0).unwrap();
409
+ assert!(
410
+ !events.iter().any(|event| {
411
+ event.get("event").and_then(serde_json::Value::as_str)
412
+ == Some("dangerously_flag_present_but_no_role_declared")
413
+ }),
414
+ "role declared true must suppress the warning; events={events:?}"
415
+ );
416
+ let _ = std::fs::remove_dir_all(&ws);
417
+ }
418
+ }
@@ -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):
@@ -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,