@team-agent/installer 0.5.61 → 0.5.63

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 (250) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -2
  4. package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +1 -0
  6. package/crates/team-agent/src/cli/emit.rs +41 -7
  7. package/crates/team-agent/src/cli/helpers.rs +1 -0
  8. package/crates/team-agent/src/cli/leader.rs +6 -1
  9. package/crates/team-agent/src/cli/leaders.rs +2 -0
  10. package/crates/team-agent/src/cli/mod.rs +33 -3
  11. package/crates/team-agent/src/cli/named_address.rs +8 -1
  12. package/crates/team-agent/src/cli/profile.rs +2 -1
  13. package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
  14. package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
  15. package/crates/team-agent/src/cli/send/persist.rs +1 -0
  16. package/crates/team-agent/src/cli/send/presentation.rs +1 -0
  17. package/crates/team-agent/src/cli/send/resolve.rs +206 -1
  18. package/crates/team-agent/src/cli/send.rs +2 -1
  19. package/crates/team-agent/src/cli/spec.rs +1 -0
  20. package/crates/team-agent/src/cli/status.rs +2 -1
  21. package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
  22. package/crates/team-agent/src/cli/status_port/format.rs +1 -0
  23. package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
  24. package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
  25. package/crates/team-agent/src/cli/types.rs +3 -2
  26. package/crates/team-agent/src/communication_mode/mod.rs +1 -0
  27. package/crates/team-agent/src/compiler.rs +1 -0
  28. package/crates/team-agent/src/conpty/backend.rs +1 -0
  29. package/crates/team-agent/src/conpty/mod.rs +1 -0
  30. package/crates/team-agent/src/coordinator/backoff.rs +5 -8
  31. package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
  32. package/crates/team-agent/src/coordinator/health.rs +1 -0
  33. package/crates/team-agent/src/coordinator/mod.rs +2 -2
  34. package/crates/team-agent/src/coordinator/orphan.rs +4 -116
  35. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  36. package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
  37. package/crates/team-agent/src/coordinator/steps/abnormal.rs +15 -866
  38. package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
  39. package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
  40. package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
  41. package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
  42. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
  43. package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
  44. package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
  45. package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +139 -375
  46. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
  47. package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -49
  49. package/crates/team-agent/src/coordinator/tick.rs +23 -40
  50. package/crates/team-agent/src/coordinator/types.rs +3 -176
  51. package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
  52. package/crates/team-agent/src/db/message_store.rs +2 -1
  53. package/crates/team-agent/src/db/migration.rs +5 -4
  54. package/crates/team-agent/src/db/mod.rs +1 -0
  55. package/crates/team-agent/src/db/schema.rs +1 -0
  56. package/crates/team-agent/src/diagnose/comms.rs +2 -1
  57. package/crates/team-agent/src/diagnose/mod.rs +1 -0
  58. package/crates/team-agent/src/diagnose/orphans.rs +1 -0
  59. package/crates/team-agent/src/layout/manager.rs +3 -7
  60. package/crates/team-agent/src/layout/mod.rs +2 -2
  61. package/crates/team-agent/src/layout/overlay.rs +6 -1
  62. package/crates/team-agent/src/layout/placement.rs +1 -0
  63. package/crates/team-agent/src/layout/recovery.rs +3 -0
  64. package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
  65. package/crates/team-agent/src/layout/sessions.rs +8 -1
  66. package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
  67. package/crates/team-agent/src/layout/worker_env.rs +3 -0
  68. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  69. package/crates/team-agent/src/leader/helpers.rs +1 -0
  70. package/crates/team-agent/src/leader/incident.rs +1 -0
  71. package/crates/team-agent/src/leader/lease.rs +27 -22
  72. package/crates/team-agent/src/leader/mod.rs +2 -3
  73. package/crates/team-agent/src/leader/owner_bind.rs +13 -3
  74. package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
  75. package/crates/team-agent/src/leader/rediscover.rs +4 -17
  76. package/crates/team-agent/src/leader/registry.rs +3 -15
  77. package/crates/team-agent/src/leader/start.rs +373 -125
  78. package/crates/team-agent/src/leader/takeover.rs +10 -232
  79. package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
  80. package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
  81. package/crates/team-agent/src/leader/tests/identity.rs +7 -2
  82. package/crates/team-agent/src/leader/tests/idle.rs +0 -193
  83. package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
  84. package/crates/team-agent/src/leader/tests/mod.rs +4 -41
  85. package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
  86. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
  87. package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
  88. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
  89. package/crates/team-agent/src/leader/types.rs +1 -28
  90. package/crates/team-agent/src/lib.rs +3 -0
  91. package/crates/team-agent/src/lifecycle/display.rs +7 -1
  92. package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
  93. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
  95. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
  96. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
  97. package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
  99. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
  100. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -27
  101. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
  102. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
  103. package/crates/team-agent/src/lifecycle/launch.rs +11 -12
  104. package/crates/team-agent/src/lifecycle/mod.rs +2 -1
  105. package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -66
  106. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
  107. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
  108. package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
  109. package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
  110. package/crates/team-agent/src/lifecycle/restart.rs +13 -15
  111. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
  112. package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
  113. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
  114. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
  115. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
  116. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
  117. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
  118. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
  119. package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
  120. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
  121. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
  122. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
  123. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
  124. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
  125. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
  126. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
  127. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
  128. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
  129. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
  130. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
  131. package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
  132. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
  133. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
  134. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
  135. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
  136. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
  137. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
  138. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
  139. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
  140. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
  141. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
  142. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
  143. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
  144. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
  145. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
  146. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
  147. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
  148. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
  149. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
  150. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
  151. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
  152. package/crates/team-agent/src/lifecycle/tests.rs +59 -0
  153. package/crates/team-agent/src/lifecycle/types.rs +0 -9
  154. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  155. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
  156. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
  157. package/crates/team-agent/src/mcp_server/mod.rs +1 -0
  158. package/crates/team-agent/src/mcp_server/normalize.rs +4 -3
  159. package/crates/team-agent/src/mcp_server/tools.rs +1 -0
  160. package/crates/team-agent/src/mcp_server/types.rs +1 -18
  161. package/crates/team-agent/src/mcp_server/wire.rs +5 -5
  162. package/crates/team-agent/src/messaging/activity.rs +1 -0
  163. package/crates/team-agent/src/messaging/address.rs +1 -0
  164. package/crates/team-agent/src/messaging/delivery.rs +4 -3
  165. package/crates/team-agent/src/messaging/helpers.rs +2 -1
  166. package/crates/team-agent/src/messaging/leader_channel.rs +1 -0
  167. package/crates/team-agent/src/messaging/leader_receiver.rs +2 -1
  168. package/crates/team-agent/src/messaging/mod.rs +10 -14
  169. package/crates/team-agent/src/messaging/peers.rs +2 -0
  170. package/crates/team-agent/src/messaging/persist.rs +3 -1
  171. package/crates/team-agent/src/messaging/presentation.rs +2 -1
  172. package/crates/team-agent/src/messaging/results.rs +1 -0
  173. package/crates/team-agent/src/messaging/scheduler.rs +1 -0
  174. package/crates/team-agent/src/messaging/selftest.rs +1 -1
  175. package/crates/team-agent/src/messaging/send.rs +41 -8
  176. package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
  177. package/crates/team-agent/src/messaging/types.rs +2 -2
  178. package/crates/team-agent/src/messaging/watchers.rs +19 -13
  179. package/crates/team-agent/src/model/enums.rs +1 -0
  180. package/crates/team-agent/src/model/errors.rs +1 -0
  181. package/crates/team-agent/src/model/ids.rs +1 -0
  182. package/crates/team-agent/src/model/mod.rs +1 -0
  183. package/crates/team-agent/src/model/name_similarity.rs +1 -0
  184. package/crates/team-agent/src/model/pane_authority_refusal.rs +1 -0
  185. package/crates/team-agent/src/model/paths.rs +1 -0
  186. package/crates/team-agent/src/model/permissions.rs +3 -2
  187. package/crates/team-agent/src/model/routing.rs +1 -0
  188. package/crates/team-agent/src/model/spec.rs +1 -0
  189. package/crates/team-agent/src/model/task_graph.rs +1 -0
  190. package/crates/team-agent/src/model/yaml/tests.rs +1 -0
  191. package/crates/team-agent/src/model/yaml.rs +1 -0
  192. package/crates/team-agent/src/packaging/install.rs +9 -147
  193. package/crates/team-agent/src/packaging/migrate.rs +2 -0
  194. package/crates/team-agent/src/packaging/mod.rs +1 -0
  195. package/crates/team-agent/src/packaging/repair.rs +2 -0
  196. package/crates/team-agent/src/packaging/tests.rs +29 -218
  197. package/crates/team-agent/src/packaging/types.rs +8 -15
  198. package/crates/team-agent/src/platform/argv.rs +4 -0
  199. package/crates/team-agent/src/platform/errors.rs +10 -1
  200. package/crates/team-agent/src/platform/file_lock.rs +4 -157
  201. package/crates/team-agent/src/platform/mod.rs +0 -61
  202. package/crates/team-agent/src/platform/process.rs +1 -0
  203. package/crates/team-agent/src/provider/adapter.rs +1 -0
  204. package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
  205. package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
  206. package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
  207. package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
  208. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
  209. package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
  210. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  211. package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
  212. package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
  213. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
  214. package/crates/team-agent/src/provider/classify.rs +1 -0
  215. package/crates/team-agent/src/provider/faults.rs +1 -26
  216. package/crates/team-agent/src/provider/helpers.rs +1 -0
  217. package/crates/team-agent/src/provider/mod.rs +1 -1
  218. package/crates/team-agent/src/provider/session/capture.rs +1 -4
  219. package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
  220. package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
  221. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
  222. package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
  223. package/crates/team-agent/src/provider/session/mod.rs +2 -4
  224. package/crates/team-agent/src/provider/session/resume.rs +2 -209
  225. package/crates/team-agent/src/provider/session_scan/claude.rs +1 -0
  226. package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
  227. package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
  228. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  229. package/crates/team-agent/src/provider/session_scan.rs +1 -42
  230. package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
  231. package/crates/team-agent/src/provider/types.rs +1 -0
  232. package/crates/team-agent/src/provider/wire.rs +1 -0
  233. package/crates/team-agent/src/state/identity.rs +16 -5
  234. package/crates/team-agent/src/state/identity_keys.rs +1 -0
  235. package/crates/team-agent/src/state/mod.rs +2 -0
  236. package/crates/team-agent/src/state/owner_gate.rs +5 -0
  237. package/crates/team-agent/src/state/ownership.rs +10 -4
  238. package/crates/team-agent/src/state/paths.rs +12 -0
  239. package/crates/team-agent/src/state/persist.rs +9 -3
  240. package/crates/team-agent/src/state/projection.rs +15 -2
  241. package/crates/team-agent/src/state/repository/intent.rs +1 -0
  242. package/crates/team-agent/src/state/repository/tests.rs +1 -0
  243. package/crates/team-agent/src/state/repository.rs +7 -13
  244. package/crates/team-agent/src/state/selector.rs +1 -0
  245. package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
  246. package/crates/team-agent/src/tmux_backend.rs +63 -0
  247. package/crates/team-agent/src/transport.rs +15 -0
  248. package/package.json +4 -4
  249. package/crates/team-agent/src/leader/inject.rs +0 -33
  250. package/crates/team-agent/src/provider/command.rs +0 -80
