@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
@@ -1059,6 +1059,16 @@ fn execute_managed_leader_plan_attempt(
1059
1059
  workspace,
1060
1060
  session_existed_before,
1061
1061
  )?;
1062
+ // A-55 (window rename race): tmux defaults `allow-rename` + `automatic-rename`
1063
+ // ON, so the pane's shell wrapper can print an OSC title-set escape during
1064
+ // init (zshrc/starship/PROMPT_COMMAND) and rename the window away from WINDOW
1065
+ // between spawn-poll and attach — making `attach-session -t SESSION:WINDOW`
1066
+ // fail with "can't find window: WINDOW". Lock both off right after spawn,
1067
+ // before run_leader_argv. Best-effort: a failure must NOT abort the launcher
1068
+ // (old tmux may lack the option), but it is audited below.
1069
+ let (rename_result, rename_stderr) =
1070
+ lock_down_window_rename(&transport, session, &spawned.window);
1071
+ write_window_rename_lockdown_event(workspace, &rename_result, &rename_stderr);
1062
1072
  if let Err(error) = persist_managed_leader_binding(plan, workspace, &spawned) {
1063
1073
  cleanup_managed_leader_resources(
1064
1074
  &transport,
@@ -2246,6 +2256,44 @@ fn write_leader_startup_prompt_event(workspace: &Path, event: &str, fields: serd
2246
2256
  let _ = crate::event_log::EventLog::new(workspace).write(event, fields);
2247
2257
  }
2248
2258
 
2259
+ /// A-55 (window rename race): lock the freshly-spawned leader window against
2260
+ /// rename — `allow-rename off` (refuse pane-internal OSC title-set renames)
2261
+ /// AND `automatic-rename off` (refuse tmux's own command-driven rename). Both
2262
+ /// must be set; tmux treats them as independent. Best-effort: a failure must
2263
+ /// NOT abort the launcher (old tmux may lack the option) — return the outcome
2264
+ /// for audit instead of propagating an error. Returns (result, tmux_stderr).
2265
+ fn lock_down_window_rename(
2266
+ transport: &dyn Transport,
2267
+ session: &SessionName,
2268
+ window: &WindowName,
2269
+ ) -> (String, String) {
2270
+ let allow_rename = transport.set_window_option(session, window, "allow-rename", "off");
2271
+ let automatic_rename = transport.set_window_option(session, window, "automatic-rename", "off");
2272
+ let (result, stderr) = match (&allow_rename, &automatic_rename) {
2273
+ (Ok(()), Ok(())) => ("ok".to_string(), String::new()),
2274
+ (Ok(()), Err(error)) => ("failed".to_string(), error.to_string()),
2275
+ (Err(error), Ok(())) => ("failed".to_string(), error.to_string()),
2276
+ (Err(a), Err(b)) => (
2277
+ "failed".to_string(),
2278
+ format!("allow-rename: {a}; automatic-rename: {b}"),
2279
+ ),
2280
+ };
2281
+ (result, stderr)
2282
+ }
2283
+
2284
+ /// A-55: audit the rename-lockdown outcome to the event log. Failure does not
2285
+ /// abort the launcher; record ok/failed + tmux stderr for regression triage.
2286
+ fn write_window_rename_lockdown_event(workspace: &Path, result: &str, stderr: &str) {
2287
+ write_leader_startup_prompt_event(
2288
+ workspace,
2289
+ "leader.launcher.window_rename_lockdown",
2290
+ serde_json::json!({
2291
+ "result": result,
2292
+ "tmux_stderr": stderr,
2293
+ }),
2294
+ );
2295
+ }
2296
+
2249
2297
  fn ensure_tmux_installed() -> Result<(), LeaderError> {
2250
2298
  match Command::new("tmux").arg("-V").output() {
2251
2299
  Ok(output) if output.status.success() => Ok(()),
@@ -2350,8 +2398,8 @@ mod tests {
2350
2398
  use super::{
2351
2399
  ensure_managed_provider_live_after_attach, execute_leader_plan_after_ambient_authority,
2352
2400
  format_launcher_failure, handle_exec_provider_startup_prompts,
2353
- is_interactive_shell_basename, push_bounded_stderr, shlex_quote,
2354
- VerifiedAmbientPaneAuthority, LEADER_STDERR_LIMIT,
2401
+ is_interactive_shell_basename, lock_down_window_rename, push_bounded_stderr, shlex_quote,
2402
+ write_window_rename_lockdown_event, VerifiedAmbientPaneAuthority, LEADER_STDERR_LIMIT,
2355
2403
  };
2356
2404
 
2357
2405
  struct ScriptedTransport {
@@ -2359,6 +2407,8 @@ mod tests {
2359
2407
  sent: Mutex<Vec<(Target, Vec<Key>)>>,
2360
2408
  liveness: PaneLiveness,
2361
2409
  targets: Vec<PaneInfo>,
2410
+ window_options: Mutex<Vec<(String, String)>>,
2411
+ window_option_error: Mutex<Option<String>>,
2362
2412
  }
2363
2413
 
2364
2414
  impl ScriptedTransport {
@@ -2368,6 +2418,8 @@ mod tests {
2368
2418
  sent: Mutex::new(Vec::new()),
2369
2419
  liveness: PaneLiveness::Unknown,
2370
2420
  targets: Vec::new(),
2421
+ window_options: Mutex::new(Vec::new()),
2422
+ window_option_error: Mutex::new(None),
2371
2423
  }
2372
2424
  }
2373
2425
 
@@ -2377,6 +2429,8 @@ mod tests {
2377
2429
  sent: Mutex::new(Vec::new()),
2378
2430
  liveness,
2379
2431
  targets: Vec::new(),
2432
+ window_options: Mutex::new(Vec::new()),
2433
+ window_option_error: Mutex::new(None),
2380
2434
  }
2381
2435
  }
2382
2436
 
@@ -2386,6 +2440,21 @@ mod tests {
2386
2440
  sent: Mutex::new(Vec::new()),
2387
2441
  liveness,
2388
2442
  targets,
2443
+ window_options: Mutex::new(Vec::new()),
2444
+ window_option_error: Mutex::new(None),
2445
+ }
2446
+ }
2447
+
2448
+ /// A-55: a ScriptedTransport whose `set_window_option` always fails with
2449
+ /// the given tmux stderr — proves the lockdown never aborts the launcher.
2450
+ fn with_window_option_error(error: &str) -> Self {
2451
+ Self {
2452
+ screens: Mutex::new(Vec::new()),
2453
+ sent: Mutex::new(Vec::new()),
2454
+ liveness: PaneLiveness::Unknown,
2455
+ targets: Vec::new(),
2456
+ window_options: Mutex::new(Vec::new()),
2457
+ window_option_error: Mutex::new(Some(error.to_string())),
2389
2458
  }
2390
2459
  }
2391
2460
 
@@ -2395,6 +2464,14 @@ mod tests {
2395
2464
  Err(poisoned) => poisoned.into_inner().clone(),
2396
2465
  }
2397
2466
  }
2467
+
2468
+ /// All `set-window-option option value` pairs, in call order.
2469
+ fn window_options(&self) -> Vec<(String, String)> {
2470
+ match self.window_options.lock() {
2471
+ Ok(guard) => guard.clone(),
2472
+ Err(poisoned) => poisoned.into_inner().clone(),
2473
+ }
2474
+ }
2398
2475
  }
2399
2476
 
2400
2477
  impl Transport for ScriptedTransport {
@@ -2511,6 +2588,34 @@ mod tests {
2511
2588
  Ok(SetEnvOutcome::Applied)
2512
2589
  }
2513
2590
 
2591
+ fn set_window_option(
2592
+ &self,
2593
+ _session: &SessionName,
2594
+ _window: &WindowName,
2595
+ option: &str,
2596
+ value: &str,
2597
+ ) -> Result<(), TransportError> {
2598
+ if let Some(stderr) = self
2599
+ .window_option_error
2600
+ .lock()
2601
+ .ok()
2602
+ .and_then(|guard| guard.clone())
2603
+ {
2604
+ return Err(TransportError::Subprocess {
2605
+ argv: vec!["tmux".to_string(), "set-window-option".to_string()],
2606
+ code: Some(1),
2607
+ stderr,
2608
+ });
2609
+ }
2610
+ match self.window_options.lock() {
2611
+ Ok(mut guard) => guard.push((option.to_string(), value.to_string())),
2612
+ Err(poisoned) => poisoned
2613
+ .into_inner()
2614
+ .push((option.to_string(), value.to_string())),
2615
+ }
2616
+ Ok(())
2617
+ }
2618
+
2514
2619
  fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
2515
2620
  Ok(())
2516
2621
  }
@@ -2988,4 +3093,64 @@ mod tests {
2988
3093
  assert_eq!(captured.len(), LEADER_STDERR_LIMIT);
2989
3094
  assert!(captured.ends_with(b"XYZ"));
2990
3095
  }
3096
+
3097
+ #[test]
3098
+ fn managed_leader_launch_disables_window_rename() {
3099
+ let transport = ScriptedTransport::with_liveness(PaneLiveness::Live);
3100
+ let session = SessionName::new("team-agent-leader-claude_code-demo");
3101
+ let window = WindowName::new("claude_code");
3102
+
3103
+ let (result, stderr) = lock_down_window_rename(&transport, &session, &window);
3104
+
3105
+ assert_eq!(result, "ok", "both set-window-option calls must succeed");
3106
+ assert!(stderr.is_empty());
3107
+ let options = transport.window_options();
3108
+ assert_eq!(
3109
+ options,
3110
+ vec![
3111
+ ("allow-rename".to_string(), "off".to_string()),
3112
+ ("automatic-rename".to_string(), "off".to_string()),
3113
+ ],
3114
+ "both rename lockdown options must be issued, in order"
3115
+ );
3116
+ }
3117
+
3118
+ #[test]
3119
+ fn launcher_survives_set_window_option_failure() {
3120
+ let workspace =
3121
+ std::env::temp_dir().join(format!("ta-rename-lockdown-fail-{}", std::process::id()));
3122
+ std::fs::create_dir_all(&workspace).unwrap();
3123
+ let transport = ScriptedTransport::with_window_option_error(
3124
+ "set-window-option: unknown option: allow-rename",
3125
+ );
3126
+ let session = SessionName::new("team-agent-leader-claude_code-demo");
3127
+ let window = WindowName::new("claude_code");
3128
+
3129
+ // Lockdown must NOT abort the launcher: it returns a failed outcome
3130
+ // (recorded for audit) instead of propagating an error.
3131
+ let (result, stderr) = lock_down_window_rename(&transport, &session, &window);
3132
+
3133
+ assert_eq!(result, "failed", "a set-window-option failure is audited, not fatal");
3134
+ assert!(
3135
+ stderr.contains("unknown option"),
3136
+ "tmux stderr must surface in the audit payload: {stderr}"
3137
+ );
3138
+
3139
+ // The audit event lands in the workspace event log with result=failed.
3140
+ write_window_rename_lockdown_event(&workspace, &result, &stderr);
3141
+ let events = crate::event_log::EventLog::new(&workspace).tail(0).unwrap();
3142
+ let lockdown = events
3143
+ .iter()
3144
+ .find(|event| {
3145
+ event["event"] == "leader.launcher.window_rename_lockdown"
3146
+ })
3147
+ .expect("rename_lockdown event must be written even on failure");
3148
+ assert_eq!(lockdown["result"], "failed");
3149
+ assert!(
3150
+ lockdown["tmux_stderr"].as_str().unwrap_or_default().contains("unknown option"),
3151
+ "event must carry tmux stderr for regression triage: {lockdown}"
3152
+ );
3153
+
3154
+ std::fs::remove_dir_all(&workspace).ok();
3155
+ }
2991
3156
  }
@@ -1054,7 +1054,7 @@ fn team_dir(root: &Path, name: &str, agents: &[(&str, &str)]) -> PathBuf {
1054
1054
 
1055
1055
  fn role_doc(name: &str, role: &str) -> String {
1056
1056
  format!(
1057
- "---\nname: {name}\nrole: {role}\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{role}.\n"
1057
+ "---\nname: {name}\nrole: {role}\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\n{role}.\n"
1058
1058
  )
1059
1059
  }
1060
1060
 
@@ -337,7 +337,6 @@ pub(super) fn add_agent_with_transport_at_paths(
337
337
  )));
