@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
@@ -1,7 +1,7 @@
1
1
  use std::path::Path;
2
2
 
3
3
  use crate::communication_mode::CommunicationMode;
4
- use crate::lifecycle::types::{DangerousApproval, LifecycleError};
4
+ use crate::lifecycle::types::LifecycleError;
5
5
  use crate::model::enums::{Enforcement, Provider};
6
6
  use crate::model::ids::AgentId;
7
7
  use crate::model::permissions::{resolve_permissions, AgentPermissionInput};
@@ -42,6 +42,8 @@ pub(crate) struct WorkerCommandAgent {
42
42
  system_prompt_file: Option<String>,
43
43
  output_contract_format: Option<String>,
44
44
  communication_mode: CommunicationMode,
45
+ /// 0.5.66 bypass 单源:agent-level `dangerously_skip_permissions`(必填 bool)。
46
+ dangerously_skip_permissions: bool,
45
47
  }
46
48
 
47
49
  impl WorkerCommandAgent {
@@ -92,6 +94,10 @@ impl WorkerCommandAgent {
92
94
  .get("communication_mode")
93
95
  .and_then(crate::model::yaml::Value::as_str),
94
96
  )?,
97
+ dangerously_skip_permissions: matches!(
98
+ agent.get("dangerously_skip_permissions"),
99
+ Some(crate::model::yaml::Value::Bool(true))
100
+ ),
95
101
  })
96
102
  }
97
103
 
@@ -142,6 +148,10 @@ impl WorkerCommandAgent {
142
148
  .get("communication_mode")
143
149
  .and_then(serde_json::Value::as_str),
144
150
  )?,
151
+ dangerously_skip_permissions: agent
152
+ .get("dangerously_skip_permissions")
153
+ .and_then(serde_json::Value::as_bool)
154
+ .unwrap_or(false),
145
155
  })
146
156
  }
147
157
  }
