@team-agent/installer 0.5.60 → 0.5.62

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 (265) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +49 -3
  4. package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +184 -0
  6. package/crates/team-agent/src/cli/emit.rs +104 -21
  7. package/crates/team-agent/src/cli/helpers.rs +1 -0
  8. package/crates/team-agent/src/cli/leader.rs +14 -2
  9. package/crates/team-agent/src/cli/leaders.rs +2 -0
  10. package/crates/team-agent/src/cli/mod.rs +296 -8
  11. package/crates/team-agent/src/cli/named_address.rs +26 -2
  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 +3 -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 +5 -1
  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/status_port/store.rs +7 -3
  25. package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
  27. package/crates/team-agent/src/cli/types.rs +36 -2
  28. package/crates/team-agent/src/communication_mode/mod.rs +54 -0
  29. package/crates/team-agent/src/compiler/tests.rs +5 -5
  30. package/crates/team-agent/src/compiler.rs +54 -1
  31. package/crates/team-agent/src/conpty/backend.rs +1 -0
  32. package/crates/team-agent/src/conpty/mod.rs +1 -0
  33. package/crates/team-agent/src/coordinator/backoff.rs +5 -8
  34. package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
  35. package/crates/team-agent/src/coordinator/health.rs +1 -0
  36. package/crates/team-agent/src/coordinator/mod.rs +2 -2
  37. package/crates/team-agent/src/coordinator/orphan.rs +4 -116
  38. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  39. package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
  40. package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
  41. package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
  42. package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
  43. package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
  44. package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
  45. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
  46. package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
  47. package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
  48. package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
  49. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
  50. package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
  51. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
  52. package/crates/team-agent/src/coordinator/tick.rs +24 -28
  53. package/crates/team-agent/src/coordinator/types.rs +3 -176
  54. package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
  55. package/crates/team-agent/src/db/message_store.rs +47 -2
  56. package/crates/team-agent/src/db/migration.rs +5 -4
  57. package/crates/team-agent/src/db/mod.rs +1 -0
  58. package/crates/team-agent/src/db/schema.rs +1 -0
  59. package/crates/team-agent/src/diagnose/comms.rs +2 -1
  60. package/crates/team-agent/src/diagnose/mod.rs +1 -0
  61. package/crates/team-agent/src/diagnose/orphans.rs +1 -0
  62. package/crates/team-agent/src/fake_worker.rs +21 -1
  63. package/crates/team-agent/src/layout/manager.rs +3 -7
  64. package/crates/team-agent/src/layout/mod.rs +2 -2
  65. package/crates/team-agent/src/layout/overlay.rs +6 -1
  66. package/crates/team-agent/src/layout/placement.rs +1 -0
  67. package/crates/team-agent/src/layout/recovery.rs +3 -0
  68. package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
  69. package/crates/team-agent/src/layout/sessions.rs +8 -1
  70. package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
  71. package/crates/team-agent/src/layout/worker_env.rs +3 -0
  72. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  73. package/crates/team-agent/src/leader/helpers.rs +1 -0
  74. package/crates/team-agent/src/leader/incident.rs +1 -0
  75. package/crates/team-agent/src/leader/lease.rs +27 -22
  76. package/crates/team-agent/src/leader/mod.rs +2 -3
  77. package/crates/team-agent/src/leader/owner_bind.rs +13 -3
  78. package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
  79. package/crates/team-agent/src/leader/rediscover.rs +4 -17
  80. package/crates/team-agent/src/leader/registry.rs +3 -15
  81. package/crates/team-agent/src/leader/start.rs +1072 -186
  82. package/crates/team-agent/src/leader/takeover.rs +10 -232
  83. package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
  84. package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
  85. package/crates/team-agent/src/leader/tests/identity.rs +71 -59
  86. package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
  87. package/crates/team-agent/src/leader/tests/idle.rs +0 -193
  88. package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
  89. package/crates/team-agent/src/leader/tests/mod.rs +4 -41
  90. package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
  91. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
  92. package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
  93. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
  94. package/crates/team-agent/src/leader/types.rs +1 -28
  95. package/crates/team-agent/src/lib.rs +7 -0
  96. package/crates/team-agent/src/lifecycle/display.rs +7 -1
  97. package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
  98. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
  99. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
  100. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
  101. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
  102. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
  103. package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
  104. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
  105. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
  106. package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -28
  107. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
  108. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
  109. package/crates/team-agent/src/lifecycle/launch.rs +11 -12
  110. package/crates/team-agent/src/lifecycle/mod.rs +2 -1
  111. package/crates/team-agent/src/lifecycle/restart/agent.rs +4 -4
  112. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  113. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
  114. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
  115. package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
  116. package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
  117. package/crates/team-agent/src/lifecycle/restart.rs +13 -11
  118. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
  119. package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
  120. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
  121. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
  122. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
  123. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
  124. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
  125. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
  126. package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
  127. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
  128. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
  129. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
  130. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
  131. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
  132. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
  133. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
  134. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
  135. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
  136. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
  137. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
  138. package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
  139. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
  140. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
  141. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
  142. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
  143. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
  144. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
  145. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
  146. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
  147. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
  148. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
  149. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
  150. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
  151. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
  152. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
  153. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
  154. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
  155. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
  156. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
  157. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
  158. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
  159. package/crates/team-agent/src/lifecycle/tests.rs +59 -0
  160. package/crates/team-agent/src/lifecycle/types.rs +0 -9
  161. package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
  162. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  163. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
  164. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
  165. package/crates/team-agent/src/mcp_server/mod.rs +1 -0
  166. package/crates/team-agent/src/mcp_server/normalize.rs +5 -3
  167. package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
  168. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  169. package/crates/team-agent/src/mcp_server/tools.rs +101 -66
  170. package/crates/team-agent/src/mcp_server/types.rs +6 -18
  171. package/crates/team-agent/src/mcp_server/wire.rs +32 -26
  172. package/crates/team-agent/src/messaging/activity.rs +1 -0
  173. package/crates/team-agent/src/messaging/address.rs +1 -0
  174. package/crates/team-agent/src/messaging/delivery.rs +109 -41
  175. package/crates/team-agent/src/messaging/helpers.rs +2 -1
  176. package/crates/team-agent/src/messaging/leader_channel.rs +34 -10
  177. package/crates/team-agent/src/messaging/leader_receiver.rs +42 -24
  178. package/crates/team-agent/src/messaging/mod.rs +10 -14
  179. package/crates/team-agent/src/messaging/peers.rs +2 -0
  180. package/crates/team-agent/src/messaging/persist.rs +3 -1
  181. package/crates/team-agent/src/messaging/presentation.rs +111 -1
  182. package/crates/team-agent/src/messaging/results.rs +166 -17
  183. package/crates/team-agent/src/messaging/scheduler.rs +1 -0
  184. package/crates/team-agent/src/messaging/selftest.rs +1 -1
  185. package/crates/team-agent/src/messaging/send.rs +135 -89
  186. package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
  187. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
  188. package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
  189. package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
  190. package/crates/team-agent/src/messaging/types.rs +7 -2
  191. package/crates/team-agent/src/messaging/watchers.rs +24 -13
  192. package/crates/team-agent/src/model/enums.rs +1 -0
  193. package/crates/team-agent/src/model/errors.rs +1 -0
  194. package/crates/team-agent/src/model/ids.rs +1 -0
  195. package/crates/team-agent/src/model/mod.rs +2 -0
  196. package/crates/team-agent/src/model/name_similarity.rs +1 -0
  197. package/crates/team-agent/src/model/pane_authority_refusal.rs +359 -0
  198. package/crates/team-agent/src/model/paths.rs +1 -0
  199. package/crates/team-agent/src/model/permissions.rs +3 -2
  200. package/crates/team-agent/src/model/routing.rs +1 -0
  201. package/crates/team-agent/src/model/spec.rs +17 -0
  202. package/crates/team-agent/src/model/task_graph.rs +1 -0
  203. package/crates/team-agent/src/model/yaml/tests.rs +1 -0
  204. package/crates/team-agent/src/model/yaml.rs +1 -0
  205. package/crates/team-agent/src/packaging/install.rs +9 -147
  206. package/crates/team-agent/src/packaging/migrate.rs +2 -0
  207. package/crates/team-agent/src/packaging/mod.rs +1 -0
  208. package/crates/team-agent/src/packaging/repair.rs +2 -0
  209. package/crates/team-agent/src/packaging/tests.rs +29 -218
  210. package/crates/team-agent/src/packaging/types.rs +8 -15
  211. package/crates/team-agent/src/platform/argv.rs +4 -0
  212. package/crates/team-agent/src/platform/errors.rs +10 -1
  213. package/crates/team-agent/src/platform/file_lock.rs +4 -157
  214. package/crates/team-agent/src/platform/mod.rs +0 -61
  215. package/crates/team-agent/src/platform/process.rs +1 -0
  216. package/crates/team-agent/src/provider/adapter.rs +1 -0
  217. package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
  218. package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
  219. package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
  220. package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
  221. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
  222. package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
  223. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  224. package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
  225. package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
  226. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
  227. package/crates/team-agent/src/provider/classify.rs +1 -0
  228. package/crates/team-agent/src/provider/faults.rs +1 -26
  229. package/crates/team-agent/src/provider/helpers.rs +1 -0
  230. package/crates/team-agent/src/provider/mod.rs +1 -1
  231. package/crates/team-agent/src/provider/session/capture.rs +41 -22
  232. package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
  233. package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
  234. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
  235. package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
  236. package/crates/team-agent/src/provider/session/mod.rs +2 -4
  237. package/crates/team-agent/src/provider/session/resume.rs +2 -209
  238. package/crates/team-agent/src/provider/session_scan/claude.rs +93 -3
  239. package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
  240. package/crates/team-agent/src/provider/session_scan/common.rs +24 -7
  241. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  242. package/crates/team-agent/src/provider/session_scan.rs +6 -42
  243. package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
  244. package/crates/team-agent/src/provider/types.rs +1 -0
  245. package/crates/team-agent/src/provider/wire.rs +1 -0
  246. package/crates/team-agent/src/state/identity.rs +16 -5
  247. package/crates/team-agent/src/state/identity_keys.rs +1 -0
  248. package/crates/team-agent/src/state/mod.rs +2 -0
  249. package/crates/team-agent/src/state/owner_gate.rs +5 -0
  250. package/crates/team-agent/src/state/ownership.rs +10 -4
  251. package/crates/team-agent/src/state/paths.rs +12 -0
  252. package/crates/team-agent/src/state/persist.rs +9 -3
  253. package/crates/team-agent/src/state/projection.rs +15 -2
  254. package/crates/team-agent/src/state/repository/intent.rs +1 -0
  255. package/crates/team-agent/src/state/repository/tests.rs +1 -0
  256. package/crates/team-agent/src/state/repository.rs +7 -0
  257. package/crates/team-agent/src/state/selector.rs +1 -0
  258. package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
  259. package/crates/team-agent/src/tmux_backend.rs +63 -0
  260. package/crates/team-agent/src/topology.rs +3 -0
  261. package/crates/team-agent/src/transport.rs +15 -0
  262. package/package.json +4 -4
  263. package/skills/team-agent/command-coverage.json +379 -0
  264. package/crates/team-agent/src/leader/inject.rs +0 -33
  265. package/crates/team-agent/src/provider/command.rs +0 -80