338
338
  }
339
339
  inject_agent_into_spec(&mut spec, compiled.agent, &compiled.id)?;
340
- let safety = effective_runtime_config(&spec)?;
341
340
  // E5 spec 迁移:重编译的 spec 原子写到 .team/runtime/<team_key>/(不落用户目录 team_dir)。
342
341
  let spec_path = crate::model::paths::runtime_spec_path(run_workspace, &canonical_team_key);
343
342
  // E42 (0.3.24 P0): capture pre-write bytes for atomic rollback. If anything
@@ -362,7 +361,6 @@ pub(super) fn add_agent_with_transport_at_paths(
362
361
  agent_id,
363
362
  &meta,
364
363
  role_file_path,
365
- &safety,
366
364
  ) {
367
365
  rollback_add_agent_atomic(
368
366
  run_workspace,
@@ -116,7 +116,6 @@ pub(super) fn upsert_agent_state_from_role(
116
116
  agent_id: &AgentId,
117
117
  meta: &Value,
118
118
  dynamic_role_file: &Path,
119
- safety: &DangerousApproval,
120
119
  ) -> Result<(), LifecycleError> {
121
120
  let mut state =
122
121
  crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
@@ -195,7 +194,13 @@ pub(super) fn upsert_agent_state_from_role(
195
194
  }
196
195
  }
197
196
  if let Some(obj) = entry.as_object_mut() {
198
- persist_effective_approval_policy(obj, safety);
197
+ // 0.5.66 bypass 单源:policy 从 role 文档字段派生,不再用全队 `DangerousApproval`。
198
+ let meta_provider = meta
199
+ .get("provider")
200
+ .and_then(Value::as_str)
201
+ .and_then(crate::provider::wire::parse_provider)
202
+ .unwrap_or(Provider::Codex);
203
+ persist_effective_approval_policy_from_yaml_agent(obj, meta, meta_provider);
199
204
  }
200
205
  agent_map.insert(agent_id.as_str().to_string(), entry);
201
206
  crate::lifecycle::restart::remove::clear_agent_retirement_in_state(&mut state, agent_id);
@@ -24,7 +24,6 @@ pub(super) fn running_agent_state(
24
24
  team_id: &str,
25
25
  pane_id: Option<&str>,
26
26
  pane_pid: Option<u32>,
27
- safety: &DangerousApproval,
28
27
  started_agent: Option<&StartedAgent>,
29
28
  profile_dir: Option<&Path>,
30
29
  ) -> Result<serde_json::Value, LifecycleError> {
@@ -84,7 +83,16 @@ pub(super) fn running_agent_state(
84
83
  permissions_json(agent, id, provider)
85
84
  .map_err(|e| LifecycleError::Compile(e.to_string()))?,
86
85
  );
87
- persist_effective_approval_policy(&mut state, safety);
86
+ // 0.5.66 bypass 单源:policy 从 agent 字段派生,不再用全队 `DangerousApproval`。
87
+ persist_effective_approval_policy_from_yaml_agent(&mut state, agent, provider);
88
+ // 0.5.66 bypass 单源 §2.6:fresh spawn 记录运行面 bypass 值(供 restart drift 挡板)。
89
+ state.insert(
90
+ "as_launched_dangerously_skip_permissions".to_string(),
91
+ serde_json::json!(matches!(
92
+ agent.get("dangerously_skip_permissions"),
93
+ Some(Value::Bool(true))
94
+ )),
95
+ );
88
96
  state.insert("session_id".to_string(), serde_json::Value::Null);
89
97
  state.insert("rollout_path".to_string(), serde_json::Value::Null);
90
98
  state.insert("captured_at".to_string(), serde_json::Value::Null);
@@ -162,3 +170,130 @@ pub(super) fn dangerous_approval_source_str(source: DangerousApprovalSource) ->
162
170
  DangerousApprovalSource::Disabled => "disabled",
163
171
  }
164
172
  }
173
+
174
+ /// 0.5.66 bypass 单源:从 agent 行 + provider 构造 approval policy JSON
175
+ /// (restart/fork/add 落盘用;取代全队 `DangerousApproval` 派生)。
176
+ pub(crate) fn effective_approval_policy_from_agent(
177
+ agent: &serde_json::Value,
178
+ provider: Provider,
179
+ ) -> serde_json::Value {
180
+ let enabled = agent
181
+ .get("dangerously_skip_permissions")
182
+ .and_then(serde_json::Value::as_bool)
183
+ .unwrap_or(false);
184
+ if !enabled {
185
+ return serde_json::json!({
186
+ "enabled": false,
187
+ "source": "disabled",
188
+ "inherited": false,
189
+ "explicit_yes_confirmed": false,
190
+ "provider": null,
191
+ "flag": null,
192
+ "worker_capability_above_leader": false,
193
+ });
194
+ }
195
+ let flag = crate::provider::bypass_flags::provider_bypass_flag(provider);
196
+ serde_json::json!({
197
+ "enabled": true,
198
+ "source": "runtime_config",
199
+ "inherited": false,
200
+ "explicit_yes_confirmed": true,
201
+ "provider": provider_wire_str(provider),
202
+ "flag": flag,
203
+ "worker_capability_above_leader": false,
204
+ })
205
+ }
206
+
207
+ fn provider_wire_str(provider: Provider) -> &'static str {
208
+ match provider {
209
+ Provider::Claude => "claude",
210
+ Provider::ClaudeCode => "claude_code",
211
+ Provider::Codex => "codex",
212
+ Provider::Copilot => "copilot",
213
+ Provider::GeminiCli => "gemini_cli",
214
+ Provider::Fake => "fake",
215
+ }
216
+ }
217
+
218
+ /// 0.5.66 bypass 单源:从 agent 行(state Map)+ provider 落盘 approval policy。
219
+ /// 目标与来源是同一 agent 行。
220
+ pub(crate) fn persist_effective_approval_policy_from_agent(
221
+ agent_state: &mut serde_json::Map<String, serde_json::Value>,
222
+ provider: Provider,
223
+ ) {
224
+ let agent = serde_json::Value::Object(agent_state.clone());
225
+ agent_state.insert(
226
+ "effective_approval_policy".to_string(),
227
+ effective_approval_policy_from_agent(&agent, provider),
228
+ );
229
+ }
230
+
231
+ /// 0.5.66 bypass 单源:yaml spec agent 版(launch/state_projection 用)。
232
+ pub(crate) fn persist_effective_approval_policy_from_yaml_agent(
233
+ agent_state: &mut serde_json::Map<String, serde_json::Value>,
234
+ agent: &Value,
235
+ provider: Provider,
236
+ ) {
237
+ let enabled = matches!(agent.get("dangerously_skip_permissions"), Some(Value::Bool(true)));
238
+ let policy = if !enabled {
239
+ serde_json::json!({
240
+ "enabled": false,
241
+ "source": "disabled",
242
+ "inherited": false,
243
+ "explicit_yes_confirmed": false,
244
+ "provider": null,
245
+ "flag": null,
246
+ "worker_capability_above_leader": false,
247
+ })
248
+ } else {
249
+ let flag = crate::provider::bypass_flags::provider_bypass_flag(provider);
250
+ serde_json::json!({
251
+ "enabled": true,
252
+ "source": "runtime_config",
253
+ "inherited": false,
254
+ "explicit_yes_confirmed": true,
255
+ "provider": provider_wire_str(provider),
256
+ "flag": flag,
257
+ "worker_capability_above_leader": false,
258
+ })
259
+ };
260
+ agent_state.insert("effective_approval_policy".to_string(), policy);
261
+ }
262
+
263
+ #[cfg(test)]
264
+ mod tests {
265
+ #![allow(clippy::unwrap_used)]
266
+ use super::*;
267
+
268
+ fn yaml_agent(bypass: bool) -> Value {
269
+ crate::model::yaml::loads(&format!(
270
+ "id: a\nrole: r\nprovider: codex\ndangerously_skip_permissions: {bypass}\n"
271
+ ))
272
+ .unwrap()
273
+ }
274
+
275
+ // 0.5.66 bypass 单源 §4.1:true 角色 → policy 落盘为 enabled=true / runtime_config,
276
+ // 且 running_agent_state 记录 as_launched_dangerously_skip_permissions=true(审计留痕前提)。
277
+ #[test]
278
+ fn test_audit_log_written_on_true_spawn() {
279
+ let mut state = serde_json::Map::new();
280
+ persist_effective_approval_policy_from_yaml_agent(&mut state, &yaml_agent(true), Provider::Codex);
281
+ let policy = state.get("effective_approval_policy").unwrap();
282
+ assert_eq!(policy["enabled"], serde_json::json!(true));
283
+ assert_eq!(policy["source"], serde_json::json!("runtime_config"));
284
+ assert_eq!(
285
+ policy["flag"],
286
+ serde_json::json!("--dangerously-bypass-approvals-and-sandbox")
287
+ );
288
+ // 运行面记录(as-launched)供 restart drift 挡板。
289
+ let mut run_state = serde_json::Map::new();
290
+ run_state.insert(
291
+ "as_launched_dangerously_skip_permissions".to_string(),
292
+ serde_json::json!(matches!(
293
+ yaml_agent(true).get("dangerously_skip_permissions"),
294
+ Some(Value::Bool(true))
295
+ )),
296
+ );
297
+ assert_eq!(run_state["as_launched_dangerously_skip_permissions"], serde_json::json!(true));
298
+ }
299
+ }
@@ -121,7 +121,7 @@ pub fn fork_agent_with_transport(
121
121
  )));