@@ -175,19 +185,44 @@ pub(crate) fn compile_worker_system_prompt(
175
185
  pub(crate) fn resolved_tool_strings_for_command(
176
186
  agent: &WorkerCommandAgent,
177
187
  provider: Provider,
178
- safety: &DangerousApproval,
179
188
  ) -> Result<Vec<String>, LifecycleError> {
180
189
  let mut tools: Vec<String> = resolve_agent_permissions(agent, provider)?
181
190
  .sorted_tool_strings()
182
191
  .into_iter()
183
192
  .map(str::to_string)
184
193
  .collect();
185
- if safety.enabled && !tools.iter().any(|tool| tool == "dangerous_auto_approve") {
186
- tools.push("dangerous_auto_approve".to_string());
194
+ // 0.5.66 bypass 单源:读 agent 自己的 `dangerously_skip_permissions` bool,
195
+ // 不再收 team/runtime/leader argv 派生的 `DangerousApproval`。true → 加
196
+ // `dangerous_auto_approve` 哨兵(adapter 消费点不变);provider 无 bypass
197
+ // argv 定义 → fail-loud,不静默 fallback。
198
+ if agent.dangerously_skip_permissions {
199
+ let flag = crate::provider::bypass_flags::provider_bypass_flag(provider).ok_or_else(
200
+ || {
201
+ LifecycleError::RequirementUnmet(format!(
202
+ "provider {} has no bypass argv flag defined; dangerously_skip_permissions=true cannot be honored",
203
+ provider_display_name(provider)
204
+ ))
205
+ },
206
+ )?;
207
+ debug_assert!(flag.starts_with("--"));
208
+ if !tools.iter().any(|tool| tool == "dangerous_auto_approve") {
209
+ tools.push("dangerous_auto_approve".to_string());
210
+ }
187
211
  }
188
212
  Ok(tools)
189
213
  }
190
214
 
215
+ fn provider_display_name(provider: Provider) -> &'static str {
216
+ match provider {
217
+ Provider::Claude => "claude",
218
+ Provider::ClaudeCode => "claude_code",
219
+ Provider::Codex => "codex",
220
+ Provider::Copilot => "copilot",
221
+ Provider::GeminiCli => "gemini_cli",
222
+ Provider::Fake => "fake",
223
+ }
224
+ }
225
+
191
226
  fn resolve_agent_permissions(
192
227
  agent: &WorkerCommandAgent,
193
228
  provider: Provider,
@@ -282,20 +317,6 @@ fn provider_default_prompt_only_tools(provider: Provider) -> &'static [&'static
282
317
  #[cfg(test)]
283
318
  mod tests {
284
319
  use super::*;
285
- use crate::lifecycle::types::DangerousApprovalSource;
286
-
287
- fn disabled_safety() -> DangerousApproval {
288
- DangerousApproval {
289
- enabled: false,
290
- source: DangerousApprovalSource::Disabled,
291
- inherited: false,
292
- provider: None,
293
- flag: None,
294
- worker_capability_above_leader: false,
295
- ancestry_binary_name: None,
296
- unexpected_binary: false,
297
- }
298
- }
299
320
 
300
321
  #[test]
301
322
  fn empty_tools_use_role_defaults_and_aliases_resolve_before_command() {
@@ -308,10 +329,10 @@ mod tests {
308
329
  system_prompt_file: None,
309
330
  output_contract_format: None,
310
331
  communication_mode: CommunicationMode::default(),
332
+ dangerously_skip_permissions: false,
311
333
  };
312
334
  let tools =
313
- resolved_tool_strings_for_command(&agent, Provider::ClaudeCode, &disabled_safety())
314
- .unwrap();
335
+ resolved_tool_strings_for_command(&agent, Provider::ClaudeCode).unwrap();
315
336
  assert_eq!(
316
337
  tools,
317
338
  [
@@ -330,11 +351,82 @@ mod tests {
330
351
  ..agent
331
352
  };
332
353
  let tools =
333
- resolved_tool_strings_for_command(&agent, Provider::ClaudeCode, &disabled_safety())
334
- .unwrap();
354
+ resolved_tool_strings_for_command(&agent, Provider::ClaudeCode).unwrap();
335
355
  assert_eq!(tools, ["fs_list", "fs_read", "fs_write", "mcp_team"]);
336
356
  }
337
357
 
358
+ // 0.5.66 bypass 单源 §4.1:true → tools 追加 `dangerous_auto_approve` 哨兵。
359
+ #[test]
360
+ fn test_true_field_produces_bypass_flag_argv() {
361
+ let agent = WorkerCommandAgent {
362
+ id: Some("dev".to_string()),
363
+ provider: Provider::ClaudeCode,
364
+ role: Some("developer".to_string()),
365
+ declared_tools: Some(vec!["mcp_team".to_string()]),
366
+ system_prompt_inline: None,
367
+ system_prompt_file: None,
368
+ output_contract_format: None,
369
+ communication_mode: CommunicationMode::default(),
370
+ dangerously_skip_permissions: true,
371
+ };
372
+ let tools = resolved_tool_strings_for_command(&agent, Provider::ClaudeCode).unwrap();
373
+ assert!(
374
+ tools.iter().any(|tool| tool == "dangerous_auto_approve"),
375
+ "true field must append the dangerous_auto_approve sentinel; tools={tools:?}"
376
+ );
377
+ assert!(
378
+ tools.iter().any(|tool| tool == "mcp_team"),
379
+ "normal tools must remain; tools={tools:?}"
380
+ );
381
+ }
382
+
383
+ // 0.5.66 bypass 单源 §4.1:false → 不追加哨兵。
384
+ #[test]
385
+ fn test_false_field_produces_no_bypass_flag_argv() {
386
+ let agent = WorkerCommandAgent {
387
+ id: Some("dev".to_string()),
388
+ provider: Provider::ClaudeCode,
389
+ role: Some("developer".to_string()),
390
+ declared_tools: Some(vec!["mcp_team".to_string()]),
391
+ system_prompt_inline: None,
392
+ system_prompt_file: None,
393
+ output_contract_format: None,
394
+ communication_mode: CommunicationMode::default(),
395
+ dangerously_skip_permissions: false,
396
+ };
397
+ let tools = resolved_tool_strings_for_command(&agent, Provider::ClaudeCode).unwrap();
398
+ assert!(
399
+ !tools.iter().any(|tool| tool == "dangerous_auto_approve"),
400
+ "false field must not append the sentinel; tools={tools:?}"
401
+ );
402
+ }
403
+
404
+ // 0.5.66 bypass 单源 §4.1:provider 未定义 bypass 参数 + true → fail-loud。
405
+ #[test]
406
+ fn test_provider_without_bypass_flag_definition_errors() {
407
+ let agent = WorkerCommandAgent {
408
+ id: Some("dev".to_string()),
409
+ provider: Provider::GeminiCli,
410
+ role: Some("developer".to_string()),
411
+ declared_tools: Some(vec!["mcp_team".to_string()]),
412
+ system_prompt_inline: None,
413
+ system_prompt_file: None,
414
+ output_contract_format: None,
415
+ communication_mode: CommunicationMode::default(),
416
+ dangerously_skip_permissions: true,
417
+ };
418
+ let err = resolved_tool_strings_for_command(&agent, Provider::GeminiCli).unwrap_err();
419
+ let msg = err.to_string();
420
+ assert!(
421
+ msg.contains("no bypass argv flag defined"),
422
+ "provider without bypass flag + true must fail-loud; got {msg}"
423
+ );
424
+ assert!(
425
+ msg.contains("gemini_cli"),
426
+ "error must name the provider; got {msg}"
427
+ );
428
+ }
429
+
338
430
  #[test]
339
431
  fn system_prompt_uses_identity_then_runtime_contract_python_order() {
340
432
  // #264 D6: Python truth source (prompt.py:39, live ps confirmed) builds
@@ -353,6 +445,7 @@ mod tests {
353
445
  system_prompt_file: None,
354
446
  output_contract_format: Some("result_envelope_v1".to_string()),
355
447
  communication_mode: CommunicationMode::default(),
448
+ dangerously_skip_permissions: false,
356
449
  };
357
450
  let prompt = compile_worker_system_prompt(&agent).unwrap();
358
451
  assert!(
@@ -94,7 +94,7 @@ fn collect_result_file_ingests_valid_known_task_envelope_into_results() {
94
94
  .unwrap();
95
95
  std::fs::write(
96
96
  team.join("agents").join("w1.md"),
97
- "---\nname: w1\nrole: Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nW1.\n",
97
+ "---\nname: w1\nrole: Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nW1.\n",
98
98
  )
99
99
  .unwrap();
100
100
  let spec = crate::compiler::compile_team(&team).expect("compile collect team");
@@ -354,8 +354,31 @@ fn basic_schema_errors(spec: &Yaml) -> Vec<String> {
354
354
  );
355
355
  match spec.get("agents") {
356
356
  Some(Yaml::List(agents)) if !agents.is_empty() => {
357
+ // 0.5.66 bypass 单源:收集缺/错 `dangerously_skip_permissions` 的角色清单,
358
+ // 循环后一次性报出,满足"缺字段错误消息含每个缺字段的角色 id 清单"硬要求。
359
+ let mut missing_bypass: Vec<String> = Vec::new();
360
+ let mut bad_type_bypass: Vec<String> = Vec::new();
357
361
  for (idx, agent) in agents.iter().enumerate() {
358
362
  check_agent(agent, &format!("/agents/{idx}"), &mut e);
363
+ match agent.get("dangerously_skip_permissions") {
364
+ None => missing_bypass.push(agent_label(agent, idx)),
365
+ Some(value) if !matches!(value, Yaml::Bool(_)) => {
366
+ bad_type_bypass.push(agent_label(agent, idx))
367
+ }
368
+ Some(_) => {}
369
+ }
370
+ }
371
+ if !missing_bypass.is_empty() {
372
+ e.push(format!(
373
+ "/agents/dangerously_skip_permissions: missing required field (role id(s): {}). This field must be declared explicitly; it controls whether the agent launches with permission prompts bypassed.",
374
+ missing_bypass.join(", ")
375
+ ));
376
+ }
377
+ if !bad_type_bypass.is_empty() {
378
+ e.push(format!(
379
+ "/agents/dangerously_skip_permissions: must be a boolean (role id(s): {}). This field must be declared explicitly; it controls whether the agent launches with permission prompts bypassed.",
380
+ bad_type_bypass.join(", ")
381
+ ));
359
382
  }
360
383
  }
361
384
  _ => e.push("/agents: must be a non-empty list".to_string()),
@@ -384,7 +407,7 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
384
407
  "working_directory",
385
408
  "system_prompt",
386
409
  "tools",
387
- "permission_mode",
410
+ "dangerously_skip_permissions",
388
411
  "preferred_for",
389
412
  "avoid_for",
390
413
  "output_contract",
@@ -397,7 +420,7 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
397
420
  "working_directory",
398
421
  "system_prompt",
399
422
  "tools",
400
- "permission_mode",
423
+ "dangerously_skip_permissions",
401
424
  "preferred_for",
402
425
  "avoid_for",
403
426
  "output_contract",
@@ -407,6 +430,10 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
407
430
  "profile",
408
431
  "credential_ref",
409
432
  "forked_from",
433
+ // 存量角色 md 的 `permission_mode` 是历史无效值(compiler 曾恒发 restricted)。
434
+ // 0.5.66 起不再被 compiler 消费;保留在 allowed 以免存量文件 fail-loud
435
+ // (§3.1 迁移时顺手删除,0.6.0 可收紧)。
436
+ "permission_mode",
410
437
  // 0.4.x provider effort MVP step 3: per-agent effort override (resolved at compile).
411
438
  "effort",
412
439
  ];
@@ -481,6 +508,15 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
481
508
  }
482
509
  }
483
510
 
511
+ /// 汇总错误里引用 agent 的可读标签:`<id>` 优先,退回 `<index> (role: <role>)`。
512
+ fn agent_label(agent: &Yaml, idx: usize) -> String {
513
+ agent
514
+ .get("id")
515
+ .and_then(Yaml::as_str)
516
+ .map(|id| format!("`{id}`"))
517
+ .unwrap_or_else(|| format!("agents[{idx}]"))
518
+ }
519
+
484
520
  fn check_routing(routing: Option<&Yaml>, errors: &mut Vec<String>) {
485
521
  check_keys_y(
486
522
  routing,
@@ -564,7 +600,6 @@ fn check_runtime(runtime: Option<&Yaml>, errors: &mut Vec<String>) {
564
600
  "max_active_agents",
565
601
  "startup_order",
566
602
  "display_backend",
567
- "dangerous_auto_approve",
568
603
  "auto_attach_leader",
569
604
  "fast",
570
605
  "tick_interval_sec",
@@ -588,9 +623,6 @@ fn check_runtime(runtime: Option<&Yaml>, errors: &mut Vec<String>) {
588
623
  errors.push("/runtime/display_backend: invalid display backend".to_string());
589
624
  }
590
625
  }
591
- if get("dangerous_auto_approve").is_some_and(|v| !matches!(v, Yaml::Bool(_))) {
592
- errors.push("/runtime/dangerous_auto_approve: must be a boolean".to_string());
593
- }
594
626
  if get("auto_trust_own_workspace").is_some_and(|v| !matches!(v, Yaml::Bool(_))) {
595
627
  errors.push("/runtime/auto_trust_own_workspace: must be a boolean".to_string());
596
628
  }
@@ -1020,6 +1052,58 @@ mod tests {
1020
1052
  );
1021
1053
  }
1022
1054
 
1055
+ // 0.5.66 bypass 单源 §4.1:缺 `dangerously_skip_permissions` 字段 → 错误消息
1056
+ // 必须列出每个缺字段的角色 id + 用户可读的必填说明。
1057
+ #[test]
1058
+ fn test_missing_dangerously_field_reports_role_list() {
1059
+ // 从 fixture 文本里删掉第一个 agent 的字段,模拟该角色缺字段。
1060
+ let text = include_str!("testdata/team.spec.yaml");
1061
+ let without_first = text.replacen(" dangerously_skip_permissions: false\n", "", 1);
1062
+ assert_ne!(text, without_first, "fixture must contain the field");
1063
+ let spec = yaml::loads(&without_first).unwrap();
1064
+ let errors = all_spec_errors(&spec);
1065
+ let missing: Vec<&String> = errors
1066
+ .iter()
1067
+ .filter(|m| m.starts_with("/agents/dangerously_skip_permissions: missing"))
1068
+ .collect();
1069
+ assert_eq!(missing.len(), 1, "exactly one missing-field error, got: {errors:#?}");
1070
+ assert!(
1071
+ missing[0].contains("`codex_implementer`"),
1072
+ "must name the role id, got: {}",
1073
+ missing[0]
1074
+ );
1075
+ assert!(
1076
+ missing[0].contains("This field must be declared explicitly; it controls whether the agent launches with permission prompts bypassed."),
1077
+ "must carry the user-facing required-field message, got: {}",
1078
+ missing[0]
1079
+ );
1080
+ }
1081
+
1082
+ // 0.5.66 bypass 单源 §4.1:非 bool 值 → fail-loud 类型错,同样带角色清单 + 硬串。
1083
+ #[test]
1084
+ fn test_invalid_dangerously_type_rejected() {
1085
+ let text = include_str!("testdata/team.spec.yaml");
1086
+ let bad = text.replacen(" dangerously_skip_permissions: false\n", " dangerously_skip_permissions: \"yes\"\n", 1);
1087
+ assert_ne!(text, bad, "fixture must contain the field");
1088
+ let spec = yaml::loads(&bad).unwrap();
1089
+ let errors = all_spec_errors(&spec);
1090
+ let type_errs: Vec<&String> = errors
1091
+ .iter()
1092
+ .filter(|m| m.starts_with("/agents/dangerously_skip_permissions: must be a boolean"))
1093
+ .collect();
1094
+ assert_eq!(type_errs.len(), 1, "exactly one type error, got: {errors:#?}");
1095
+ assert!(
1096
+ type_errs[0].contains("`codex_implementer`"),
1097
+ "must name the role id, got: {}",
1098
+ type_errs[0]
1099
+ );
1100
+ assert!(
1101
+ type_errs[0].contains("This field must be declared explicitly; it controls whether the agent launches with permission prompts bypassed."),
1102
+ "must carry the user-facing required-field message, got: {}",
1103
+ type_errs[0]
1104
+ );
1105
+ }
1106
+
1023
1107
  #[test]
1024
1108
  fn empty_spec_matches_python_golden() {
1025
1109
  let spec = yaml::loads("{}").unwrap();
@@ -1050,4 +1134,43 @@ mod tests {
1050
1134
  ]
1051
1135
  );
1052
1136
  }
1137
+
1138
+ // 0.5.66 bypass 单源 §4.1:7 触发面共用同一 spec 校验层——compile_team 是它们的共同
1139
+ // 必经点(quick-start/restart/add/fork 都调用),缺字段一律 fail-loud。此处验证
1140
+ // compile 级校验确实 fail-loud,即各触发面(经 compile_team)都被覆盖。
1141
+ #[test]
1142
+ fn test_all_7_triggers_run_field_check() {
1143
+ // compile_team 是 7 触发面的 spec 校验必经点:
1144
+ // quick-start(quick_start.rs:233 compile_team)
1145
+ // restart(rebuild.rs:3245 rebuild_runtime_spec_from_roles → compile_team)
1146
+ // add-agent(add_agent.rs:331 compile_role_agent)
1147
+ // clone-agent(clone_agent.rs:62 → add_agent)
1148
+ // fork-agent(fork_agent.rs:96 validate_spec)
1149
+ // start-agent / reset-agent(操作已编译 spec,团队创建时经 compile_team 校验)
1150
+ // 验证 compile_team 对缺字段的角色文档 fail-loud。
1151
+ let team = std::env::temp_dir().join(format!(
1152
+ "ta-spec-7trigger-{}",
1153
+ std::process::id()
1154
+ ));
1155
+ let _ = std::fs::remove_dir_all(&team);
1156
+ std::fs::create_dir_all(team.join("agents")).unwrap();
1157
+ std::fs::write(
1158
+ team.join("TEAM.md"),
1159
+ "---\nname: t7\nobjective: 7 trigger.\nprovider: codex\n---\n\nteam.\n",
1160
+ )
1161
+ .unwrap();
1162
+ // 角色文档故意缺 dangerously_skip_permissions
1163
+ std::fs::write(
1164
+ team.join("agents").join("w.md"),
1165
+ "---\nname: w\nrole: Worker\nprovider: codex\ntools:\n - mcp_team\n---\n\nw.\n",
1166
+ )
1167
+ .unwrap();
1168
+ let err = crate::compiler::compile_team(&team).unwrap_err().to_string();
1169
+ assert!(
1170
+ err.contains("missing front matter field dangerously_skip_permissions")
1171
+ && err.contains("This field must be declared explicitly; it controls whether the agent launches with permission prompts bypassed."),
1172
+ "compile_team must fail-loud on missing field (shared by all 7 triggers); got {err}"
1173
+ );
1174
+ let _ = std::fs::remove_dir_all(&team);
1175
+ }
1053
1176
  }
@@ -39,6 +39,7 @@ agents:
39
39
  - "provider_builtin"
40
40
  - "banana"
41
41
  permission_mode: "restricted"
42
+ dangerously_skip_permissions: false
42
43
  preferred_for:
43
44
  - "implementation"
44
45
  - "bug_fix"
@@ -69,6 +70,7 @@ agents:
69
70
  - "mcp_team"
70
71
  - "provider_builtin"
71
72
  permission_mode: "restricted"
73
+ dangerously_skip_permissions: false
72
74
  preferred_for:
73
75
  - "research"
74
76
  - "architecture"
@@ -99,6 +101,7 @@ agents:
99
101
  - "mcp_team"
100
102
  - "provider_builtin"
101
103
  permission_mode: "restricted"
104
+ dangerously_skip_permissions: false
102
105
  preferred_for:
103
106
  - "review"
104
107
  - "risk_check"
@@ -141,7 +144,6 @@ runtime:
141
144
  session_name: "teamspec-full-example"
142
145
  auto_launch: true
143
146
  require_user_approval_before_launch: true
144
- dangerous_auto_approve: false
145
147
  max_active_agents: 3
146
148
  startup_order:
147
149
  - "codex_implementer"
@@ -38,6 +38,7 @@ agents:
38
38
  - "mcp_team"
39
39
  - "provider_builtin"
40
40
  permission_mode: "restricted"
41
+ dangerously_skip_permissions: false
41
42
  preferred_for:
42
43
  - "implementation"
43
44
  - "bug_fix"
@@ -68,6 +69,7 @@ agents:
68
69
  - "mcp_team"
69
70
  - "provider_builtin"
70
71
  permission_mode: "restricted"
72
+ dangerously_skip_permissions: false
71
73
  preferred_for:
72
74
  - "research"
73
75
  - "architecture"
@@ -98,6 +100,7 @@ agents:
98
100
  - "mcp_team"
99
101
  - "provider_builtin"
100
102
  permission_mode: "restricted"
103
+ dangerously_skip_permissions: false
101
104
  preferred_for:
102
105
  - "review"
103
106
  - "risk_check"
@@ -140,7 +143,6 @@ runtime:
140
143
  session_name: "teamspec-full-example"
141
144
  auto_launch: true
142
145
  require_user_approval_before_launch: true
143
- dangerous_auto_approve: false
144
146
  max_active_agents: 3
145
147
  startup_order:
146
148
  - "codex_implementer"
@@ -38,6 +38,7 @@ agents:
38
38
  - mcp_team
39
39
  - provider_builtin
40
40
  permission_mode: restricted
41
+ dangerously_skip_permissions: false
41
42
  preferred_for:
42
43
  - implementation
43
44
  - bug_fix
@@ -68,6 +69,7 @@ agents:
68
69
  - mcp_team
69
70
  - provider_builtin
70
71
  permission_mode: restricted
72
+ dangerously_skip_permissions: false
71
73
  preferred_for:
72
74
  - research
73
75
  - architecture
@@ -98,6 +100,7 @@ agents:
98
100
  - mcp_team
99
101
  - provider_builtin
100
102
  permission_mode: restricted
103
+ dangerously_skip_permissions: false
101
104
  preferred_for:
102
105
  - review
103
106
  - risk_check
@@ -140,7 +143,6 @@ runtime:
140
143
  session_name: teamspec-full-example
141
144
  auto_launch: true
142
145
  require_user_approval_before_launch: true
143
- dangerous_auto_approve: false
144
146
  max_active_agents: 3
145
147
  startup_order:
146
148
  - codex_implementer
@@ -8,7 +8,7 @@
8
8
  //! resolver (`claude_context_model`) stay in `adapter.rs`; capture scanning
9
9
  //! helpers now live under `provider/session_scan/claude.rs`.
10
10
 
11
- use crate::model::enums::AuthMode;
11
+ use crate::model::enums::{AuthMode, Provider};
12
12
  use crate::provider::adapter::{next_session_token, prompt_needs_native_mcp, BasicProviderAdapter};
13
13
  use crate::provider::{McpConfig, ProviderAdapter, ProviderError};
14
14
 
@@ -49,7 +49,10 @@ pub(crate) fn claude_base_command(
49
49
  ) -> Result<Vec<String>, ProviderError> {
50
50
  let mut argv = vec!["claude".to_string()];
51
51
  if claude_dangerous_auto_approve(tools) {
52
- argv.push("--dangerously-skip-permissions".to_string());
52
+ // 0.5.66 bypass 单源:flag 由 provider_bypass_flag 表供给(单源)
53
+ let flag = crate::provider::bypass_flags::provider_bypass_flag(Provider::Claude)
54
+ .expect("claude provider must define a bypass flag");
55
+ argv.push(flag.to_string());
53
56
  } else {
54
57
  argv.push("--permission-mode".to_string());
55
58
  argv.push("default".to_string());
@@ -6,7 +6,7 @@
6
6
  //! `json_inline` stays in `adapter.rs` because it is also used by other
7
7
  //! sites; this file reaches it via `super::*`.
8
8
 
9
- use crate::model::enums::AuthMode;
9
+ use crate::model::enums::{AuthMode, Provider};
10
10
  use crate::provider::adapter::json_inline;
11
11
  use crate::provider::{McpConfig, ProviderCommandOverrides};
12
12
 
@@ -41,7 +41,10 @@ pub(crate) fn codex_base_command(
41
41
  argv.push(profile.to_string());
42
42
  }
43
43
  if codex_dangerous_auto_approve(tools) {
44
- argv.push("--dangerously-bypass-approvals-and-sandbox".to_string());
44
+ // 0.5.66 bypass 单源:flag 由 provider_bypass_flag 表供给。
45
+ let flag = crate::provider::bypass_flags::provider_bypass_flag(Provider::Codex)
46
+ .expect("codex provider must define a bypass flag");
47
+ argv.push(flag.to_string());
45
48
  } else {
46
49
  argv.push("--sandbox".to_string());
47
50
  argv.push(codex_sandbox_mode(tools).to_string());
@@ -7,7 +7,7 @@
7
7
  //! translation. Auth hint (`copilot_auth_hint`) stays in `adapter.rs`;
8
8
  //! session-store scanning lives under `provider/session_scan/copilot.rs`.
9
9
 
10
- use crate::model::enums::AuthMode;
10
+ use crate::model::enums::{AuthMode, Provider};
11
11
  use crate::provider::McpConfig;
12
12
 
13
13
  pub(crate) fn copilot_base_command(
@@ -29,7 +29,10 @@ pub(crate) fn copilot_base_command(
29
29
  "--disable-builtin-mcps".to_string(),
30
30
  ];
31
31
  if copilot_dangerous_auto_approve(tools) {
32
- argv.push("--allow-all".to_string());
32
+ // 0.5.66 bypass 单源:flag 由 provider_bypass_flag 表供给。
33
+ let flag = crate::provider::bypass_flags::provider_bypass_flag(Provider::Copilot)
34
+ .expect("copilot provider must define a bypass flag");
35
+ argv.push(flag.to_string());
33
36
  } else {
34
37
  for flag in copilot_permission_flags(tools) {
35
38
  argv.push(flag);
@@ -0,0 +1,80 @@
1
+ //!
2
+ //! 0.5.66 bypass 单源统一 — provider → bypass argv flag 配置表。
3
+ //!
4
+ //! **唯一权威**:角色 md 的 `dangerously_skip_permissions: true` 决定是否加 bypass flag;
5
+ //! 本文件只回答"某个 provider 的 bypass 命令行参数是什么"。
6
+ //!
7
+ //! 数据源自旧 `lifecycle/launch/approval.rs::dangerous_leader_flags()` 表迁来:
8
+ //! - claude/claude_code → `--dangerously-skip-permissions`
9
+ //! - codex → `--dangerously-bypass-approvals-and-sandbox`
10
+ //! - copilot → `--allow-all`(0.3.27 P1 E54 symptom 2 起)
11
+ //!
12
+ //! 未定义(返回 None)的 provider:调用侧必须 fail-loud(见
13
+ //! `resolved_tool_strings_for_command`),不得静默 fallback。
14
+
15
+ use crate::model::enums::Provider;
16
+
17
+ /// 某 provider 的 bypass argv flag;未定义 → `None`。
18
+ pub(crate) fn provider_bypass_flag(provider: Provider) -> Option<&'static str> {
19
+ match provider {
20
+ Provider::Claude | Provider::ClaudeCode => Some("--dangerously-skip-permissions"),
21
+ Provider::Codex => Some("--dangerously-bypass-approvals-and-sandbox"),
22
+ Provider::Copilot => Some("--allow-all"),
23
+ // TODO: 查 gemini_cli 的等价 bypass 参数;未定义前 fail-loud。
24
+ Provider::GeminiCli => None,
25
+ Provider::Fake => None,
26
+ }
27
+ }
28
+
29
+ /// 0.5.66 §3.2 跨 workspace 兼容警示用:扫 argv tokens 找已知 bypass flag。
30
+ /// **只做检测,不做行为决策**(0.6.0 删)。
31
+ ///
32
+ /// TODO(0.6.0): remove
33
+ pub(crate) fn detect_bypass_flag_in_argv(argv_tokens: &[String]) -> Option<&'static str> {
34
+ for token in argv_tokens {
35
+ if token == "--dangerously-skip-permissions"
36
+ || token == "--dangerously-skip-permission"
37
+ {
38
+ return Some("--dangerously-skip-permissions");
39
+ }
40
+ if token == "--dangerously-bypass-approvals-and-sandbox" {
41
+ return Some("--dangerously-bypass-approvals-and-sandbox");
42
+ }
43
+ if token == "--allow-all" || token == "--yolo" {
44
+ return Some("--allow-all");
45
+ }
46
+ }
47
+ None
48
+ }
49
+
50
+ #[cfg(test)]
51
+ mod tests {
52
+ use super::*;
53
+
54
+ #[test]
55
+ fn claude_family_bypass_flag() {
56
+ assert_eq!(
57
+ provider_bypass_flag(Provider::Claude),
58
+ Some("--dangerously-skip-permissions")
59
+ );
60
+ assert_eq!(
61
+ provider_bypass_flag(Provider::ClaudeCode),
62
+ Some("--dangerously-skip-permissions")
63
+ );
64
+ }
65
+
66
+ #[test]
67
+ fn codex_and_copilot_bypass_flags() {
68
+ assert_eq!(
69
+ provider_bypass_flag(Provider::Codex),
70
+ Some("--dangerously-bypass-approvals-and-sandbox")
71
+ );
72
+ assert_eq!(provider_bypass_flag(Provider::Copilot), Some("--allow-all"));
73
+ }
74
+
75
+ #[test]
76
+ fn undefined_providers_return_none() {
77
+ assert_eq!(provider_bypass_flag(Provider::GeminiCli), None);
78
+ assert_eq!(provider_bypass_flag(Provider::Fake), None);
79
+ }
80
+ }
@@ -32,6 +32,8 @@
32
32
  // 复用 model::enums 的 Provider/AuthMode,并 re-export(下游惯于 `provider::Provider` 取用)。
33
33
  pub use crate::model::enums::{AuthMode, Provider};
34
34
 
35
+ /// 0.5.66 bypass 单源:provider → bypass argv flag 配置表(唯一权威)。
36
+ pub(crate) mod bypass_flags;
35
37
  pub mod adapter;
36
38
  /// 0.4.x decoupling step 2: per-provider command builders + permission/auth
37
39
  /// helpers split out of `adapter.rs`. Pure extraction — behavior unchanged.