@@ -0,0 +1,1187 @@
1
+ //! #232 Codex MCP approval inheritance command-shape contracts.
2
+ //!
3
+ //! User-facing invariant: a worker Codex process must mirror the leader's effective approval mode,
4
+ //! but never silently exceed it. These are cargo-only command-shape contracts; no subscription or
5
+ //! real Codex process is started.
6
+
7
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
8
+
9
+ #[path = "../../../tests/support/hermetic.rs"]
10
+ mod hermetic_guard;
11
+ #[allow(dead_code)]
12
+ fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
13
+
14
+ #[path = "../../../tests/support/composite_source.rs"]
15
+ mod composite_source;
16
+ use std::collections::BTreeMap;
17
+ use std::path::{Path, PathBuf};
18
+ use std::sync::atomic::{AtomicU64, Ordering};
19
+ use std::sync::Mutex;
20
+
21
+ use serde_json::json;
22
+ use serial_test::serial;
23
+ use team_agent::lifecycle::{
24
+ add_agent_with_transport, fork_agent_with_transport, launch_with_transport,
25
+ reset_agent_with_transport, restart_with_transport, start_agent_with_transport,
26
+ };
27
+ use team_agent::lifecycle::{
28
+ DangerousApprovalSource, LaunchReport, LifecycleError, StartAgentOutcome, StartMode,
29
+ };
30
+ use team_agent::model::ids::AgentId;
31
+ use team_agent::transport::{
32
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
33
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, SessionName, SetEnvOutcome,
34
+ SpawnResult, SubmitVerification, Target, Transport, TransportError, TurnVerification,
35
+ WindowName,
36
+ };
37
+
38
+ const CODEX_BYPASS: &str = "--dangerously-bypass-approvals-and-sandbox";
39
+ const ANCESTRY_ENV: &str = "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON";
40
+ const APPROVAL_ENV_KEYS: &[&str] = &[
41
+ "TEAM_AGENT_LEADER_BYPASS",
42
+ "TEAM_AGENT_LEADER_BYPASS_SOURCE",
43
+ "TEAM_AGENT_LEADER_BYPASS_PROVIDER",
44
+ "TEAM_AGENT_LEADER_BYPASS_FLAG",
45
+ "TEAM_AGENT_MCP_AUTO_APPROVE",
46
+ "TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE",
47
+ ];
48
+
49
+ #[test]
50
+ #[serial(env)]
51
+ fn codex_worker_inherits_leader_bypass_flag_on_fresh_launch() {
52
+ let _ancestry = MockAncestry::codex_bypass();
53
+ let team = compiled_team_dir("fresh-inherited", false, &[("worker_a", "Worker A")]);
54
+ let transport = RecordingTransport::new();
55
+
56
+ let report = launch_with_transport(&team.join("team.spec.yaml"), false, true, true, &transport)
57
+ .expect("fresh launch should spawn worker under recording transport");
58
+ let spawn = transport.only_spawn("fresh launch");
59
+
60
+ assert_codex_bypass(&spawn.argv, "fresh launch inherited leader Codex bypass");
61
+ assert_bypass_env(&spawn, "fresh launch inherited leader Codex bypass");
62
+ assert_eq!(
63
+ report.safety.source,
64
+ DangerousApprovalSource::LeaderProcess,
65
+ "AI-1: dry/launch safety source must be leader_process when inherited from Codex ancestry"
66
+ );
67
+ assert!(
68
+ report.safety.inherited,
69
+ "AI-1: inherited approval must be marked inherited=true"
70
+ );
71
+ assert_eq!(report.safety.provider.as_deref(), Some("codex"));
72
+ assert_eq!(report.safety.flag.as_deref(), Some(CODEX_BYPASS));
73
+ }
74
+
75
+ #[test]
76
+ #[serial(env)]
77
+ fn codex_worker_restricted_when_leader_restricted_reverse_core() {
78
+ let _ancestry = MockAncestry::restricted();
79
+ let team = compiled_team_dir("fresh-restricted", false, &[("worker_a", "Worker A")]);
80
+ let transport = RecordingTransport::new();
81
+
82
+ launch_with_transport(&team.join("team.spec.yaml"), false, true, true, &transport)
83
+ .expect("restricted fresh launch should still spawn worker");
84
+ let argv = transport.only_spawn_argv("restricted fresh launch");
85
+
86
+ assert_codex_restricted(&argv, "AI-3: leader restricted -> worker restricted");
87
+ }
88
+
89
+ #[test]
90
+ #[serial(env)]
91
+ fn runtime_config_dangerous_requires_yes_and_then_applies_with_audit() {
92
+ let _ancestry = MockAncestry::restricted();
93
+ let team = compiled_team_dir("runtime-dangerous", true, &[("worker_a", "Worker A")]);
94
+ let spec = team.join("team.spec.yaml");
95
+
96
+ let dry_run = launch_with_transport(&spec, true, false, true, &RecordingTransport::new())
97
+ .expect("dry-run should expose explicit runtime config safety");
98
+ assert_eq!(
99
+ dry_run.safety.source,
100
+ DangerousApprovalSource::RuntimeConfig
101
+ );
102
+ assert!(!dry_run.safety.inherited);
103
+
104
+ let without_yes = launch_with_transport(&spec, false, false, true, &RecordingTransport::new());
105
+ assert!(
106
+ without_yes.is_err(),
107
+ "AI-9: explicit dangerous_auto_approve not inherited from leader must require --yes consent"
108
+ );
109
+
110
+ let transport = RecordingTransport::new();
111
+ let with_yes = launch_with_transport(&spec, false, true, true, &transport)
112
+ .expect("explicit runtime dangerous with --yes should launch");
113
+ assert!(
114
+ launch_safety_json(&with_yes).contains("worker_capability_above_leader"),
115
+ "AI-6: explicit RuntimeConfig override above restricted leader must expose worker_capability_above_leader audit field; safety={:?}",
116
+ with_yes.safety
117
+ );
118
+ assert_codex_bypass(
119
+ &transport.only_spawn_argv("runtime config --yes"),
120
+ "runtime config + --yes",
121
+ );
122
+ }
123
+
124
+ #[test]
125
+ #[serial(env)]
126
+ fn inherited_bypass_non_dry_run_does_not_require_yes_but_explicit_raise_does() {
127
+ let _inherited = MockAncestry::codex_bypass();
128
+ let inherited_team = compiled_team_dir("inherited-no-yes", false, &[("worker_a", "Worker A")]);
129
+ let inherited_transport = RecordingTransport::new();
130
+
131
+ let inherited = launch_with_transport(
132
+ &inherited_team.join("team.spec.yaml"),
133
+ false,
134
+ false,
135
+ true,
136
+ &inherited_transport,
137
+ )
138
+ .expect("AI-9: inherited leader-process bypass is mirrored consent and must not require --yes");
139
+ assert_eq!(
140
+ inherited.safety.source,
141
+ DangerousApprovalSource::LeaderProcess,
142
+ "AI-9 precondition: inherited bypass must be sourced from leader process"
143
+ );
144
+ assert!(
145
+ inherited.safety.inherited,
146
+ "AI-9 precondition: inherited bypass must be marked inherited"
147
+ );
148
+ assert_codex_bypass(
149
+ &inherited_transport.only_spawn_argv("inherited bypass without --yes"),
150
+ "AI-9 inherited non-dry-run without --yes",
151
+ );
152
+
153
+ drop(_inherited);
154
+
155
+ let _restricted = MockAncestry::restricted();
156
+ let explicit_team = compiled_team_dir("explicit-no-yes", true, &[("worker_a", "Worker A")]);
157
+ let explicit = launch_with_transport(
158
+ &explicit_team.join("team.spec.yaml"),
159
+ false,
160
+ false,
161
+ true,
162
+ &RecordingTransport::new(),
163
+ );
164
+ assert!(
165
+ matches!(explicit, Err(LifecycleError::DangerousApprovalRequired(_))),
166
+ "AI-9: explicit dangerous_auto_approve above a restricted leader is capability elevation and must require --yes; got {explicit:?}"
167
+ );
168
+ }
169
+
170
+ #[test]
171
+ #[serial(env)]
172
+ fn inherited_bypass_is_preserved_across_restart_start_add_and_fork() {
173
+ let _home = hermetic_guard::HermeticTestEnv::enter("codex-approval-fork-inherited");
174
+ let _ancestry = MockAncestry::codex_bypass();
175
+
176
+ let restart_ws = runtime_workspace("restart-inherited", &[("worker_a", "Worker A")]);
177
+ seed_agent_state(&restart_ws, "worker_a", "Worker A", true);
178
+ let restart_transport = RecordingTransport::new().with_session_present(false);
179
+ let mut failures = Vec::new();
180
+
181
+ restart_with_transport(&restart_ws, false, None, &restart_transport)
182
+ .expect("restart fixture should spawn worker");
183
+ let restart_spawn = restart_transport.only_spawn("restart resumed");
184
+ failures.extend(codex_bypass_failures(&restart_spawn.argv, "AI-1 restart"));
185
+ assert_bypass_env(&restart_spawn, "AI-1 restart resumed");
186
+
187
+ let restart_fresh_ws = runtime_workspace("restart-fresh", &[("worker_a", "Worker A")]);
188
+ seed_agent_state(&restart_fresh_ws, "worker_a", "Worker A", false);
189
+ let restart_fresh_transport = RecordingTransport::new().with_session_present(false);
190
+ restart_with_transport(&restart_fresh_ws, true, None, &restart_fresh_transport)
191
+ .expect("fresh restart fixture should spawn worker");
192
+ assert_bypass_env(
193
+ &restart_fresh_transport.only_spawn("restart fresh"),
194
+ "AI-1 restart fresh",
195
+ );
196
+
197
+ let start_ws = runtime_workspace("start-inherited", &[("worker_a", "Worker A")]);
198
+ seed_agent_state(&start_ws, "worker_a", "Worker A", false);
199
+ let start_transport = RecordingTransport::new().with_session_present(false);
200
+ start_agent_with_transport(
201
+ &start_ws,
202
+ &AgentId::new("worker_a"),
203
+ true,
204
+ false,
205
+ true,
206
+ None,
207
+ &start_transport,
208
+ )
209
+ .expect("start-agent fixture should spawn worker");
210
+ let start_spawn = start_transport.only_spawn("start-agent fresh");
211
+ failures.extend(codex_bypass_failures(&start_spawn.argv, "AI-1 start-agent"));
212
+ assert_bypass_env(&start_spawn, "AI-1 start-agent fresh");
213
+
214
+ let missing_ws = runtime_workspace("start-missing", &[("worker_a", "Worker A")]);
215
+ seed_agent_state(&missing_ws, "worker_a", "Worker A", true);
216
+ std::fs::remove_file(missing_ws.join("worker_a-rollout.jsonl")).unwrap();
217
+ let missing_transport = RecordingTransport::new().with_session_present(false);
218
+ let missing_outcome = start_agent_with_transport(
219
+ &missing_ws,
220
+ &AgentId::new("worker_a"),
221
+ true,
222
+ false,
223
+ true,
224
+ None,
225
+ &missing_transport,
226
+ )
227
+ .expect("missing rollout with allow-fresh should spawn worker");
228
+ assert!(matches!(
229
+ missing_outcome,
230
+ StartAgentOutcome::Running {
231
+ start_mode: StartMode::FreshAfterMissingRollout,
232
+ ..
233
+ }
234
+ ));
235
+ assert_bypass_env(
236
+ &missing_transport.only_spawn("start-agent fresh after missing rollout"),
237
+ "AI-1 start-agent fresh after missing rollout",
238
+ );
239
+
240
+ let reset_team = team_dir_with_running_workspace("reset-inherited");
241
+ let reset_ws = team_workspace(&reset_team);
242
+ let reset_transport = RecordingTransport::new().with_session_present(false);
243
+ reset_agent_with_transport(
244
+ &reset_ws,
245
+ &AgentId::new("worker_a"),
246
+ true,
247
+ false,
248
+ None,
249
+ &reset_transport,
250
+ )
251
+ .expect("reset fixture should spawn worker");
252
+ assert_bypass_env(
253
+ &reset_transport.only_spawn("reset-agent fresh"),
254
+ "AI-1 reset-agent fresh",
255
+ );
256
+ assert_persisted_bypass_policy(&reset_ws, "worker_a", "AI-1 reset-agent fresh");
257
+
258
+ let add_team = team_dir_with_running_workspace("add-inherited");
259
+ let add_role = add_team.join("worker_b.md");
260
+ std::fs::write(&add_role, role_doc("worker_b", "Worker B")).unwrap();
261
+ let add_transport = RecordingTransport::new().with_session_present(true);
262
+ add_agent_with_transport(
263
+ &add_team,
264
+ &AgentId::new("worker_b"),
265
+ &add_role,
266
+ false,
267
+ None,
268
+ &add_transport,
269
+ )
270
+ .expect("add-agent fixture should spawn worker");
271
+ let add_spawn = add_transport.only_spawn("add-agent");
272
+ failures.extend(codex_bypass_failures(&add_spawn.argv, "AI-1 add-agent"));
273
+ assert_bypass_env(&add_spawn, "AI-1 add-agent");
274
+
275
+ let fork_team = team_dir_with_forkable_source("fork-inherited");
276
+ let fork_transport = RecordingTransport::new().with_session_present(true);
277
+ fork_agent_with_transport(
278
+ &fork_team,
279
+ &AgentId::new("worker_a"),
280
+ &AgentId::new("worker_fork"),
281
+ None,
282
+ false,
283
+ None,
284
+ &fork_transport,
285
+ )
286
+ .expect("fork fixture should spawn worker");
287
+ let fork_spawn = fork_transport.only_spawn("fork-agent");
288
+ failures.extend(codex_bypass_failures(&fork_spawn.argv, "AI-1 fork"));
289
+ assert_bypass_env(&fork_spawn, "AI-1 fork");
290
+
291
+ assert!(
292
+ failures.is_empty(),
293
+ "inherited bypass must be preserved across restart/start/add/fork:\n{}",
294
+ failures.join("\n")
295
+ );
296
+ }
297
+
298
+ #[test]
299
+ #[serial(env)]
300
+ fn add_agent_preserves_restricted_default_and_fork_preserves_restricted_origin() {
301
+ let _home = hermetic_guard::HermeticTestEnv::enter("codex-approval-fork-restricted");
302
+ let _ancestry = MockAncestry::restricted();
303
+
304
+ let add_team = team_dir_with_running_workspace("add-restricted");
305
+ let add_role = add_team.join("worker_b.md");
306
+ std::fs::write(&add_role, role_doc("worker_b", "Worker B")).unwrap();
307
+ let add_transport = RecordingTransport::new().with_session_present(true);
308
+ add_agent_with_transport(
309
+ &add_team,
310
+ &AgentId::new("worker_b"),
311
+ &add_role,
312
+ false,
313
+ None,
314
+ &add_transport,
315
+ )
316
+ .expect("add-agent restricted fixture should spawn worker");
317
+ assert_codex_restricted(
318
+ &add_transport.only_spawn_argv("restricted add-agent"),
319
+ "add-agent restricted default",
320
+ );
321
+
322
+ let fork_team = team_dir_with_forkable_source("fork-restricted");
323
+ let fork_transport = RecordingTransport::new().with_session_present(true);
324
+ fork_agent_with_transport(
325
+ &fork_team,
326
+ &AgentId::new("worker_a"),
327
+ &AgentId::new("worker_fork"),
328
+ None,
329
+ false,
330
+ None,
331
+ &fork_transport,
332
+ )
333
+ .expect("fork restricted fixture should spawn worker");
334
+ assert_codex_restricted(
335
+ &fork_transport.only_spawn_argv("restricted fork-agent"),
336
+ "fork preserves restricted origin",
337
+ );
338
+
339
+ let reset_team = team_dir_with_running_workspace("reset-restricted");
340
+ let reset_ws = team_workspace(&reset_team);
341
+ let reset_transport = RecordingTransport::new().with_session_present(false);
342
+ reset_agent_with_transport(
343
+ &reset_ws,
344
+ &AgentId::new("worker_a"),
345
+ true,
346
+ false,
347
+ None,
348
+ &reset_transport,
349
+ )
350
+ .expect("restricted reset fixture should spawn worker");
351
+ assert_restricted_env(
352
+ &reset_transport.only_spawn("restricted reset-agent"),
353
+ "reset-agent restricted default",
354
+ );
355
+ }
356
+
357
+ #[test]
358
+ #[serial(env)]
359
+ fn approval_detection_uses_token_boundaries_and_warns_unexpected_binary() {
360
+ let _substring =
361
+ MockAncestry::raw(&["codex", &format!("--comment=literal-{CODEX_BYPASS}-inside")]);
362
+ let detected = team_agent::lifecycle::detect_dangerous_approval()
363
+ .expect("detect dangerous approval with substring fixture");
364
+ assert!(
365
+ !detected.enabled,
366
+ "AI-4: unrelated substring must not trigger inheritance; detection must match argv tokens exactly, got {detected:?}"
367
+ );
368
+
369
+ let _unexpected = MockAncestry::raw(&["bash", CODEX_BYPASS]);
370
+ let detected = team_agent::lifecycle::detect_dangerous_approval()
371
+ .expect("detect dangerous approval with unexpected binary fixture");
372
+ assert!(
373
+ detected.enabled,
374
+ "AI-5: wrapper/alias parity still allows inheritance"
375
+ );
376
+ assert_eq!(detected.provider.as_deref(), Some("codex"));
377
+ assert!(
378
+ detected
379
+ .flag
380
+ .as_deref()
381
+ .is_some_and(|flag| flag == CODEX_BYPASS),
382
+ "AI-5: detected flag should be recorded"
383
+ );
384
+ assert!(
385
+ event_or_source_contains("dangerous_flag_in_unexpected_binary")
386
+ && event_or_source_contains("ancestry_binary_name"),
387
+ "AI-5: unexpected binary must emit dangerous_flag_in_unexpected_binary with ancestry_binary_name audit"
388
+ );
389
+ }
390
+
391
+ #[test]
392
+ #[serial(env)]
393
+ fn prompt_handler_does_not_raise_worker_above_leader() {
394
+ let provider_sources = source_tree("src/provider");
395
+ let coordinator_sources = source_tree("src/coordinator");
396
+ let all = format!("{provider_sources}\n{coordinator_sources}");
397
+ let mut failures = Vec::new();
398
+
399
+ if !all.contains("awaiting_human_confirm")
400
+ && !all.contains("runtime_approval.blocked_by_leader_safety")
401
+ && !all.contains("leader_restricted")
402
+ {
403
+ failures.push(
404
+ "AI-7: restricted approval prompts must be surfaced as awaiting_human_confirm / blocked_by_leader_safety / leader_restricted, not auto-consented"
405
+ .to_string(),
406
+ );
407
+ }
408
+ let runtime_uses_approval_choice = coordinator_sources
409
+ .contains("choose_internal_mcp_approval_choice")
410
+ || source("src/provider/mod.rs").contains("choose_internal_mcp_approval_choice");
411
+ if runtime_uses_approval_choice {
412
+ if !coordinator_sources.contains("runtime_approval_policy_from_agent") {
413
+ failures.push(
414
+ "AI-7/#232: coordinator auto-answer path must read persisted per-agent approval policy via runtime_approval_policy_from_agent, not re-detect leader process ancestry"
415
+ .to_string(),
416
+ );
417
+ }
418
+ if !coordinator_sources.contains("effective_approval_policy") {
419
+ failures.push(
420
+ "AI-7/#232: auto-answer gate must be based on state.agents[*].effective_approval_policy"
421
+ .to_string(),
422
+ );
423
+ }
424
+ if !coordinator_sources.contains("auto_answer_allowed") {
425
+ failures.push(
426
+ "AI-7/#232: persisted policy must expose an auto_answer_allowed gate before approval_choice_keys/send_keys can run"
427
+ .to_string(),
428
+ );
429
+ }
430
+ if coordinator_sources.contains("detect_dangerous_approval()") {
431
+ failures.push(
432
+ "AI-7/#232: coordinator approval handling must not call detect_dangerous_approval(); coordinator process ancestry is not the worker's approval source"
433
+ .to_string(),
434
+ );
435
+ }
436
+ }
437
+ if !provider_sources
438
+ .contains("ApprovalKind::Command => Some(\"command_approval_requires_human\")")
439
+ {
440
+ failures.push(
441
+ "AI-7/#232 C5: command approval must stay on the human-confirm path even when persisted policy allows MCP auto-approval"
442
+ .to_string(),
443
+ );
444
+ }
445
+ if all.contains("Automation is consent") {
446
+ failures.push(
447
+ "AI-2: automation is not consent; prompt handler must not encode the opposite policy"
448
+ .to_string(),
449
+ );
450
+ }
451
+
452
+ assert!(
453
+ failures.is_empty(),
454
+ "AI-7 prompt handler approval ceiling contract failed:\n{}",
455
+ failures.join("\n")
456
+ );
457
+ }
458
+
459
+ #[test]
460
+ #[serial(env)]
461
+ fn approval_inheritance_static_guards_cover_ai_1_through_ai_9() {
462
+ let launch = source("src/lifecycle/launch.rs");
463
+ let adapter = source("src/provider/adapter.rs");
464
+ let restart_common = source("src/lifecycle/restart/common.rs");
465
+ let mut failures = Vec::new();
466
+
467
+ if function_body(&launch, "detect_dangerous_approval")
468
+ .is_some_and(|body| body.contains(".contains(") && !body.contains("split('\\0')"))
469
+ {
470
+ failures.push("AI-4: detect_dangerous_approval must not use raw cmdline contains without argv token splitting".to_string());
471
+ }
472
+ if !launch.contains("split('\\0')")
473
+ && !launch.contains("argv_tokens")
474
+ && !launch.contains(ANCESTRY_ENV)
475
+ {
476
+ failures.push(
477
+ "AI-4: detect_dangerous_approval must expose argv-token parsing over NUL/argv tokens"
478
+ .to_string(),
479
+ );
480
+ }
481
+ if !launch.contains("dangerous_flag_in_unexpected_binary")
482
+ || !launch.contains("ancestry_binary_name")
483
+ {
484
+ failures.push("AI-5: dangerous flag in non-provider binary must emit warning audit with ancestry_binary_name".to_string());
485
+ }
486
+ if !launch.contains("worker_capability_above_leader") {
487
+ failures.push(
488
+ "AI-6: RuntimeConfig override audit must include worker_capability_above_leader"
489
+ .to_string(),
490
+ );
491
+ }
492
+ if !launch.contains("effective_runtime_config") && !launch.contains("effective_worker") {
493
+ failures.push("AI-8: worker spawn approval policy must be centralized in an effective runtime/config helper".to_string());
494
+ }
495
+ if !adapter.contains("dangerous_auto_approve") {
496
+ failures
497
+ .push("AI-1: Codex adapter must still have a bypass command-shape input".to_string());
498
+ }
499
+ for (surface, text) in [
500
+ ("fresh launch", launch.as_str()),
501
+ ("restart common", restart_common.as_str()),
502
+ ("fork", launch.as_str()),
503
+ ] {
504
+ if text.contains("agent_tool_strings(agent)")
505
+ && !text.contains("effective_runtime_config")
506
+ && !text.contains("worker_tool_refs")
507
+ {
508
+ failures.push(format!(
509
+ "AI-8: {surface} calls agent_tool_strings(agent) without centralized effective approval helper"
510
+ ));
511
+ }
512
+ }
513
+ let fork_body_without_comments = function_body(&launch, "fork_agent_with_transport")
514
+ .map(strip_line_comments)
515
+ .unwrap_or_default();
516
+ if !(fork_body_without_comments.contains(".fork_plan(")
517
+ || fork_body_without_comments.contains(".fork_with_context("))
518
+ {
519
+ failures.push("AI-1/AI-8: fork path must call fork_plan or fork_with_context in executable code, not bare fork or comment-only guard text".to_string());
520
+ }
521
+
522
+ assert!(
523
+ failures.is_empty(),
524
+ "MCP approval inheritance static guard failed:\n{}",
525
+ failures.join("\n")
526
+ );
527
+ }
528
+
529
+ fn assert_codex_bypass(argv: &[String], label: &str) {
530
+ let failures = codex_bypass_failures(argv, label);
531
+ assert!(failures.is_empty(), "{}", failures.join("\n"));
532
+ }
533
+
534
+ fn assert_bypass_env(spawn: &RecordedSpawn, label: &str) {
535
+ let expected = BTreeMap::from([
536
+ ("TEAM_AGENT_LEADER_BYPASS", "1"),
537
+ ("TEAM_AGENT_LEADER_BYPASS_SOURCE", "leader_process"),
538
+ ("TEAM_AGENT_LEADER_BYPASS_PROVIDER", "codex"),
539
+ ("TEAM_AGENT_LEADER_BYPASS_FLAG", CODEX_BYPASS),
540
+ ("TEAM_AGENT_MCP_AUTO_APPROVE", "team_orchestrator"),
541
+ ("TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE", "leader_bypass"),
542
+ ]);
543
+ for (key, value) in expected {
544
+ assert_eq!(
545
+ spawn.env.get(key).map(String::as_str),
546
+ Some(value),
547
+ "{label}: final worker env must carry {key}={value}; spawn={spawn:?}"
548
+ );
549
+ assert!(
550
+ !spawn.env_unset.iter().any(|unset| unset == key),
551
+ "{label}: final env_unset must not erase {key}; spawn={spawn:?}"
552
+ );
553
+ }
554
+ }
555
+
556
+ fn assert_restricted_env(spawn: &RecordedSpawn, label: &str) {
557
+ assert_eq!(
558
+ spawn
559
+ .env
560
+ .get("TEAM_AGENT_LEADER_BYPASS")
561
+ .map(String::as_str),
562
+ Some("0"),
563
+ "{label}: restricted worker must carry explicit bypass=0; spawn={spawn:?}"
564
+ );
565
+ for key in &APPROVAL_ENV_KEYS[1..] {
566
+ assert!(
567
+ !spawn.env.contains_key(*key),
568
+ "{label}: restricted worker must not carry {key}; spawn={spawn:?}"
569
+ );
570
+ }
571
+ }
572
+
573
+ fn assert_persisted_bypass_policy(workspace: &Path, agent_id: &str, label: &str) {
574
+ let state = team_agent::state::persist::load_runtime_state(workspace).unwrap();
575
+ let policy = &state["agents"][agent_id]["effective_approval_policy"];
576
+ assert_eq!(policy["enabled"], true, "{label}: policy={policy}");
577
+ assert_eq!(
578
+ policy["source"], "leader_process",
579
+ "{label}: policy={policy}"
580
+ );
581
+ assert_eq!(policy["inherited"], true, "{label}: policy={policy}");
582
+ }
583
+
584
+ fn codex_bypass_failures(argv: &[String], label: &str) -> Vec<String> {
585
+ let mut failures = Vec::new();
586
+ if !argv.iter().any(|arg| arg == CODEX_BYPASS) {
587
+ failures.push(format!(
588
+ "{label}: Codex worker argv must inherit bypass flag; argv={argv:?}"
589
+ ));
590
+ }
591
+ if argv
592
+ .iter()
593
+ .any(|arg| arg == "--sandbox" || arg == "--ask-for-approval")
594
+ {
595
+ failures.push(format!(
596
+ "{label}: bypass argv must not also include restricted sandbox/approval flags; argv={argv:?}"
597
+ ));
598
+ }
599
+ if !argv.iter().any(|arg| arg == "codex") {
600
+ failures.push(format!(
601
+ "{label}: provider executable must remain user PATH command name `codex`; argv={argv:?}"
602
+ ));
603
+ }
604
+ if argv.iter().any(|arg| arg.ends_with("/codex")) {
605
+ failures.push(format!(
606
+ "{label}: Codex executable must not be hard-coded as an absolute path; argv={argv:?}"
607
+ ));
608
+ }
609
+ failures
610
+ }
611
+
612
+ fn assert_codex_restricted(argv: &[String], label: &str) {
613
+ assert!(
614
+ has_adjacent(argv, &["--sandbox", "read-only"])
615
+ || has_adjacent(argv, &["--sandbox", "workspace-write"]),
616
+ "{label}: restricted Codex worker argv must include sandbox mode; argv={argv:?}"
617
+ );
618
+ assert!(
619
+ has_adjacent(argv, &["--ask-for-approval", "on-request"]),
620
+ "{label}: restricted Codex worker argv must ask on-request; argv={argv:?}"
621
+ );
622
+ assert!(
623
+ !argv.iter().any(|arg| arg == CODEX_BYPASS),
624
+ "{label}: restricted Codex worker argv must not contain bypass flag; argv={argv:?}"
625
+ );
626
+ assert_codex_command_name(argv, label);
627
+ }
628
+
629
+ fn assert_codex_command_name(argv: &[String], label: &str) {
630
+ assert!(
631
+ argv.iter().any(|arg| arg == "codex"),
632
+ "{label}: provider executable must remain user PATH command name `codex`; argv={argv:?}"
633
+ );
634
+ assert!(
635
+ !argv.iter().any(|arg| arg.ends_with("/codex")),
636
+ "{label}: Codex executable must not be hard-coded as an absolute path; argv={argv:?}"
637
+ );
638
+ }
639
+
640
+ fn has_adjacent(argv: &[String], needle: &[&str]) -> bool {
641
+ argv.windows(needle.len())
642
+ .any(|window| window.iter().zip(needle).all(|(a, b)| a == b))
643
+ }
644
+
645
+ fn launch_safety_json(report: &LaunchReport) -> String {
646
+ serde_json::to_string(&report.safety).unwrap()
647
+ }
648
+
649
+ fn compiled_team_dir(tag: &str, dangerous: bool, agents: &[(&str, &str)]) -> PathBuf {
650
+ let team = tmp_dir(tag).join("teamdir");
651
+ std::fs::create_dir_all(team.join("agents")).unwrap();
652
+ std::fs::write(
653
+ team.join("TEAM.md"),
654
+ format!(
655
+ "---\nname: {tag}\nobjective: Approval mirror contract.\nprovider: codex\ndangerous_auto_approve: {dangerous}\n---\n\nTeam.\n"
656
+ ),
657
+ )
658
+ .unwrap();
659
+ for (name, role) in agents {
660
+ std::fs::write(
661
+ team.join("agents").join(format!("{name}.md")),
662
+ role_doc(name, role),
663
+ )
664
+ .unwrap();
665
+ }
666
+ let spec = team_agent::compiler::compile_team(&team).unwrap();
667
+ std::fs::write(
668
+ team.join("team.spec.yaml"),
669
+ team_agent::model::yaml::dumps(&spec),
670
+ )
671
+ .unwrap();
672
+ seed_healthy_coordinator(&team_workspace(&team));
673
+ team
674
+ }
675
+
676
+ fn runtime_workspace(tag: &str, agents: &[(&str, &str)]) -> PathBuf {
677
+ compiled_team_dir(tag, false, agents)
678
+ }
679
+
680
+ fn team_workspace(team: &Path) -> PathBuf {
681
+ team.parent().unwrap().to_path_buf()
682
+ }
683
+
684
+ fn team_dir_with_running_workspace(tag: &str) -> PathBuf {
685
+ let team = compiled_team_dir(tag, false, &[("worker_a", "Worker A")]);
686
+ let ws = team_workspace(&team);
687
+ team_agent::state::persist::save_runtime_state(
688
+ &ws,
689
+ &json!({
690
+ "active_team_key": tag,
691
+ "team_dir": team.to_string_lossy().to_string(),
692
+ "spec_path": team.join("team.spec.yaml").to_string_lossy().to_string(),
693
+ "session_name": format!("team-{tag}"),
694
+ "agents": {
695
+ "worker_a": {
696
+ "status": "running",
697
+ "provider": "codex",
698
+ "auth_mode": "subscription",
699
+ "role": "Worker A",
700
+ "window": "worker_a",
701
+ "owner_team_id": tag,
702
+ "tools": ["mcp_team"]
703
+ }
704
+ }
705
+ }),
706
+ )
707
+ .unwrap();
708
+ seed_healthy_coordinator(&ws);
709
+ team
710
+ }
711
+
712
+ fn team_dir_with_forkable_source(tag: &str) -> PathBuf {
713
+ let team = compiled_team_dir(tag, false, &[("worker_a", "Worker A")]);
714
+ let ws = team_workspace(&team);
715
+ let rollout_root = PathBuf::from(std::env::var_os("HOME").expect("hermetic HOME"))
716
+ .join(".codex/sessions/approval-source");
717
+ std::fs::create_dir_all(&rollout_root).unwrap();
718
+ let rollout = rollout_root.join("rollout-sess-worker-a.jsonl");
719
+ std::fs::write(
720
+ &rollout,
721
+ format!(
722
+ "{}\n{}\n",
723
+ json!({
724
+ "type": "session_meta",
725
+ "payload": {"id": "sess-worker-a", "cwd": ws}
726
+ }),
727
+ json!({
728
+ "type": "response_item",
729
+ "payload": {"content": [{
730
+ "type": "input_text",
731
+ "text": "You are Team Agent worker `worker_a` with role `fixture`."
732
+ }]}
733
+ })
734
+ ),
735
+ )
736
+ .unwrap();
737
+ team_agent::state::persist::save_runtime_state(
738
+ &ws,
739
+ &json!({
740
+ "active_team_key": tag,
741
+ "team_dir": team.to_string_lossy().to_string(),
742
+ "spec_path": team.join("team.spec.yaml").to_string_lossy().to_string(),
743
+ "session_name": format!("team-{tag}"),
744
+ "agents": {
745
+ "worker_a": {
746
+ "status": "running",
747
+ "provider": "codex",
748
+ "auth_mode": "subscription",
749
+ "role": "Worker A",
750
+ "window": "worker_a",
751
+ "owner_team_id": tag,
752
+ "session_id": "sess-worker-a",
753
+ "rollout_path": rollout.to_string_lossy().to_string(),
754
+ // 0.4.6 tuple-atomic contract: fork requires complete
755
+ // source tuple. Seed captured_at + captured_via so the
756
+ // fork-source backing guard passes and the test
757
+ // exercises the inheritance behaviour being asserted.
758
+ "captured_at": "2026-06-25T10:00:00+00:00",
759
+ "captured_via": "session.captured",
760
+ "tools": ["mcp_team"]
761
+ }
762
+ }
763
+ }),
764
+ )
765
+ .unwrap();
766
+ seed_healthy_coordinator(&ws);
767
+ team
768
+ }
769
+
770
+ fn seed_agent_state(workspace: &Path, agent_id: &str, role: &str, resumable: bool) {
771
+ let rollout = workspace.join(format!("{agent_id}-rollout.jsonl"));
772
+ if resumable {
773
+ std::fs::write(&rollout, "{\"session_id\":\"sess-worker-a\"}\n").unwrap();
774
+ }
775
+ team_agent::state::persist::save_runtime_state(
776
+ workspace,
777
+ &json!({
778
+ "active_team_key": "team-a",
779
+ "team_dir": workspace.to_string_lossy().to_string(),
780
+ "spec_path": workspace.join("team.spec.yaml").to_string_lossy().to_string(),
781
+ "session_name": "team-approval",
782
+ "agents": {
783
+ agent_id: {
784
+ "status": "running",
785
+ "provider": "codex",
786
+ "auth_mode": "subscription",
787
+ "role": role,
788
+ "window": agent_id,
789
+ "owner_team_id": "team-a",
790
+ "session_id": if resumable { "sess-worker-a" } else { "" },
791
+ "rollout_path": if resumable { rollout.to_string_lossy().to_string() } else { String::new() },
792
+ // 0.4.6 tuple-atomic contract: when resumable, seed the
793
+ // full authoritative tuple so fork/restart can probe a
794
+ // real backing.
795
+ "captured_at": if resumable { "2026-06-25T10:00:00+00:00" } else { "" },
796
+ "captured_via": if resumable { "session.captured" } else { "" },
797
+ "first_send_at": "2026-06-05T09:00:00+00:00",
798
+ "tools": ["mcp_team"]
799
+ }
800
+ }
801
+ }),
802
+ )
803
+ .unwrap();
804
+ std::fs::write(workspace.join("team.spec.yaml"), "name: team-a\n").unwrap();
805
+ seed_healthy_coordinator(workspace);
806
+ }
807
+
808
+ fn seed_healthy_coordinator(workspace: &Path) {
809
+ let workspace = team_agent::coordinator::WorkspacePath::new(workspace.to_path_buf());
810
+ std::fs::create_dir_all(team_agent::model::paths::runtime_dir(workspace.as_path())).unwrap();
811
+ let _ = team_agent::message_store::MessageStore::open(workspace.as_path()).unwrap();
812
+ let pid = team_agent::coordinator::Pid::new(std::process::id());
813
+ team_agent::coordinator::write_coordinator_metadata(
814
+ &workspace,
815
+ pid,
816
+ team_agent::coordinator::MetadataSource::Boot,
817
+ )
818
+ .unwrap();
819
+ std::fs::write(
820
+ team_agent::coordinator::coordinator_pid_path(&workspace),
821
+ pid.to_string(),
822
+ )
823
+ .unwrap();
824
+ }
825
+
826
+ fn role_doc(name: &str, role: &str) -> String {
827
+ format!(
828
+ "---\nname: {name}\nrole: {role}\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{role}.\n"
829
+ )
830
+ }
831
+
832
+ fn source(rel: &str) -> String {
833
+ composite_source::composite_source(rel)
834
+ }
835
+
836
+ fn source_tree(rel: &str) -> String {
837
+ let root = Path::new(env!("CARGO_MANIFEST_DIR")).join(rel);
838
+ let mut out = String::new();
839
+ append_rs_sources(&root, &mut out);
840
+ out
841
+ }
842
+
843
+ fn append_rs_sources(path: &Path, out: &mut String) {
844
+ if path.is_dir() {
845
+ for entry in std::fs::read_dir(path).unwrap() {
846
+ append_rs_sources(&entry.unwrap().path(), out);
847
+ }
848
+ return;
849
+ }
850
+ if path.extension().and_then(|v| v.to_str()) == Some("rs") {
851
+ out.push_str(&std::fs::read_to_string(path).unwrap());
852
+ out.push('\n');
853
+ }
854
+ }
855
+
856
+ fn function_body<'a>(source: &'a str, name: &str) -> Option<&'a str> {
857
+ let start = source.find(&format!("fn {name}"))?;
858
+ let after = &source[start..];
859
+ let open = after.find('{')?;
860
+ let mut depth = 0usize;
861
+ for (idx, ch) in after[open..].char_indices() {
862
+ match ch {
863
+ '{' => depth += 1,
864
+ '}' => {
865
+ depth = depth.saturating_sub(1);
866
+ if depth == 0 {
867
+ return after.get(open + 1..open + idx);
868
+ }
869
+ }
870
+ _ => {}
871
+ }
872
+ }
873
+ None
874
+ }
875
+
876
+ fn strip_line_comments(source: &str) -> String {
877
+ source
878
+ .lines()
879
+ .map(|line| line.split_once("//").map_or(line, |(code, _)| code))
880
+ .collect::<Vec<_>>()
881
+ .join("\n")
882
+ }
883
+
884
+ fn event_or_source_contains(needle: &str) -> bool {
885
+ source_tree("src").contains(needle)
886
+ }
887
+
888
+ fn tmp_dir(tag: &str) -> PathBuf {
889
+ static N: AtomicU64 = AtomicU64::new(0);
890
+ let dir = std::env::temp_dir().join(format!(
891
+ "ta-rs-codex-approval-{tag}-{}-{}",
892
+ std::process::id(),
893
+ N.fetch_add(1, Ordering::Relaxed)
894
+ ));
895
+ let _ = std::fs::remove_dir_all(&dir);
896
+ std::fs::create_dir_all(&dir).unwrap();
897
+ std::fs::canonicalize(dir).unwrap()
898
+ }
899
+
900
+ struct MockAncestry {
901
+ previous: Option<String>,
902
+ }
903
+
904
+ impl MockAncestry {
905
+ fn codex_bypass() -> Self {
906
+ Self::raw(&["codex", CODEX_BYPASS])
907
+ }
908
+
909
+ fn restricted() -> Self {
910
+ Self::raw(&[
911
+ "codex",
912
+ "--sandbox",
913
+ "read-only",
914
+ "--ask-for-approval",
915
+ "on-request",
916
+ ])
917
+ }
918
+
919
+ fn raw(argv: &[&str]) -> Self {
920
+ let previous = std::env::var(ANCESTRY_ENV).ok();
921
+ unsafe {
922
+ std::env::set_var(ANCESTRY_ENV, serde_json::to_string(argv).unwrap());
923
+ }
924
+ Self { previous }
925
+ }
926
+ }
927
+
928
+ impl Drop for MockAncestry {
929
+ fn drop(&mut self) {
930
+ unsafe {
931
+ if let Some(previous) = self.previous.take() {
932
+ std::env::set_var(ANCESTRY_ENV, previous);
933
+ } else {
934
+ std::env::remove_var(ANCESTRY_ENV);
935
+ }
936
+ }
937
+ }
938
+ }
939
+
940
+ #[derive(Debug, Clone)]
941
+ struct RecordedSpawn {
942
+ kind: &'static str,
943
+ argv: Vec<String>,
944
+ env: BTreeMap<String, String>,
945
+ env_unset: Vec<String>,
946
+ session: SessionName,
947
+ window: WindowName,
948
+ pane_id: PaneId,
949
+ }
950
+
951
+ #[derive(Debug, Default)]
952
+ struct RecordingTransport {
953
+ spawns: Mutex<Vec<RecordedSpawn>>,
954
+ session_present: bool,
955
+ }
956
+
957
+ impl RecordingTransport {
958
+ fn new() -> Self {
959
+ Self::default()
960
+ }
961
+
962
+ fn with_session_present(mut self, present: bool) -> Self {
963
+ self.session_present = present;
964
+ self
965
+ }
966
+
967
+ fn only_spawn_argv(&self, label: &str) -> Vec<String> {
968
+ self.only_spawn(label).argv
969
+ }
970
+
971
+ fn only_spawn(&self, label: &str) -> RecordedSpawn {
972
+ let spawns = self.spawns.lock().unwrap();
973
+ assert_eq!(
974
+ spawns.len(),
975
+ 1,
976
+ "{label}: expected exactly one spawn; spawns={spawns:?}"
977
+ );
978
+ let _kind = spawns[0].kind;
979
+ spawns[0].clone()
980
+ }
981
+
982
+ fn record_spawn(
983
+ &self,
984
+ kind: &'static str,
985
+ session: &SessionName,
986
+ window: &WindowName,
987
+ argv: &[String],
988
+ cwd: &Path,
989
+ env: &BTreeMap<String, String>,
990
+ env_unset: &[String],
991
+ ) -> SpawnResult {
992
+ emit_codex_fork_backing(argv, cwd);
993
+ let mut spawns = self.spawns.lock().unwrap();
994
+ let pane_id = PaneId::new(format!("%{}", spawns.len() + 1));
995
+ spawns.push(RecordedSpawn {
996
+ kind,
997
+ argv: argv.to_vec(),
998
+ env: env.clone(),
999
+ env_unset: env_unset.to_vec(),
1000
+ session: session.clone(),
1001
+ window: window.clone(),
1002
+ pane_id: pane_id.clone(),
1003
+ });
1004
+ SpawnResult {
1005
+ pane_id,
1006
+ session: session.clone(),
1007
+ window: window.clone(),
1008
+ child_pid: None,
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ fn emit_codex_fork_backing(argv: &[String], cwd: &Path) {
1014
+ if !argv
1015
+ .windows(2)
1016
+ .any(|pair| pair[0] == "codex" && pair[1] == "fork")
1017
+ {
1018
+ return;
1019
+ }
1020
+ static SESSION_SEQ: AtomicU64 = AtomicU64::new(0);
1021
+ let sequence = SESSION_SEQ.fetch_add(1, Ordering::Relaxed);
1022
+ let session_id = format!("approval-fork-{}-{sequence}", std::process::id());
1023
+ let root = PathBuf::from(std::env::var_os("HOME").expect("hermetic HOME"))
1024
+ .join(".codex/sessions/approval-fixture");
1025
+ std::fs::create_dir_all(&root).unwrap();
1026
+ let record = serde_json::json!({
1027
+ "session_meta": {"payload": {
1028
+ "id": session_id.clone(),
1029
+ "cwd": cwd.to_string_lossy(),
1030
+ "created_at": chrono::Utc::now().to_rfc3339(),
1031
+ }}
1032
+ });
1033
+ std::fs::write(
1034
+ root.join(format!("rollout-{session_id}.jsonl")),
1035
+ format!("{record}\n"),
1036
+ )
1037
+ .unwrap();
1038
+ }
1039
+
1040
+ impl Transport for RecordingTransport {
1041
+ fn kind(&self) -> BackendKind {
1042
+ BackendKind::Tmux
1043
+ }
1044
+
1045
+ fn spawn_first(
1046
+ &self,
1047
+ session: &SessionName,
1048
+ window: &WindowName,
1049
+ argv: &[String],
1050
+ cwd: &Path,
1051
+ env: &BTreeMap<String, String>,
1052
+ ) -> Result<SpawnResult, TransportError> {
1053
+ Ok(self.record_spawn("spawn_first", session, window, argv, cwd, env, &[]))
1054
+ }
1055
+
1056
+ fn spawn_into(
1057
+ &self,
1058
+ session: &SessionName,
1059
+ window: &WindowName,
1060
+ argv: &[String],
1061
+ cwd: &Path,
1062
+ env: &BTreeMap<String, String>,
1063
+ ) -> Result<SpawnResult, TransportError> {
1064
+ Ok(self.record_spawn("spawn_into", session, window, argv, cwd, env, &[]))
1065
+ }
1066
+
1067
+ fn spawn_first_with_env_unset(
1068
+ &self,
1069
+ session: &SessionName,
1070
+ window: &WindowName,
1071
+ argv: &[String],
1072
+ cwd: &Path,
1073
+ env: &BTreeMap<String, String>,
1074
+ env_unset: &[String],
1075
+ ) -> Result<SpawnResult, TransportError> {
1076
+ Ok(self.record_spawn("spawn_first", session, window, argv, cwd, env, env_unset))
1077
+ }
1078
+
1079
+ fn spawn_into_with_env_unset(
1080
+ &self,
1081
+ session: &SessionName,
1082
+ window: &WindowName,
1083
+ argv: &[String],
1084
+ cwd: &Path,
1085
+ env: &BTreeMap<String, String>,
1086
+ env_unset: &[String],
1087
+ ) -> Result<SpawnResult, TransportError> {
1088
+ Ok(self.record_spawn("spawn_into", session, window, argv, cwd, env, env_unset))
1089
+ }
1090
+
1091
+ fn inject(
1092
+ &self,
1093
+ _target: &Target,
1094
+ _payload: &InjectPayload,
1095
+ _submit: Key,
1096
+ _bracketed: bool,
1097
+ ) -> Result<InjectReport, TransportError> {
1098
+ Ok(InjectReport {
1099
+ stage_reached: InjectStage::Submit,
1100
+ inject_verification: InjectVerification::CaptureContainsToken,
1101
+ submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
1102
+ turn_verification: TurnVerification::NotYetObserved,
1103
+ attempts: 1,
1104
+ submit_diagnostics: None,
1105
+ })
1106
+ }
1107
+
1108
+ fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
1109
+ Ok(())
1110
+ }
1111
+
1112
+ fn capture(
1113
+ &self,
1114
+ _target: &Target,
1115
+ range: CaptureRange,
1116
+ ) -> Result<CapturedText, TransportError> {
1117
+ Ok(CapturedText {
1118
+ text: "OpenAI Codex\ncodex>".to_string(),
1119
+ range,
1120
+ })
1121
+ }
1122
+
1123
+ fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
1124
+ match field {
1125
+ PaneField::PaneWidth => Ok(Some("120".to_string())),
1126
+ _ => Ok(None),
1127
+ }
1128
+ }
1129
+
1130
+ fn liveness(
1131
+ &self,
1132
+ _pane: &PaneId,
1133
+ ) -> Result<team_agent::transport::PaneLiveness, TransportError> {
1134
+ Ok(team_agent::transport::PaneLiveness::Live)
1135
+ }
1136
+
1137
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
1138
+ Ok(self
1139
+ .spawns
1140
+ .lock()
1141
+ .unwrap()
1142
+ .iter()
1143
+ .map(|spawn| PaneInfo {
1144
+ pane_id: spawn.pane_id.clone(),
1145
+ session: spawn.session.clone(),
1146
+ window_index: None,
1147
+ window_name: Some(spawn.window.clone()),
1148
+ pane_index: None,
1149
+ tty: None,
1150
+ current_command: Some("codex".to_string()),
1151
+ current_path: None,
1152
+ active: false,
1153
+ pane_pid: None,
1154
+ leader_env: BTreeMap::new(),
1155
+ })
1156
+ .collect())
1157
+ }
1158
+
1159
+ fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
1160
+ Ok(self.session_present || !self.spawns.lock().unwrap().is_empty())
1161
+ }
1162
+
1163
+ fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
1164
+ Ok(Vec::new())
1165
+ }
1166
+
1167
+ fn set_session_env(
1168
+ &self,
1169
+ _session: &SessionName,
1170
+ _key: &str,
1171
+ _value: &str,
1172
+ ) -> Result<SetEnvOutcome, TransportError> {
1173
+ Ok(SetEnvOutcome::Applied)
1174
+ }
1175
+
1176
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
1177
+ Ok(())
1178
+ }
1179
+
1180
+ fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
1181
+ Ok(())
1182
+ }
1183
+
1184
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
1185
+ Ok(AttachOutcome::Attached)
1186
+ }
1187
+ }