122
122
  }
123
123
  let model = new_agent.get("model").and_then(Value::as_str);
124
- let safety = effective_runtime_config(&new_spec)?;
124
+ let safety = effective_runtime_config_for_worker_spawn(new_agent, provider)?;
125
125
  let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
126
126
  new_agent,
127
127
  Some(as_agent_id.as_str()),
@@ -132,7 +132,6 @@ pub fn fork_agent_with_transport(
132
132
  let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
133
133
  &command_agent,
134
134
  provider,
135
- &safety,
136
135
  )?;
137
136
  let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
138
137
  let mcp_config = adapter.mcp_config(auth_mode).map_err(|e| {
@@ -232,6 +232,11 @@ pub(super) fn finalize_fork_state(
232
232
  .and_then(serde_json::Value::as_object_mut)
233
233
  {
234
234
  persist_effective_approval_policy(agent, input.safety);
235
+ // 0.5.66 bypass 单源 §2.6:fork fresh spawn 记录运行面 bypass 值。
236
+ agent.insert(
237
+ "as_launched_dangerously_skip_permissions".to_string(),
238
+ serde_json::json!(input.safety.enabled),
239
+ );
235
240
  }
236
241
  maybe_fail_fork_after_spawn("save_runtime_state")?;
237
242
  crate::state::repository::StateRepository::new(input.workspace)
@@ -27,7 +27,6 @@ pub(super) fn spawn_agents(
27
27
  spec_path: &Path,
28
28
  spec: &Value,
29
29
  session_name: &SessionName,
30
- safety: &DangerousApproval,
31
30
  transport: &dyn Transport,
32
31
  ) -> Result<Vec<StartedAgent>, LifecycleError> {
33
32
  // E5 解耦:team_dir(角色定义 + profiles 所在)≠ spec_path.parent()(spec 已迁出到 .team/runtime)。
@@ -100,9 +99,10 @@ pub(super) fn spawn_agents(
100
99
  let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
101
100
  &command_agent,
102
101
  provider,
103
- safety,
104
102
  )?;
105
103
  let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
104
+ // 0.5.66 bypass 单源:per-agent safety 只服务 env 注入与审计,不参与 argv 决策。
105
+ let safety = effective_runtime_config_for_worker_spawn(agent, provider)?;
106
106
  let mcp_team_id = runtime_team_key_for_spec(spec_path, spec, session_name);
107
107
  let mcp_config = adapter
108
108
  .mcp_config(auth_mode)
@@ -283,6 +283,25 @@ pub(super) fn spawn_agents(
283
283
  "spawn_epoch": spawn_epoch,
284
284
  })),
285
285
  );
286
+ // 0.5.66 bypass 单源 §2.7:最终 argv 含 bypass flag → 审计留痕。
287
+ if matches!(
288
+ agent.get("dangerously_skip_permissions"),
289
+ Some(Value::Bool(true))
290
+ ) {
291
+ let _ = event_log.write(
292
+ "worker.spawn_dangerously_skip_permissions",
293
+ serde_json::json!({
294
+ "agent_id": agent_id_raw,
295
+ "role": agent
296
+ .get("role")
297
+ .and_then(Value::as_str)
298
+ .unwrap_or_default(),
299
+ "provider": provider,
300
+ "trigger": "launch",
301
+ "spec_source_path": agent_id_spec_source_path(workspace, agent_id_raw),
302
+ }),
303
+ );
304
+ }
286
305
  }
287
306
  // 0.3.28 Step 4b: replaced the `adaptive_layout_plan` 3-pane tiling
288
307
  // with Python-parity 1-window-per-agent placement. Window name =
@@ -381,3 +400,21 @@ pub(super) fn spawn_agents(
381
400
  }
382
401
  Ok(started)
383
402
  }
403
+
404
+ /// 0.5.66 bypass 单源 §2.7:审计事件里角色 md 的 spec 来源路径(尽力解析,失败空串)。
405
+ pub(crate) fn agent_id_spec_source_path(workspace: &Path, agent_id: &str) -> String {
406
+ let team_dir = crate::state::persist::load_runtime_state(workspace)
407
+ .ok()
408
+ .and_then(|state| {
409
+ state
410
+ .get("team_dir")
411
+ .and_then(serde_json::Value::as_str)
412
+ .filter(|s| !s.is_empty())
413
+ .map(PathBuf::from)
414
+ });
415
+ team_dir
416
+ .map(|dir| dir.join("agents").join(format!("{agent_id}.md")))
417
+ .unwrap_or_else(|| workspace.join(".team/current/agents").join(format!("{agent_id}.md")))
418
+ .display()
419
+ .to_string()
420
+ }