@@ -0,0 +1,986 @@
1
+ //! P0 RED contract: host co-tenant death / worker cohort identity.
2
+ //!
3
+ //! Source: `.team/artifacts/host-cotenant-death-p0-architecture-locate.md` §8.
4
+
5
+ #![cfg(unix)]
6
+ #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
7
+
8
+ #[path = "../../../tests/support/hermetic.rs"]
9
+ mod hermetic_guard;
10
+ #[allow(dead_code)]
11
+ fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
12
+
13
+ use std::collections::{BTreeMap, BTreeSet};
14
+ use std::fs;
15
+ use std::io::Write;
16
+ use std::os::unix::fs::PermissionsExt;
17
+ use std::path::{Path, PathBuf};
18
+ use std::sync::{Arc, Mutex};
19
+
20
+ use serde_json::{json, Value};
21
+ use serial_test::serial;
22
+ use team_agent::coordinator::{
23
+ coordinator_pid_path, Coordinator, MetadataSource, Pid, ProviderRegistry, WorkspacePath,
24
+ };
25
+ use team_agent::lifecycle::{
26
+ reset_agent_with_transport, restart_with_transport_with_readiness_deadline,
27
+ start_agent_with_transport, LifecycleError, RestartReport,
28
+ };
29
+ use team_agent::message_store::MessageStore;
30
+ use team_agent::model::enums::{PaneLiveness, Provider};
31
+ use team_agent::model::ids::AgentId;
32
+ use team_agent::model::paths::{runtime_dir, runtime_spec_path};
33
+ use team_agent::provider::{get_adapter, CaptureSessionContext, ProviderAdapter};
34
+ use team_agent::state::persist::{load_runtime_state, save_runtime_state};
35
+ use team_agent::transport::{
36
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
37
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, SessionName,
38
+ SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
39
+ TurnVerification, WindowName,
40
+ };
41
+
42
+ const TEAM: &str = "current";
43
+ const SESSION: &str = "team-current";
44
+ const TMUX_ENDPOINT: &str = "/private/tmp/ta-p0-cohort.sock";
45
+ const POLLUTED_CALLER_AUTHORITY_ENVS: &[&str] = &[
46
+ "TEAM_AGENT_LEADER_PANE_ID",
47
+ "TEAM_AGENT_LEADER_SESSION_UUID",
48
+ "TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
49
+ "TEAM_AGENT_LEADER_SESSION_NAME",
50
+ "TEAM_AGENT_LEADER_PROVIDER",
51
+ "TEAM_AGENT_MACHINE_FINGERPRINT",
52
+ "TEAM_AGENT_WORKSPACE",
53
+ "TEAM_AGENT_TEAM_ID",
54
+ "TEAM_AGENT_OWNER_TEAM_ID",
55
+ "TEAM_AGENT_ACTIVE_TEAM",
56
+ "TEAM_AGENT_ID",
57
+ "TEAM_AGENT_AGENT_ID",
58
+ "TEAM_AGENT_AUTH_MODE",
59
+ "TEAM_AGENT_LEADER_BYPASS",
60
+ "TEAM_AGENT_LEADER_BYPASS_SOURCE",
61
+ "TEAM_AGENT_LEADER_BYPASS_PROVIDER",
62
+ "TEAM_AGENT_LEADER_BYPASS_FLAG",
63
+ "TEAM_AGENT_MCP_AUTO_APPROVE",
64
+ "TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE",
65
+ ];
66
+
67
+ #[test]
68
+ #[serial(env)]
69
+ fn polluted_caller_env_never_authorizes_teardown_to_kill_foreign_tmux() {
70
+ let env = hermetic_guard::HermeticTestEnv::enter("p0-caller-env-sentinel");
71
+ let shim_dir = env.root().join("bin");
72
+ fs::create_dir_all(&shim_dir).unwrap();
73
+ let recorder = hermetic_guard::short_tmux_socket("p0-caller-env-recorder")
74
+ .with_extension("log");
75
+ let _ = fs::remove_file(&recorder);
76
+ let sentinel = env.root().join("foreign-sentinel.sock");
77
+ let polluted_tmux = format!("{},4242,0", sentinel.display());
78
+
79
+ let real_path = std::env::var_os("PATH").unwrap_or_default();
80
+ let real_tmux = std::env::split_paths(&real_path)
81
+ .map(|dir| dir.join("tmux"))
82
+ .find(|path| path.is_file())
83
+ .expect("real tmux binary on PATH");
84
+ let shim = shim_dir.join("tmux");
85
+ fs::write(
86
+ &shim,
87
+ r#"#!/bin/sh
88
+ {
89
+ printf 'TMUX=%s' "${TMUX-}"
90
+ for arg in "$@"; do printf '\t%s' "$arg"; done
91
+ printf '\n'
92
+ } >> "$TEAM_AGENT_TEST_TMUX_RECORDER_LOG"
93
+ case " $* " in
94
+ *" kill-session "*|*" kill-server "*) exit 0 ;;
95
+ esac
96
+ exec "$TEAM_AGENT_TEST_REAL_TMUX" "$@"
97
+ "#,
98
+ )
99
+ .unwrap();
100
+ let mut permissions = fs::metadata(&shim).unwrap().permissions();
101
+ permissions.set_mode(0o755);
102
+ fs::set_permissions(&shim, permissions).unwrap();
103
+
104
+ let mut polluted_env = vec![
105
+ env.with_env(
106
+ "PATH",
107
+ &format!("{}:{}", shim_dir.display(), real_path.to_string_lossy()),
108
+ ),
109
+ env.with_env(
110
+ "TEAM_AGENT_TEST_TMUX_RECORDER_LOG",
111
+ recorder.to_str().unwrap(),
112
+ ),
113
+ env.with_env("TEAM_AGENT_TEST_REAL_TMUX", real_tmux.to_str().unwrap()),
114
+ env.with_env("TMUX", &polluted_tmux),
115
+ env.with_env("TMUX_PANE", "%foreign-sentinel"),
116
+ ];
117
+ for key in POLLUTED_CALLER_AUTHORITY_ENVS {
118
+ polluted_env.push(env.with_env(key, "foreign-caller-authority"));
119
+ }
120
+
121
+ let missing_scrub_keys = POLLUTED_CALLER_AUTHORITY_ENVS
122
+ .iter()
123
+ .filter(|key| !hermetic_guard::CALLER_IDENTITY_ENVS.contains(key))
124
+ .copied()
125
+ .collect::<Vec<_>>();
126
+
127
+ let _registration = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
128
+ env.register_owned_tmux_socket(&sentinel);
129
+ }));
130
+ drop(env);
131
+
132
+ let recorded = fs::read_to_string(&recorder).unwrap_or_default();
133
+ let destructive_hits = recorded
134
+ .lines()
135
+ .filter(|line| line.contains("\tkill-session") || line.contains("\tkill-server"))
136
+ .filter(|line| {
137
+ let explicit_endpoint = line.contains("\t-S\t") || line.contains("\t-L\t");
138
+ let targets_sentinel = line.split('\t').any(|arg| arg == sentinel.to_str().unwrap());
139
+ let inherits_sentinel = line.starts_with(&format!("TMUX={polluted_tmux}\t"));
140
+ targets_sentinel || (inherits_sentinel && !explicit_endpoint)
141
+ })
142
+ .collect::<Vec<_>>();
143
+ let _ = fs::remove_file(&recorder);
144
+
145
+ assert!(
146
+ missing_scrub_keys.is_empty() && destructive_hits.is_empty(),
147
+ "P0 caller-env boundary: polluted caller identity must never become teardown authority; missing_scrub_keys={missing_scrub_keys:?}; destructive tmux argv targeting ambient sentinel={destructive_hits:?}; all recorded argv={recorded:?}"
148
+ );
149
+ drop(polluted_env);
150
+ }
151
+
152
+ #[test]
153
+ #[serial(env)]
154
+ fn restart_allow_fresh_does_not_leave_same_role_host_cotenants() {
155
+ let mut failures = Vec::new();
156
+ let (env, workspace) = team_case("p0-restart", &["developer", "test-engineer", "arch-reviewer"]);
157
+ let shim_dir = env.root().join("bin");
158
+ fs::create_dir_all(&shim_dir).unwrap();
159
+ let recorder = env.root().join("ps-recorder.log");
160
+ let real_path = std::env::var_os("PATH").unwrap_or_default();
161
+ let real_ps = std::env::split_paths(&real_path)
162
+ .map(|dir| dir.join("ps"))
163
+ .find(|path| path.is_file())
164
+ .expect("real ps binary on PATH");
165
+ let shim = shim_dir.join("ps");
166
+ fs::write(
167
+ &shim,
168
+ r#"#!/bin/sh
169
+ {
170
+ first=1
171
+ for arg in "$@"; do
172
+ if [ "$first" -eq 0 ]; then printf '\t'; fi
173
+ printf '%s' "$arg"
174
+ first=0
175
+ done
176
+ printf '\n'
177
+ } >> "$TEAM_AGENT_TEST_PS_RECORDER_LOG"
178
+ if [ "$#" -eq 4 ] && [ "$1" = "-p" ] && [ "$2" = "$TEAM_AGENT_TEST_SELF_PID" ] && [ "$3" = "-o" ] && [ "$4" = "stat=" ]; then
179
+ printf 'S\n'
180
+ exit 0
181
+ fi
182
+ exec "$TEAM_AGENT_TEST_REAL_PS" "$@"
183
+ "#,
184
+ )
185
+ .unwrap();
186
+ let mut permissions = fs::metadata(&shim).unwrap().permissions();
187
+ permissions.set_mode(0o755);
188
+ fs::set_permissions(&shim, permissions).unwrap();
189
+ let self_pid = std::process::id().to_string();
190
+ let _ps_env = [
191
+ env.with_env(
192
+ "PATH",
193
+ &format!("{}:{}", shim_dir.display(), real_path.to_string_lossy()),
194
+ ),
195
+ env.with_env(
196
+ "TEAM_AGENT_TEST_PS_RECORDER_LOG",
197
+ recorder.to_str().unwrap(),
198
+ ),
199
+ env.with_env("TEAM_AGENT_TEST_REAL_PS", real_ps.to_str().unwrap()),
200
+ env.with_env("TEAM_AGENT_TEST_SELF_PID", &self_pid),
201
+ ];
202
+ let transport = CohortTransport::with_old_panes(&["developer", "test-engineer", "arch-reviewer"]);
203
+ let before = load_runtime_state(&workspace).unwrap();
204
+ let restart = restart_with_transport_with_readiness_deadline(
205
+ &workspace,
206
+ true,
207
+ Some(TEAM),
208
+ &transport,
209
+ Some(1_000),
210
+ );
211
+ if !matches!(&restart, Ok(RestartReport::Restarted { .. })) {
212
+ failures.push(format!(
213
+ "restart --allow-fresh: report must be Restarted, never readiness timeout; report={restart:?}"
214
+ ));
215
+ }
216
+ collect_lifecycle_cohort_result(
217
+ &mut failures,
218
+ "restart --allow-fresh",
219
+ restart.map(|_| ()),
220
+ &transport,
221
+ );
222
+ let restart_coordinator_pid = fs::read_to_string(coordinator_pid_path(&WorkspacePath::new(
223
+ workspace.clone(),
224
+ )))
225
+ .unwrap_or_default();
226
+ if restart_coordinator_pid.trim() != self_pid {
227
+ failures.push(format!(
228
+ "restart --allow-fresh: seeded coordinator pid must remain the integration-test self pid; expected={self_pid}; actual={restart_coordinator_pid:?}"
229
+ ));
230
+ }
231
+ let after = load_runtime_state(&workspace).unwrap();
232
+ for worker in ["developer", "test-engineer", "arch-reviewer"] {
233
+ let live = transport.live_panes_for_window(worker);
234
+ if live.len() != 1 {
235
+ failures.push(format!(
236
+ "restart --allow-fresh: {worker} must have one live tuple before state comparison; live={live:?}"
237
+ ));
238
+ continue;
239
+ }
240
+ let live = &live[0];
241
+ let saved = &after["agents"][worker];
242
+ let prior = &before["agents"][worker];
243
+ let saved_tuple = (
244
+ saved.get("window").and_then(Value::as_str),
245
+ saved.get("pane_id").and_then(Value::as_str),
246
+ saved.get("pane_pid").and_then(Value::as_u64),
247
+ );
248
+ let live_tuple = (
249
+ live.window_name.as_ref().map(WindowName::as_str),
250
+ Some(live.pane_id.as_str()),
251
+ live.pane_pid.map(u64::from),
252
+ );
253
+ if saved_tuple != live_tuple {
254
+ failures.push(format!(
255
+ "restart --allow-fresh: {worker} saved state tuple must equal its sole live tuple; saved={saved_tuple:?}; live={live_tuple:?}"
256
+ ));
257
+ }
258
+ let prior_pane = prior.get("pane_id").and_then(Value::as_str);
259
+ if saved.get("pane_id").and_then(Value::as_str) == prior_pane {
260
+ failures.push(format!(
261
+ "restart --allow-fresh: {worker} state must not remain bound to retired pane {prior_pane:?}"
262
+ ));
263
+ }
264
+ let prior_epoch = prior.get("spawn_epoch").and_then(Value::as_u64).unwrap_or(0);
265
+ let saved_epoch = saved.get("spawn_epoch").and_then(Value::as_u64).unwrap_or(0);
266
+ if saved_epoch <= prior_epoch {
267
+ failures.push(format!(
268
+ "restart --allow-fresh: {worker} spawn_epoch must increase monotonically; before={prior_epoch}; after={saved_epoch}"
269
+ ));
270
+ }
271
+ }
272
+ let (_env2, workspace2) = team_case("p0-start-force", &["developer"]);
273
+ let mut before_force = load_runtime_state(&workspace2).unwrap();
274
+ before_force["agents"]["developer"]["provider"] = json!("fake");
275
+ save_runtime_state(&workspace2, &before_force).unwrap();
276
+ let transport2 = CohortTransport::with_old_panes(&["developer"]);
277
+ collect_lifecycle_cohort_result(
278
+ &mut failures,
279
+ "start-agent --force",
280
+ start_agent_with_transport(&workspace2, &AgentId::new("developer"), true, false, true, Some(TEAM), &transport2).map(|_| ()),
281
+ &transport2,
282
+ );
283
+ let force_spawned = transport2
284
+ .pane_summary()
285
+ .into_iter()
286
+ .filter(|pane| pane.contains(":%new-"))
287
+ .collect::<Vec<_>>();
288
+ let after_force = load_runtime_state(&workspace2).unwrap();
289
+ if !force_spawned.is_empty()
290
+ || after_force["agents"]["developer"]["pane_id"]
291
+ != before_force["agents"]["developer"]["pane_id"]
292
+ {
293
+ failures.push(format!(
294
+ "start-agent --force: fake provider must share the same zero-mutation pre-spawn cohort refusal; spawned={force_spawned:?}; before={}; after={}",
295
+ before_force["agents"]["developer"], after_force["agents"]["developer"]
296
+ ));
297
+ }
298
+ let start_force_coordinator_pid = fs::read_to_string(coordinator_pid_path(
299
+ &WorkspacePath::new(workspace2.clone()),
300
+ ))
301
+ .unwrap_or_default();
302
+ if start_force_coordinator_pid.trim() != self_pid {
303
+ failures.push(format!(
304
+ "start-agent --force: seeded coordinator pid must remain the integration-test self pid; expected={self_pid}; actual={start_force_coordinator_pid:?}"
305
+ ));
306
+ }
307
+ let recorded_ps = fs::read_to_string(&recorder).unwrap_or_default();
308
+ let expected_probe = format!("-p\t{self_pid}\t-o\tstat=");
309
+ if !recorded_ps.lines().any(|line| line == expected_probe) {
310
+ failures.push(format!(
311
+ "fixture ps shim must observe the exact self-pid coordinator health probe {expected_probe:?}; recorded={recorded_ps:?}"
312
+ ));
313
+ }
314
+ assert!(
315
+ failures.is_empty(),
316
+ "P0 fixture1: lifecycle fresh/force paths must retire/swap/refuse so each (team_key, agent_id) has exactly one live tuple:\n{}",
317
+ failures.join("\n")
318
+ );
319
+ }
320
+
321
+ #[test]
322
+ #[serial(env)]
323
+ fn restart_bad_spawn_identity_fails_closed_before_stale_binding_and_new_orphan() {
324
+ let (_env, workspace) = team_case("p0-bad-spawn-identity", &["developer"]);
325
+ let before = load_runtime_state(&workspace).unwrap();
326
+ let old_pane = before["agents"]["developer"]["pane_id"]
327
+ .as_str()
328
+ .unwrap()
329
+ .to_string();
330
+ let old_epoch = before["agents"]["developer"]["spawn_epoch"]
331
+ .as_u64()
332
+ .unwrap();
333
+ let transport = CohortTransport::with_old_panes(&["developer"]).with_bad_spawn_identity();
334
+
335
+ let result = restart_with_transport_with_readiness_deadline(
336
+ &workspace,
337
+ true,
338
+ Some(TEAM),
339
+ &transport,
340
+ Some(0),
341
+ );
342
+ let report = format!("{result:?}");
343
+ let report_lower = report.to_ascii_lowercase();
344
+ let refused = !matches!(
345
+ &result,
346
+ Ok(RestartReport::Restarted { .. } | RestartReport::Partial { .. })
347
+ );
348
+ let named_identity_mismatch = report_lower.contains("spawn")
349
+ && (report_lower.contains("identity") || report_lower.contains("binding"))
350
+ && report_lower.contains("mismatch");
351
+ let after = load_runtime_state(&workspace).unwrap();
352
+ let saved = &after["agents"]["developer"];
353
+ let saved_pane = saved.get("pane_id").and_then(Value::as_str);
354
+ let saved_epoch = saved.get("spawn_epoch").and_then(Value::as_u64).unwrap_or(0);
355
+ let old_live = transport
356
+ .has_pane(&PaneId::new(old_pane.as_str()))
357
+ .unwrap()
358
+ .unwrap_or(false);
359
+ let new_live = transport
360
+ .live_panes_for_window("developer")
361
+ .into_iter()
362
+ .filter(|pane| pane.pane_id.as_str().starts_with("%new-"))
363
+ .map(|pane| pane.pane_id.as_str().to_string())
364
+ .collect::<Vec<_>>();
365
+ let stale_third_state = !old_live && saved_pane == Some(old_pane.as_str()) && !new_live.is_empty();
366
+ let old_redeclared_as_new = saved_pane == Some(old_pane.as_str()) && saved_epoch > old_epoch;
367
+
368
+ assert!(
369
+ refused && named_identity_mismatch && !stale_third_state && !old_redeclared_as_new,
370
+ "P0 bad-spawn-identity must fail closed with a spawn identity/binding mismatch and must never reach retired-old + state-bound-old + live-new-orphan third state; report={report}; old_live={old_live}; saved_pane={saved_pane:?}; epochs={old_epoch}->{saved_epoch}; new_live={new_live:?}; panes={:?}",
371
+ transport.pane_summary()
372
+ );
373
+ }
374
+
375
+ #[test]
376
+ #[serial(env)]
377
+ fn start_agent_spawn_verification_failure_rolls_back_new_pane() {
378
+ let (_env, workspace) = team_case("p0-start-spawn-rollback", &["developer"]);
379
+ let before = load_runtime_state(&workspace).unwrap();
380
+ let old_pane = before["agents"]["developer"]["pane_id"]
381
+ .as_str()
382
+ .unwrap()
383
+ .to_string();
384
+ let old_epoch = before["agents"]["developer"]["spawn_epoch"]
385
+ .as_u64()
386
+ .unwrap();
387
+ let transport = CohortTransport::with_old_panes(&["developer"]).with_hidden_new_panes();
388
+ transport
389
+ .kill_pane(&PaneId::new(old_pane.as_str()))
390
+ .unwrap();
391
+
392
+ let result = start_agent_with_transport(
393
+ &workspace,
394
+ &AgentId::new("developer"),
395
+ true,
396
+ false,
397
+ true,
398
+ Some(TEAM),
399
+ &transport,
400
+ );
401
+ let report = format!("{result:?}");
402
+ let after = load_runtime_state(&workspace).unwrap();
403
+ let live_new = transport
404
+ .live_panes_for_window("developer")
405
+ .into_iter()
406
+ .filter(|pane| pane.pane_id.as_str().starts_with("%new-"))
407
+ .map(|pane| pane.pane_id.as_str().to_string())
408
+ .collect::<Vec<_>>();
409
+
410
+ assert!(
411
+ result.is_err() && report.contains("spawned pane not addressable"),
412
+ "spawn verification must fail closed with identity evidence; report={report}"
413
+ );
414
+ assert!(
415
+ live_new.is_empty(),
416
+ "a failed start-agent must kill only the pane created by that attempt; live_new={live_new:?}; panes={:?}",
417
+ transport.pane_summary()
418
+ );
419
+ assert_eq!(after["agents"]["developer"]["pane_id"], old_pane);
420
+ assert_eq!(
421
+ after["agents"]["developer"]["spawn_epoch"], old_epoch,
422
+ "failed replacement must leave persisted state authoritative"
423
+ );
424
+ }
425
+
426
+ #[test]
427
+ #[serial(env)]
428
+ fn start_agent_list_targets_failure_refuses_before_spawn() {
429
+ let (_env, workspace) = team_case("p0-start-observation-failure", &["developer"]);
430
+ let before = load_runtime_state(&workspace).unwrap();
431
+ let transport = CohortTransport::with_old_panes(&["developer"]).with_list_targets_failure();
432
+
433
+ let result = start_agent_with_transport(
434
+ &workspace,
435
+ &AgentId::new("developer"),
436
+ true,
437
+ false,
438
+ true,
439
+ Some(TEAM),
440
+ &transport,
441
+ );
442
+ let after = load_runtime_state(&workspace).unwrap();
443
+ let report = format!("{result:?}");
444
+
445
+ assert!(
446
+ result.is_err() && report.contains("same-role cohort observation failed"),
447
+ "an unobservable cohort must refuse before spawn; report={report}"
448
+ );
449
+ assert_eq!(after["agents"]["developer"], before["agents"]["developer"]);
450
+ assert!(
451
+ transport
452
+ .pane_summary()
453
+ .iter()
454
+ .all(|pane| !pane.contains(":%new-")),
455
+ "observation failure must not spawn a pane; panes={:?}",
456
+ transport.pane_summary()
457
+ );
458
+ }
459
+
460
+ #[test]
461
+ #[serial(env)]
462
+ fn codex_capture_rejects_old_rollout_with_new_mtime_and_uses_typed_claim_keys() {
463
+ let env = hermetic_guard::HermeticTestEnv::enter("p0-capture");
464
+ let spawn_cwd = env.workspace("spawn-cwd");
465
+ let rollout = write_old_codex_rollout(env.home(), &spawn_cwd);
466
+ fs::OpenOptions::new()
467
+ .append(true)
468
+ .open(&rollout)
469
+ .unwrap()
470
+ .write_all(b"{\"type\":\"event\",\"payload\":{\"append\":\"after spawn\"}}\n")
471
+ .unwrap();
472
+
473
+ let context = CaptureSessionContext {
474
+ agent_id: "developer".to_string(),
475
+ spawn_cwd: spawn_cwd.clone(),
476
+ pane_id: Some("%42".to_string()),
477
+ pane_pid: Some(4242),
478
+ spawned_at: Some("2026-01-02T00:00:00+00:00".to_string()),
479
+ expected_session_id: None,
480
+ provider_projects_root: None,
481
+ };
482
+ let candidates = get_adapter(Provider::Codex)
483
+ .capture_session_candidates(&context, 0)
484
+ .expect("scan codex candidates");
485
+
486
+ let capture_src = include_str!(concat!(
487
+ env!("CARGO_MANIFEST_DIR"),
488
+ "/src/provider/session/capture.rs"
489
+ ));
490
+ let mut failures = Vec::new();
491
+ if candidates.iter().any(|candidate| {
492
+ candidate
493
+ .captured
494
+ .rollout_path
495
+ .as_ref()
496
+ .is_some_and(|path| path.as_path() == rollout)
497
+ }) {
498
+ failures.push(format!(
499
+ "old rollout was accepted as a fresh candidate: filename/created_at predate spawn, only mutable mtime is new; candidates={candidates:?}"
500
+ ));
501
+ }
502
+ if !capture_src.contains("SessionAttributionKey")
503
+ || capture_src.contains("format!(\"session:{}\"")
504
+ || capture_src.contains("format!(\"rollout:{}\"")
505
+ {
506
+ failures.push(
507
+ "claimed/captured keys must be typed by provider+team_key+agent_id(+session/rollout), not bare session:/rollout: strings".to_string(),
508
+ );
509
+ }
510
+
511
+ assert!(
512
+ failures.is_empty(),
513
+ "P0 capture contract failed:\n{}",
514
+ failures.join("\n")
515
+ );
516
+ }
517
+
518
+ #[test]
519
+ #[serial(env)]
520
+ fn reset_stopped_true_still_rechecks_same_role_residue_before_start() {
521
+ let (_env, workspace) = team_case("p0-reset", &["developer"]);
522
+ let transport = CohortTransport::with_old_panes(&["developer"]);
523
+
524
+ let result = reset_agent_with_transport(&workspace, &AgentId::new("developer"), true, false, Some(TEAM), &transport);
525
+ let error_text = result.as_ref().err().map(ToString::to_string).unwrap_or_default();
526
+ assert!(
527
+ error_text.contains("stop_not_proven")
528
+ || error_text.contains("residue")
529
+ || error_text.contains("same-role"),
530
+ "P0 reset: stop.stopped=true is not proof when the same-role pane remains visible; reset must return a named residue proof error (stop_not_proven/residue/same-role) before spawning; result={result:?} error={error_text:?} panes={:?}",
531
+ transport.pane_summary()
532
+ );
533
+ }
534
+
535
+ #[test]
536
+ fn mcp_reset_output_exposes_capture_outcome_and_weak_reset_proof() {
537
+ let src = include_str!(concat!(
538
+ env!("CARGO_MANIFEST_DIR"),
539
+ "/src/mcp_server/lifecycle_tools/agent_ops.rs"
540
+ ));
541
+ let reset = source_section(src, "pub(crate) fn reset_agent", "pub(crate) fn fork_agent");
542
+ let json_object = reset_success_json_object(reset);
543
+ let missing = ["\"capture_state\":", "\"reset_proof\":", "\"transcript_missing\"", "\"attribution_ambiguous\"", "\"weak\""]
544
+ .into_iter().filter(|needle| !json_object.contains(needle)).collect::<Vec<_>>();
545
+ assert!(
546
+ missing.is_empty(),
547
+ "P0 MCP reset: ok=true/status=reset must expose additive capture_state and reset_proof=weak inside the ResetAgentOutcome::Reset serde_json::json! object; missing={missing:?}; reset_json={json_object}"
548
+ );
549
+ }
550
+
551
+ #[test]
552
+ #[serial(env)]
553
+ fn abnormal_shell_pane_with_live_delivery_is_not_dead_and_dedupe_ignores_size() {
554
+ let env = hermetic_guard::HermeticTestEnv::enter("p0-abnormal");
555
+ let workspace = env.workspace("abnormal");
556
+ let rollout = workspace.join("architect-rollout.jsonl");
557
+ fs::write(&rollout, "{\"type\":\"session_meta\",\"payload\":{\"id\":\"sess-arch\",\"cwd\":\"/tmp\"}}\n").unwrap();
558
+ seed_healthy_coordinator(&workspace);
559
+ save_runtime_state(
560
+ &workspace,
561
+ &json!({
562
+ "active_team_key": TEAM,
563
+ "session_name": SESSION,
564
+ "agents": {
565
+ "architect": {
566
+ "status": "running",
567
+ "provider": "codex",
568
+ "agent_id": "architect",
569
+ "window": "architect",
570
+ "pane_id": "%old-architect",
571
+ "session_id": "sess-arch",
572
+ "rollout_path": rollout.to_string_lossy(),
573
+ "spawn_epoch": 7,
574
+ "spawn_cwd": workspace.to_string_lossy()
575
+ }
576
+ }
577
+ }),
578
+ )
579
+ .unwrap();
580
+ let transport = CohortTransport::with_old_panes(&["architect"]).with_current_command("bash");
581
+ transport
582
+ .inject(
583
+ &Target::Pane(PaneId::new("%old-architect")),
584
+ &InjectPayload::Text("ping live tuple".to_string()),
585
+ Key::Enter,
586
+ false,
587
+ )
588
+ .expect("fixture proves the same physical tuple can receive a message");
589
+ let coord = Coordinator::new(
590
+ WorkspacePath::new(workspace.clone()),
591
+ Box::new(RealAdapterRegistry),
592
+ Box::new(transport.clone()),
593
+ );
594
+ let tick = coord.tick();
595
+
596
+ let state = load_runtime_state(&workspace).unwrap();
597
+ let watch = state
598
+ .pointer("/coordinator/abnormal_exit_watch/architect")
599
+ .cloned()
600
+ .unwrap_or(Value::Null);
601
+ let src = include_str!(concat!(
602
+ env!("CARGO_MANIFEST_DIR"),
603
+ "/src/coordinator/steps/abnormal.rs"
604
+ ));
605
+ let mut failures = Vec::new();
606
+ if let Err(error) = tick { failures.push(format!("coordinator tick failed before abnormal fixture could be evaluated: {error}")); }
607
+ if watch.is_null() { failures.push("coordinator tick did not write abnormal_exit_watch for the live shell fixture".to_string()); }
608
+ if watch.pointer("/last_liveness").and_then(Value::as_str) == Some("dead")
609
+ || watch
610
+ .pointer("/last_liveness_detail")
611
+ .and_then(Value::as_str)
612
+ .is_some_and(|detail| detail.contains("provider_not_foreground:bash"))
613
+ {
614
+ failures.push(format!(
615
+ "live wrapper shell with no exit marker was classified dead; watch={watch}"
616
+ ));
617
+ }
618
+ for needle in [
619
+ "fn abnormal_dedupe_key(\n agent: &AbnormalWatchAgent,\n fact: &crate::provider::FaultFact,\n size: u64",
620
+ "fn abnormal_suppression_key(\n agent: &AbnormalWatchAgent,\n liveness: &ProcessCheck,\n reason: &str,\n size: u64",
621
+ "fn abnormal_check_key(\n agent: &AbnormalWatchAgent,\n liveness: &ProcessCheck,\n fact: Option<&crate::provider::FaultFact>,\n error_recency: ErrorRecency,\n error_observation_key: Option<&str>,\n size: u64",
622
+ "unwrap_or_else(|| size.to_string())",
623
+ ] {
624
+ if src.contains(needle) {
625
+ failures.push(format!("abnormal dedupe/check identity still includes mutable rollout size marker `{needle}`"));
626
+ }
627
+ }
628
+ assert!(
629
+ failures.is_empty(),
630
+ "P0 abnormal contract failed:\n{}",
631
+ failures.join("\n")
632
+ );
633
+ }
634
+
635
+ #[test]
636
+ #[serial(env)]
637
+ fn status_model_uses_effective_codex_source_not_stale_claude_state() {
638
+ let (_env, workspace) = team_case_with_model("p0-status", &[("developer", "gpt-5.6-sol")]);
639
+ let mut state = load_runtime_state(&workspace).unwrap();
640
+ state["agents"]["developer"]["provider"] = json!("codex");
641
+ state["agents"]["developer"]["model"] = json!("claude-opus-4-7");
642
+ save_runtime_state(&workspace, &state).unwrap();
643
+
644
+ let status = team_agent::cli::status_port::status(&workspace, false, true)
645
+ .expect("status --json --detail");
646
+ let agent = status.pointer("/agents/developer").unwrap_or(&Value::Null);
647
+ let model = agent.get("model").and_then(Value::as_str);
648
+ let model_stale = agent.get("model_stale").and_then(Value::as_bool) == Some(true);
649
+ let model_source = agent.get("model_source").and_then(Value::as_str);
650
+ assert!(
651
+ model == Some("gpt-5.6-sol") || model_stale || model_source == Some("role"),
652
+ "P0 provider/model projection: Codex seat must not render stale Claude model as truth; agent={agent} status={status}"
653
+ );
654
+ }
655
+
656
+ fn team_case(tag: &str, workers: &[&str]) -> (hermetic_guard::HermeticTestEnv, PathBuf) {
657
+ let pairs = workers.iter().map(|worker| (*worker, "gpt-5")).collect::<Vec<_>>();
658
+ team_case_with_model(tag, &pairs)
659
+ }
660
+
661
+ fn team_case_with_model(
662
+ tag: &str,
663
+ workers: &[(&str, &str)],
664
+ ) -> (hermetic_guard::HermeticTestEnv, PathBuf) {
665
+ let env = hermetic_guard::HermeticTestEnv::enter(tag);
666
+ env.scrub_tmux();
667
+ env.assert_no_real_tmux();
668
+ let workspace = env.workspace(tag);
669
+ write_team_docs(&workspace, workers);
670
+ write_runtime_spec(&workspace);
671
+ seed_state(&workspace, workers);
672
+ seed_healthy_coordinator(&workspace);
673
+ (env, workspace)
674
+ }
675
+
676
+ fn write_team_docs(workspace: &Path, workers: &[(&str, &str)]) {
677
+ fs::create_dir_all(workspace.join("agents")).unwrap();
678
+ fs::write(workspace.join("TEAM.md"), "---\nname: current\nobjective: P0 host co-tenant contract.\nprovider: codex\n---\n").unwrap();
679
+ for (worker, model) in workers {
680
+ let role = format!("---\nname: {worker}\nrole: {worker}\nprovider: codex\nmodel: {model}\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{worker}.\n");
681
+ fs::write(workspace.join("agents").join(format!("{worker}.md")), role).unwrap();
682
+ }
683
+ }
684
+
685
+ fn write_runtime_spec(workspace: &Path) {
686
+ let spec = team_agent::compiler::compile_team(workspace).unwrap();
687
+ let spec_text = team_agent::model::yaml::dumps(&spec);
688
+ fs::write(workspace.join("team.spec.yaml"), &spec_text).unwrap();
689
+ let path = runtime_spec_path(workspace, TEAM);
690
+ fs::create_dir_all(path.parent().unwrap()).unwrap();
691
+ fs::write(path, spec_text).unwrap();
692
+ }
693
+
694
+ fn seed_state(workspace: &Path, workers: &[(&str, &str)]) {
695
+ let agents = workers
696
+ .iter()
697
+ .map(|(worker, model)| ((*worker).to_string(), json!({
698
+ "status": "running", "provider": "codex", "agent_id": worker, "role": worker,
699
+ "model": model, "auth_mode": "subscription", "tools": ["mcp_team"],
700
+ "window": worker, "pane_id": format!("%old-{worker}"), "pane_pid": 10_000,
701
+ "owner_team_id": TEAM,
702
+ "session_id": format!("sess-{worker}"),
703
+ "rollout_path": workspace.join(format!("{worker}.jsonl")).to_string_lossy(),
704
+ "spawn_epoch": 1, "spawn_cwd": workspace.to_string_lossy()
705
+ })))
706
+ .collect::<serde_json::Map<_, _>>();
707
+ for (worker, _) in workers {
708
+ fs::write(workspace.join(format!("{worker}.jsonl")), "{}\n").unwrap();
709
+ }
710
+ save_runtime_state(workspace, &json!({
711
+ "active_team_key": TEAM, "team_dir": workspace.to_string_lossy(),
712
+ "spec_path": workspace.join("team.spec.yaml").to_string_lossy(),
713
+ "session_name": SESSION, "agents": agents
714
+ })).unwrap();
715
+ }
716
+
717
+ fn seed_healthy_coordinator(workspace: &Path) {
718
+ fs::create_dir_all(runtime_dir(workspace)).unwrap();
719
+ let _ = MessageStore::open(workspace).unwrap();
720
+ let workspace = WorkspacePath::new(workspace.to_path_buf());
721
+ let pid = Pid::new(std::process::id());
722
+ team_agent::coordinator::write_coordinator_metadata(&workspace, pid, MetadataSource::Boot)
723
+ .unwrap();
724
+ fs::write(coordinator_pid_path(&workspace), pid.to_string()).unwrap();
725
+ }
726
+
727
+ fn write_old_codex_rollout(home: &Path, spawn_cwd: &Path) -> PathBuf {
728
+ let dir = home.join(".codex/sessions/2026/01/01");
729
+ fs::create_dir_all(&dir).unwrap();
730
+ let rollout = dir.join("rollout-2026-01-01T00-00-00-old.jsonl");
731
+ let meta = json!({"type": "session_meta", "payload": {
732
+ "id": "old-session-before-spawn", "cwd": spawn_cwd.to_string_lossy(),
733
+ "created_at": "2026-01-01T00:00:00+00:00"
734
+ }});
735
+ fs::write(&rollout, format!("{meta}\n{}\n", json!({"type": "event", "payload": {"message": "old rollout"}}))).unwrap();
736
+ rollout
737
+ }
738
+
739
+ #[derive(Clone)]
740
+ struct CohortTransport {
741
+ state: Arc<Mutex<TransportState>>,
742
+ bad_spawn_identity: bool,
743
+ hide_new_from_list_targets: bool,
744
+ fail_list_targets: bool,
745
+ }
746
+
747
+ struct TransportState {
748
+ panes: Vec<PaneInfo>,
749
+ killed: BTreeSet<String>,
750
+ next: usize,
751
+ current_command: String,
752
+ }
753
+
754
+ impl CohortTransport {
755
+ fn with_old_panes(workers: &[&str]) -> Self {
756
+ let panes = workers.iter().map(|w| pane_info(w, &format!("%old-{w}"), "team-agent-worker")).collect();
757
+ Self {
758
+ state: Arc::new(Mutex::new(TransportState { panes, killed: BTreeSet::new(), next: 1, current_command: "team-agent-worker".to_string() })),
759
+ bad_spawn_identity: false,
760
+ hide_new_from_list_targets: false,
761
+ fail_list_targets: false,
762
+ }
763
+ }
764
+
765
+ fn with_bad_spawn_identity(mut self) -> Self {
766
+ self.bad_spawn_identity = true;
767
+ self
768
+ }
769
+
770
+ fn with_hidden_new_panes(mut self) -> Self {
771
+ self.hide_new_from_list_targets = true;
772
+ self
773
+ }
774
+
775
+ fn with_list_targets_failure(mut self) -> Self {
776
+ self.fail_list_targets = true;
777
+ self
778
+ }
779
+
780
+ fn with_current_command(self, command: &str) -> Self {
781
+ let mut state = self.state.lock().unwrap();
782
+ state.current_command = command.to_string();
783
+ for pane in &mut state.panes { pane.current_command = Some(command.to_string()); }
784
+ drop(state);
785
+ self
786
+ }
787
+
788
+ fn spawn(&self, session: &SessionName, window: &WindowName) -> SpawnResult {
789
+ let mut state = self.state.lock().unwrap();
790
+ let old_identity = state.panes.iter().find(|pane| {
791
+ pane.window_name.as_ref().is_some_and(|name| name == window)
792
+ && pane.pane_id.as_str().starts_with("%old-")
793
+ && !state.killed.contains(pane.pane_id.as_str())
794
+ }).map(|pane| (pane.pane_id.clone(), pane.pane_pid));
795
+ let pane_id = PaneId::new(format!("%new-{}-{}", window.as_str(), state.next));
796
+ let pane_pid = 20_000 + u32::try_from(state.next).unwrap();
797
+ state.next += 1;
798
+ let current_command = state.current_command.clone();
799
+ state.panes.push(pane_info_with_pid(
800
+ window.as_str(),
801
+ pane_id.as_str(),
802
+ current_command.as_str(),
803
+ pane_pid,
804
+ ));
805
+ if self.bad_spawn_identity {
806
+ let (old_pane, old_pid) = old_identity.expect("bad identity fixture needs old pane");
807
+ SpawnResult { pane_id: old_pane, session: session.clone(), window: window.clone(), child_pid: old_pid }
808
+ } else {
809
+ SpawnResult { pane_id, session: session.clone(), window: window.clone(), child_pid: Some(pane_pid) }
810
+ }
811
+ }
812
+
813
+ fn pane_summary(&self) -> Vec<String> {
814
+ self.state.lock().unwrap().panes.iter().map(|p| {
815
+ format!("{}:{}", p.window_name.as_ref().map(WindowName::as_str).unwrap_or("-"), p.pane_id.as_str())
816
+ }).collect()
817
+ }
818
+
819
+ fn window_counts(&self) -> BTreeMap<String, usize> {
820
+ let mut counts = BTreeMap::new();
821
+ let state = self.state.lock().unwrap();
822
+ for pane in state.panes.iter().filter(|pane| !state.killed.contains(pane.pane_id.as_str())) {
823
+ if let Some(window) = pane.window_name.as_ref() { *counts.entry(window.as_str().to_string()).or_insert(0) += 1; }
824
+ }
825
+ counts
826
+ }
827
+
828
+ fn live_panes_for_window(&self, window: &str) -> Vec<PaneInfo> {
829
+ let state = self.state.lock().unwrap();
830
+ state.panes.iter().filter(|pane| {
831
+ !state.killed.contains(pane.pane_id.as_str())
832
+ && pane.window_name.as_ref().is_some_and(|name| name.as_str() == window)
833
+ }).cloned().collect()
834
+ }
835
+ }
836
+
837
+ fn collect_lifecycle_cohort_result(
838
+ failures: &mut Vec<String>,
839
+ label: &str,
840
+ result: Result<(), LifecycleError>,
841
+ transport: &CohortTransport,
842
+ ) {
843
+ let spawned = transport.pane_summary().into_iter().filter(|pane| pane.contains(":%new-")).collect::<Vec<_>>();
844
+ if let Err(error) = result {
845
+ let text = error.to_string();
846
+ if spawned.is_empty()
847
+ && (text.contains("cohort") || text.contains("same-role") || text.contains("duplicate"))
848
+ {
849
+ return;
850
+ }
851
+ failures.push(if spawned.is_empty() {
852
+ format!("{label}: refusal must name cohort/duplicate proof; error={text}")
853
+ } else {
854
+ format!("{label}: refusal happened after spawning {spawned:?}, so it is not a safe pre-spawn cohort/duplicate refusal; error={text}; panes={:?}", transport.pane_summary())
855
+ });
856
+ return;
857
+ }
858
+ let duplicates = transport.window_counts().into_iter().filter(|(_, count)| *count != 1).map(|(window, count)| format!("{window}={count}")).collect::<Vec<_>>();
859
+ if !duplicates.is_empty() {
860
+ failures.push(format!(
861
+ "{label}: duplicate live tuple(s) {duplicates:?}; panes={:?}",
862
+ transport.pane_summary()
863
+ ));
864
+ }
865
+ }
866
+
867
+ impl Transport for CohortTransport {
868
+ fn kind(&self) -> BackendKind { BackendKind::Tmux }
869
+
870
+ fn tmux_endpoint(&self) -> Option<String> { Some(TMUX_ENDPOINT.to_string()) }
871
+
872
+ fn spawn_first(
873
+ &self,
874
+ session: &SessionName,
875
+ window: &WindowName,
876
+ _argv: &[String],
877
+ _cwd: &Path,
878
+ _env: &BTreeMap<String, String>,
879
+ ) -> Result<SpawnResult, TransportError> {
880
+ Ok(self.spawn(session, window))
881
+ }
882
+
883
+ fn spawn_into(&self, session: &SessionName, window: &WindowName, argv: &[String], cwd: &Path, env: &BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
884
+ self.spawn_first(session, window, argv, cwd, env)
885
+ }
886
+
887
+ fn inject(&self, _target: &Target, _payload: &InjectPayload, _submit: Key, _bracketed: bool) -> Result<InjectReport, TransportError> {
888
+ Ok(InjectReport { stage_reached: InjectStage::Submit, inject_verification: InjectVerification::CaptureContainsToken, submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck, turn_verification: TurnVerification::NotRequired, attempts: 1, submit_diagnostics: None })
889
+ }
890
+
891
+ fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> { Ok(()) }
892
+
893
+ fn capture(&self, _target: &Target, range: CaptureRange) -> Result<CapturedText, TransportError> {
894
+ Ok(CapturedText { text: "wrapper shell prompt without team-agent provider exit marker".to_string(), range })
895
+ }
896
+
897
+ fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
898
+ Ok(match field {
899
+ PaneField::PaneId => Some("%query".to_string()),
900
+ PaneField::SessionName => Some(SESSION.to_string()),
901
+ PaneField::PaneCurrentCommand => Some(self.state.lock().unwrap().current_command.clone()),
902
+ _ => None,
903
+ })
904
+ }
905
+
906
+ fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
907
+ Ok(if self.has_pane(pane)? == Some(true) { PaneLiveness::Live } else { PaneLiveness::Dead })
908
+ }
909
+
910
+ fn has_pane(&self, pane: &PaneId) -> Result<Option<bool>, TransportError> {
911
+ let state = self.state.lock().unwrap();
912
+ Ok(Some(!state.killed.contains(pane.as_str()) && state.panes.iter().any(|item| item.pane_id == *pane)))
913
+ }
914
+
915
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
916
+ if self.fail_list_targets {
917
+ return Err(TransportError::MuxUnavailable {
918
+ backend: BackendKind::Tmux,
919
+ detail: "injected list-targets failure".to_string(),
920
+ });
921
+ }
922
+ let state = self.state.lock().unwrap();
923
+ Ok(state.panes.iter().filter(|pane| {
924
+ !self.hide_new_from_list_targets || !pane.pane_id.as_str().starts_with("%new-")
925
+ }).cloned().collect())
926
+ }
927
+
928
+ fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> { Ok(true) }
929
+
930
+ fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
931
+ Ok(self.state.lock().unwrap().panes.iter().filter_map(|pane| pane.window_name.clone()).collect::<BTreeSet<_>>().into_iter().collect())
932
+ }
933
+
934
+ fn set_session_env(&self, _session: &SessionName, _key: &str, _value: &str) -> Result<SetEnvOutcome, TransportError> { Ok(SetEnvOutcome::Applied) }
935
+
936
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> { Ok(()) }
937
+
938
+ fn kill_window(&self, target: &Target) -> Result<(), TransportError> {
939
+ if let Target::Pane(pane) = target { self.kill_pane(pane)?; }
940
+ Ok(())
941
+ }
942
+
943
+ fn kill_pane(&self, pane: &PaneId) -> Result<(), TransportError> {
944
+ self.state.lock().unwrap().killed.insert(pane.as_str().to_string());
945
+ Ok(())
946
+ }
947
+
948
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> { Ok(AttachOutcome::Attached) }
949
+ }
950
+
951
+ fn pane_info(window: &str, pane: &str, current_command: &str) -> PaneInfo {
952
+ pane_info_with_pid(window, pane, current_command, 10_000)
953
+ }
954
+
955
+ fn pane_info_with_pid(window: &str, pane: &str, current_command: &str, pane_pid: u32) -> PaneInfo {
956
+ PaneInfo {
957
+ pane_id: PaneId::new(pane),
958
+ session: SessionName::new(SESSION),
959
+ window_name: Some(WindowName::new(window)),
960
+ window_index: None,
961
+ pane_index: None, tty: None,
962
+ current_command: Some(current_command.to_string()),
963
+ current_path: None, active: true, pane_pid: Some(pane_pid),
964
+ leader_env: BTreeMap::new(),
965
+ }
966
+ }
967
+
968
+ struct RealAdapterRegistry;
969
+
970
+ impl ProviderRegistry for RealAdapterRegistry {
971
+ fn adapter_for(&self, provider: Provider) -> Box<dyn ProviderAdapter> {
972
+ get_adapter(provider)
973
+ }
974
+ }
975
+
976
+ fn source_section<'a>(src: &'a str, start: &str, end: &str) -> &'a str { let start = src.find(start).unwrap_or(0); let rest = &src[start..]; &rest[..rest.find(end).unwrap_or(rest.len())] }
977
+
978
+ fn reset_success_json_object(reset_src: &str) -> &str {
979
+ let success = source_section(reset_src, "ResetAgentOutcome::Reset", "ResetAgentOutcome::Refused");
980
+ let macro_start = success.find("serde_json::json!({").expect("reset success json object"); let object_start = macro_start + success[macro_start..].find('{').unwrap(); let mut depth = 0_i32;
981
+ for (offset, ch) in success[object_start..].char_indices() {
982
+ if ch == '{' { depth += 1; } if ch == '}' { depth -= 1; }
983
+ if depth == 0 { return &success[object_start..object_start + offset + 1]; }
984
+ }
985
+ panic!("unterminated reset success json object")
986
+ }