@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
@@ -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
+ }
@@ -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
+ }