@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,737 @@
1
+ //! agent-clone-fork · successor RED batch 4 (verifier) — R8 Copilot fork
2
+ //! (caps re-charter) + R7 permission-clamp GUARDRAIL.
3
+ //!
4
+ //! From locate.md §7 R7/R8 + §1.4 (Copilot 1.0.61 real-machine proof: directory
5
+ //! copy + SQLite transaction under COPILOT_HOME is feasible) + §3 surface
6
+ //! (`provider/adapter.rs` Copilot caps=false; `provider/session_scan/copilot.rs`
7
+ //! hard-reads `$HOME/.copilot`; `fork_agent.rs:173-288` provider/permission
8
+ //! inheritance) + MUST-16, NOT the §4 design. Baseline `9e6be51` (v0.5.52).
9
+ //!
10
+ //! - **R8 Copilot fork caps RE-CHARTER (RED + verifier signature)**: baseline
11
+ //! locks "Copilot fork MUST be refused (capability unsupported)"
12
+ //! (`copilot_provider_red.rs::copilot_fork_returns_structured_capability_unsupported`).
13
+ //! §1.4 proves Copilot CAN fork (directory copy + atomic SQLite transaction
14
+ //! under an isolated COPILOT_HOME). This batch RED asserts that a Copilot fork
15
+ //! is no longer rejected by a blanket capability gate. Baseline red: the fork
16
+ //! is refused with a copilot/fork capability-unsupported error. Retiring the old
17
+ //! "must refuse" contract is a re-charter that REQUIRES a verifier signature —
18
+ //! recorded in
19
+ //! `.team/artifacts/pipeline-runs/agent-clone-fork/verifier-r8-caps-recharter-signoff.md`.
20
+ //! (The SQLite foreign-key table integrity + exact-nonce recall are the GREEN
21
+ //! half / subscription-E2E per §1.4 — not asserted offline here.)
22
+ //!
23
+ //! - **R7 permission-clamp GUARDRAIL (naturally-green, not a RED, per L1 r2)**: a
24
+ //! fork's effective approval policy must be derived from the LEADER
25
+ //! (`source=leader_process`, `worker_capability_above_leader=false`), never a
26
+ //! raw clone of a source-declared capability above the leader (MUST-16).
27
+ //! Measured: baseline already derives approval from the leader and marks
28
+ //! `worker_capability_above_leader=false`, so there is no offline structural
29
+ //! red. Following the L1 r2 naturally-green-guardrail precedent, this pins the
30
+ //! clamp as a guardrail whose teeth engage after implement recompiles the role
31
+ //! via RoleSourceResolver (§4.2): any impl that raw-clones a compiled permission
32
+ //! above the leader trips it on the spot. R7's escalation scenario (a source
33
+ //! role declaring capability above the leader) cannot be constructed offline —
34
+ //! the role model forces `permission_mode=restricted` and inherits approval from
35
+ //! the leader — so the true escalation guard is a subscription/real-machine
36
+ //! concern; the guardrail defends the invariant that already holds.
37
+ //!
38
+ //! R9 clone-semantics is NOT frozen in this batch: offline, `clone-agent` does not
39
+ //! exist (already RED in batch1 R10), and its distinguishing semantics
40
+ //! (fresh-session-distinct-from-fork, carries no source nonce) are subscription
41
+ //! real-machine concerns (locate §4.5, §5 obs 6). It is recorded against R10 +
42
+ //! the E2E gate, not asserted offline here.
43
+
44
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
45
+
46
+ use std::collections::BTreeMap;
47
+ use std::path::{Path, PathBuf};
48
+ use std::process::Output;
49
+ use std::sync::Mutex;
50
+
51
+ use serde_json::{json, Value};
52
+ use team_agent::lifecycle::launch::fork_agent_with_transport;
53
+ use team_agent::lifecycle::quick_start_with_transport_in_workspace;
54
+ use team_agent::model::ids::AgentId;
55
+ use team_agent::state::persist::{load_runtime_state, save_runtime_state};
56
+ use team_agent::transport::{
57
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
58
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
59
+ SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
60
+ TurnVerification, WindowName,
61
+ };
62
+
63
+ #[path = "../../../tests/support/hermetic.rs"]
64
+ mod hermetic_guard;
65
+ use hermetic_guard::HermeticTestEnv;
66
+
67
+ const TEAM_NAME: &str = "cf-batch4";
68
+ const SOURCE: &str = "cp_worker";
69
+ const NEW: &str = "cp_fork";
70
+
71
+ /// Gate6 CI-hermeticity revision: the R7 fixture must pin the process-ancestry
72
+ /// seam explicitly (`TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON`) instead of
73
+ /// depending on the REAL ancestry of the test-runner host. Unpinned, a local
74
+ /// runner descending from a dangerous-bypass leader yields
75
+ /// `source=leader_process` (historical local green) while a clean CI runner
76
+ /// correctly yields `source=disabled` — a non-hermetic assertion, not a product
77
+ /// regression. Precedent: `codex_mcp_approval_inheritance_red.rs` `MockAncestry`
78
+ /// (pins both bypass and restricted leader argv).
79
+ const ANCESTRY_ENV: &str = "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON";
80
+ const CODEX_BYPASS: &str = "--dangerously-bypass-approvals-and-sandbox";
81
+ const RESTRICTED_LEADER_ARGV: &[&str] = &[
82
+ "codex",
83
+ "--sandbox",
84
+ "read-only",
85
+ "--ask-for-approval",
86
+ "on-request",
87
+ ];
88
+
89
+ struct Case {
90
+ env: HermeticTestEnv,
91
+ workspace: PathBuf,
92
+ shim_path: String,
93
+ ancestry_json: String,
94
+ socket: Option<PathBuf>,
95
+ }
96
+
97
+ impl Case {
98
+ /// R7 (permission-clamp guardrail) uses a claude source with a success-path
99
+ /// backing shim so the fork actually produces a NEW row whose permission clamp
100
+ /// can be inspected — without a real fork the guardrail would read no row and
101
+ /// pass vacuously. (R8 does not use Case: it exercises the library entry with
102
+ /// an in-process RecordingTransport — see `r8_...`.)
103
+ fn start(tag: &str) -> Self {
104
+ Self::start_with_ancestry(tag, &["codex", CODEX_BYPASS])
105
+ }
106
+
107
+ /// Every CLI invocation of this Case pins `ancestry_argv` through the
108
+ /// product's existing test seam so the derived approval policy is a
109
+ /// function of the FIXTURE, never of the host process tree.
110
+ fn start_with_ancestry(tag: &str, ancestry_argv: &[&str]) -> Self {
111
+ let env = HermeticTestEnv::enter(tag);
112
+ let workspace = env.workspace("ws");
113
+ write_team_docs(&workspace, "claude");
114
+ let shim_dir = write_claude_backing_shim(&workspace);
115
+ let shim_path = format!(
116
+ "{}:{}",
117
+ shim_dir.display(),
118
+ std::env::var("PATH").expect("PATH present")
119
+ );
120
+ let mut case = Self {
121
+ env,
122
+ workspace,
123
+ shim_path,
124
+ ancestry_json: serde_json::to_string(ancestry_argv).expect("ancestry argv json"),
125
+ socket: None,
126
+ };
127
+ case.quick_start();
128
+ case.seed_source_session_tuple();
129
+ case
130
+ }
131
+
132
+ fn ws(&self) -> &str {
133
+ self.workspace.to_str().expect("ws utf8")
134
+ }
135
+
136
+ fn state_path(&self) -> PathBuf {
137
+ self.workspace
138
+ .join(".team")
139
+ .join("runtime")
140
+ .join("state.json")
141
+ }
142
+
143
+ fn run(&self, args: &[&str]) -> Output {
144
+ self.env.run_cli_env(
145
+ &self.workspace,
146
+ args,
147
+ &[
148
+ ("PATH", self.shim_path.as_str()),
149
+ (ANCESTRY_ENV, self.ancestry_json.as_str()),
150
+ ],
151
+ )
152
+ }
153
+
154
+ fn quick_start(&mut self) {
155
+ let out = self.run(&[
156
+ "quick-start",
157
+ self.ws(),
158
+ "--workspace",
159
+ self.ws(),
160
+ "--name",
161
+ TEAM_NAME,
162
+ "--yes",
163
+ "--json",
164
+ ]);
165
+ if let Ok(v) = serde_json::from_slice::<Value>(&out.stdout) {
166
+ if let Some(cmd) = v
167
+ .get("attach_commands")
168
+ .and_then(|a| a.as_array())
169
+ .and_then(|a| a.first())
170
+ .and_then(|c| c.as_str())
171
+ .or_else(|| v.get("leader_attach_command").and_then(|c| c.as_str()))
172
+ {
173
+ let toks: Vec<&str> = cmd.split_whitespace().collect();
174
+ if let Some(i) = toks.iter().position(|t| *t == "-S") {
175
+ if let Some(sock) = toks.get(i + 1) {
176
+ self.socket = Some(PathBuf::from(*sock));
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ /// Seed the SOURCE session tuple so the fork reaches the capability gate
184
+ /// (not a missing-session gate) — mirrors the old refusal contract's
185
+ /// `seed_session_id`, which existed precisely so "the capability gate is what
186
+ /// fires".
187
+ fn seed_source_session_tuple(&self) {
188
+ // A real rollout file the source tuple points at.
189
+ let rollout_file = self.workspace.join("fixture-source-rollout.jsonl");
190
+ let _ = std::fs::write(&rollout_file, "{\"type\":\"fixture-source\"}\n");
191
+ let Ok(raw) = std::fs::read_to_string(self.state_path()) else {
192
+ return;
193
+ };
194
+ let Ok(mut state) = serde_json::from_str::<Value>(&raw) else {
195
+ return;
196
+ };
197
+ let tuple = json!({
198
+ "session_id": "sess-cf-batch4-source",
199
+ "rollout_path": rollout_file.to_string_lossy(),
200
+ "captured_at": "2026-07-21T00:00:00Z",
201
+ "captured_via": "contract-fixture"
202
+ });
203
+ let mut patch = |row: &mut Value| {
204
+ if let Some(obj) = row.as_object_mut() {
205
+ for (k, v) in tuple.as_object().unwrap() {
206
+ obj.insert(k.clone(), v.clone());
207
+ }
208
+ }
209
+ };
210
+ if let Some(row) = state.get_mut("agents").and_then(|a| a.get_mut(SOURCE)) {
211
+ patch(row);
212
+ }
213
+ if let Some(teams) = state.get_mut("teams").and_then(Value::as_object_mut) {
214
+ for team in teams.values_mut() {
215
+ if let Some(row) = team.get_mut("agents").and_then(|a| a.get_mut(SOURCE)) {
216
+ patch(row);
217
+ }
218
+ }
219
+ }
220
+ let _ = std::fs::write(
221
+ self.state_path(),
222
+ serde_json::to_string_pretty(&state).unwrap(),
223
+ );
224
+ }
225
+
226
+ fn fork(&self, as_name: &str) -> Value {
227
+ let out = self.run(&[
228
+ "fork-agent",
229
+ SOURCE,
230
+ "--as",
231
+ as_name,
232
+ "--workspace",
233
+ self.ws(),
234
+ "--team",
235
+ TEAM_NAME,
236
+ "--no-display",
237
+ "--json",
238
+ ]);
239
+ let stderr = String::from_utf8_lossy(&out.stderr).to_string();
240
+ serde_json::from_slice::<Value>(&out.stdout).unwrap_or_else(|_| {
241
+ json!({
242
+ "ok": out.status.success(),
243
+ "_exit": out.status.code(),
244
+ "_stderr": stderr,
245
+ })
246
+ })
247
+ }
248
+
249
+ fn team_agent_rows(&self) -> serde_json::Map<String, Value> {
250
+ let Ok(raw) = std::fs::read_to_string(self.state_path()) else {
251
+ return serde_json::Map::new();
252
+ };
253
+ let Ok(v) = serde_json::from_str::<Value>(&raw) else {
254
+ return serde_json::Map::new();
255
+ };
256
+ if let Some(teams) = v.get("teams").and_then(Value::as_object) {
257
+ for team in teams.values() {
258
+ if let Some(agents) = team.get("agents").and_then(Value::as_object) {
259
+ if agents.contains_key(SOURCE) {
260
+ return agents.clone();
261
+ }
262
+ }
263
+ }
264
+ }
265
+ serde_json::Map::new()
266
+ }
267
+ }
268
+
269
+ impl Drop for Case {
270
+ fn drop(&mut self) {
271
+ if let Some(sock) = &self.socket {
272
+ let _ = std::process::Command::new("tmux")
273
+ .args(["-S", sock.to_str().unwrap_or(""), "kill-server"])
274
+ .output();
275
+ }
276
+ let ws = self.workspace.to_string_lossy().to_string();
277
+ if let Ok(out) = std::process::Command::new("ps")
278
+ .args(["-axo", "pid=,command="])
279
+ .output()
280
+ {
281
+ for line in String::from_utf8_lossy(&out.stdout).lines() {
282
+ if line.contains(&ws) {
283
+ if let Some(pid) = line.split_whitespace().next() {
284
+ let _ = std::process::Command::new("kill")
285
+ .args(["-TERM", pid])
286
+ .output();
287
+ }
288
+ }
289
+ }
290
+ }
291
+ let _ = &self.env;
292
+ }
293
+ }
294
+
295
+ fn write_team_docs(workspace: &Path, provider: &str) {
296
+ std::fs::create_dir_all(workspace.join("agents")).expect("create agents dir");
297
+ std::fs::write(
298
+ workspace.join("TEAM.md"),
299
+ format!(
300
+ "---\nname: {TEAM_NAME}\nobjective: clone-fork batch4.\nprovider: {provider}\n---\n"
301
+ ),
302
+ )
303
+ .expect("write TEAM.md");
304
+ std::fs::write(
305
+ workspace.join("agents").join(format!("{SOURCE}.md")),
306
+ format!(
307
+ "---\nname: {SOURCE}\nrole: {SOURCE}\nprovider: {provider}\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{SOURCE} body.\n"
308
+ ),
309
+ )
310
+ .expect("write source role doc");
311
+ }
312
+
313
+ /// Copilot team docs for the library-entry R8 (a separate agents dir so the
314
+ /// OfflineTransport quick-start compiles a copilot source).
315
+ fn write_copilot_team_docs(workspace: &Path) -> PathBuf {
316
+ let team_dir = workspace.join("cf-r8-team");
317
+ std::fs::create_dir_all(team_dir.join("agents")).expect("create team agents dir");
318
+ std::fs::write(
319
+ team_dir.join("TEAM.md"),
320
+ format!("---\nname: {TEAM_NAME}\nobjective: clone-fork batch4 copilot recharter.\nprovider: copilot\n---\n\nTeam fixture.\n"),
321
+ )
322
+ .expect("write TEAM.md");
323
+ std::fs::write(
324
+ team_dir.join("agents").join(format!("{SOURCE}.md")),
325
+ format!(
326
+ "---\nname: {SOURCE}\nrole: Copilot Worker\nprovider: copilot\nmodel: gpt-5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nCopilot source body.\n"
327
+ ),
328
+ )
329
+ .expect("write source role doc");
330
+ team_dir
331
+ }
332
+
333
+ /// Seed the SOURCE's complete session tuple directly in runtime state (library
334
+ /// path) so the copilot fork reaches the capability gate, not a missing-session
335
+ /// gate. Mirrors the old refusal contract's `seed_session_id`.
336
+ fn seed_source_tuple_in_state(workspace: &Path, agent: &str) {
337
+ let mut state = load_runtime_state(workspace).expect("load runtime state");
338
+ let source_session_id = "99999999-aaaa-4bbb-8ccc-dddddddddddd";
339
+ let copilot_home =
340
+ PathBuf::from(std::env::var_os("HOME").expect("hermetic HOME")).join(".copilot");
341
+ let source_dir = copilot_home.join("session-state").join(source_session_id);
342
+ std::fs::create_dir_all(&source_dir).expect("create Copilot source state");
343
+ std::fs::write(
344
+ source_dir.join("context.json"),
345
+ format!("{{\"session_id\":\"{source_session_id}\"}}\n"),
346
+ )
347
+ .expect("write Copilot source state");
348
+ let store = rusqlite::Connection::open(copilot_home.join("session-store.db"))
349
+ .expect("open Copilot source store");
350
+ store
351
+ .execute_batch(
352
+ "create table sessions (id text primary key);
353
+ create table turns (session_id text);
354
+ create table checkpoints (session_id text);
355
+ create table session_files (session_id text);
356
+ create table session_refs (session_id text);
357
+ create table forge_trajectory_events (session_id text);
358
+ create table search_index (session_id text);",
359
+ )
360
+ .expect("create Copilot source schema");
361
+ store
362
+ .execute(
363
+ "insert into sessions(id) values (?1)",
364
+ rusqlite::params![source_session_id],
365
+ )
366
+ .expect("seed Copilot source session");
367
+ drop(store);
368
+ let tuple = json!({
369
+ "status": "running",
370
+ "provider": "copilot",
371
+ "session_id": source_session_id,
372
+ "rollout_path": copilot_home.join("session-store.db").to_string_lossy(),
373
+ "captured_at": "2026-07-21T00:00:00Z",
374
+ "captured_via": "contract-fixture",
375
+ });
376
+ let apply = |obj: &mut serde_json::Map<String, Value>| {
377
+ for (k, v) in tuple.as_object().unwrap() {
378
+ obj.insert(k.clone(), v.clone());
379
+ }
380
+ };
381
+ if let Some(obj) = state
382
+ .pointer_mut(&format!("/agents/{agent}"))
383
+ .and_then(Value::as_object_mut)
384
+ {
385
+ apply(obj);
386
+ }
387
+ if let Some(teams) = state.get_mut("teams").and_then(Value::as_object_mut) {
388
+ for team in teams.values_mut() {
389
+ if let Some(obj) = team
390
+ .get_mut("agents")
391
+ .and_then(|a| a.get_mut(agent))
392
+ .and_then(Value::as_object_mut)
393
+ {
394
+ apply(obj);
395
+ }
396
+ }
397
+ }
398
+ save_runtime_state(workspace, &state).expect("save seeded state");
399
+ }
400
+
401
+ /// Claude success-path shim producing a NEW backing keyed to the predetermined
402
+ /// `--session-id` (same mechanism as batch3): the R7 fork must actually complete
403
+ /// so a NEW state row exists to inspect for the permission clamp — otherwise the
404
+ /// guardrail reads no row and passes vacuously (false-green-hole type 8).
405
+ fn write_claude_backing_shim(workspace: &Path) -> PathBuf {
406
+ let bin_dir = workspace.join("shim-bin");
407
+ std::fs::create_dir_all(&bin_dir).expect("create shim dir");
408
+ let shim = bin_dir.join("claude");
409
+ std::fs::write(
410
+ &shim,
411
+ r#"#!/bin/sh
412
+ sid=""
413
+ prev=""
414
+ for a in "$@"; do
415
+ if [ "$prev" = "--session-id" ]; then sid="$a"; fi
416
+ prev="$a"
417
+ done
418
+ if [ -n "$sid" ]; then
419
+ enc=$(printf '%s' "$PWD" | sed 's/[^a-zA-Z0-9]/-/g')
420
+ dir="$HOME/.claude/projects/$enc"
421
+ mkdir -p "$dir"
422
+ printf '{"sessionId":"%s","type":"fork-backing"}\n' "$sid" > "$dir/$sid.jsonl"
423
+ fi
424
+ echo 'claude shim ready'
425
+ exec sleep 3600
426
+ "#,
427
+ )
428
+ .expect("write claude shim");
429
+ #[cfg(unix)]
430
+ {
431
+ use std::os::unix::fs::PermissionsExt;
432
+ std::fs::set_permissions(&shim, std::fs::Permissions::from_mode(0o755))
433
+ .expect("chmod claude shim");
434
+ }
435
+ bin_dir
436
+ }
437
+
438
+ /// R8 Copilot fork caps RE-CHARTER — a Copilot fork must no longer be refused by a
439
+ /// blanket capability gate (§1.4 proves the directory-copy + SQLite-transaction
440
+ /// primitive is feasible under an isolated COPILOT_HOME). Baseline red: the fork
441
+ /// is refused with a copilot/fork capability-unsupported error.
442
+ ///
443
+ /// Retiring the old "Copilot fork must be refused" contract is a re-charter
444
+ /// requiring a verifier signature — see
445
+ /// `verifier-r8-caps-recharter-signoff.md`. The SQLite FK-table integrity and
446
+ /// exact-nonce recall remain the GREEN half / subscription-E2E (§1.4).
447
+ #[test]
448
+ fn r8_copilot_fork_is_no_longer_capability_refused() {
449
+ // R8 uses the LIBRARY entry `fork_agent_with_transport` (the same entry the old
450
+ // refusal contract exercises), because the CLI `fork-agent` path reports
451
+ // ok:true for a copilot source (registration-only success) and never reaches
452
+ // the capability gate — a CLI-shaped R8 could never be RED. An in-process
453
+ // RecordingTransport stands the team up without a live copilot TUI (no
454
+ // ready-screen hang).
455
+ let env = HermeticTestEnv::enter("cf-r8");
456
+ let workspace = env.workspace("ws");
457
+ let team_dir = write_copilot_team_docs(&workspace);
458
+ // Session ABSENT at quick-start so it can create the team session; spawn_first
459
+ // flips session_present true, so the later fork sees a live session. (Starting
460
+ // with session_present=true causes a SessionConflict at quick-start.)
461
+ let transport = RecordingTransport::default();
462
+ quick_start_with_transport_in_workspace(
463
+ &workspace,
464
+ &team_dir,
465
+ Some(TEAM_NAME),
466
+ true,
467
+ Some("cfr8"),
468
+ &transport,
469
+ )
470
+ .expect("quick-start seeds the copilot team");
471
+ // Seed the source's complete session tuple so the fork reaches the capability
472
+ // gate (not a missing-session gate) — mirrors the old contract's seed.
473
+ seed_source_tuple_in_state(&workspace, SOURCE);
474
+
475
+ // The team KEY is the quick-start team_id ("cfr8"), not the display name.
476
+ let result = fork_agent_with_transport(
477
+ &workspace,
478
+ &AgentId::new(SOURCE),
479
+ &AgentId::new(NEW),
480
+ None,
481
+ false,
482
+ Some("cfr8"),
483
+ &transport,
484
+ );
485
+
486
+ // Baseline: copilot fork is refused with a structured copilot/fork
487
+ // capability-unsupported error (copilot_provider_red.rs C-4-2/C-4-3). R8 pins
488
+ // that this blanket capability refusal is GONE post-recharter.
489
+ let text = format!("{result:?}").to_lowercase();
490
+ let capability_refused = result.is_err()
491
+ && (text.contains("unsupported")
492
+ || text.contains("capability")
493
+ || (text.contains("copilot") && text.contains("fork")));
494
+ assert!(
495
+ !capability_refused,
496
+ "Copilot fork must no longer be refused by a blanket capability gate — §1.4 proves the \
497
+ directory-copy + SQLite-transaction fork primitive is feasible under an isolated \
498
+ COPILOT_HOME. Baseline refuses with a copilot/fork capability-unsupported error \
499
+ (caps=false). This re-charter is verifier-signed (verifier-r8-caps-recharter-signoff.md). \
500
+ result={result:?}"
501
+ );
502
+ }
503
+
504
+ /// R7 permission-clamp GUARDRAIL — a fork's effective approval policy is derived
505
+ /// from the LEADER and never escalates above it (MUST-16). Naturally green at
506
+ /// baseline (the role model forces permission_mode=restricted and inherits
507
+ /// approval from the leader); its teeth engage after implement recompiles the
508
+ /// role via RoleSourceResolver — any raw clone of a compiled permission above the
509
+ /// leader trips it. The true escalation scenario (a source declaring capability
510
+ /// above the leader) is a real-machine concern.
511
+ #[test]
512
+ fn r7_fork_effective_permissions_are_clamped_to_leader_guardrail() {
513
+ // Claude source + success-path backing shim so the fork COMPLETES and a NEW
514
+ // row exists to inspect — no vacuous pass on a refused fork. The dangerous
515
+ // leader is established by the PINNED ancestry seam (see ANCESTRY_ENV note):
516
+ // `source=leader_process` is only correct when a dangerous ancestor is
517
+ // actually detected, so the fixture must supply one instead of borrowing
518
+ // the host's.
519
+ let case = Case::start("cf-r7");
520
+ let fork = case.fork(NEW);
521
+ assert_eq!(
522
+ fork.get("ok").and_then(Value::as_bool),
523
+ Some(true),
524
+ "R7 fixture precondition: the claude fork must complete so a NEW row exists to check the \
525
+ permission clamp (no vacuous guardrail). fork={fork}"
526
+ );
527
+
528
+ let rows = case.team_agent_rows();
529
+ let new_row = rows.get(NEW).unwrap_or_else(|| {
530
+ panic!(
531
+ "R7 fixture precondition: NEW fork row must exist to inspect the clamp; rows={rows:?}"
532
+ )
533
+ });
534
+ let policy = new_row.get("effective_approval_policy").unwrap_or_else(|| {
535
+ panic!("NEW fork row must carry an effective_approval_policy; row={new_row}")
536
+ });
537
+ let source = policy.get("source").and_then(Value::as_str);
538
+ let above = policy
539
+ .get("worker_capability_above_leader")
540
+ .and_then(Value::as_bool);
541
+ assert_eq!(
542
+ source,
543
+ Some("leader_process"),
544
+ "guardrail: a fork's effective approval policy must be derived from the leader \
545
+ (source=leader_process), not raw-cloned from the source; MUST-16. policy={policy}"
546
+ );
547
+ assert_ne!(
548
+ above,
549
+ Some(true),
550
+ "guardrail: a fork must never carry a capability above the leader \
551
+ (worker_capability_above_leader must not be true); MUST-16. policy={policy}"
552
+ );
553
+ }
554
+
555
+ /// R7b reverse positive control — with a pinned RESTRICTED leader ancestry the
556
+ /// fork's policy must resolve to `source=disabled` (fail-closed), never
557
+ /// `leader_process`, and still never escalate. This is the CI-truth face of the
558
+ /// clamp (a clean runner == no dangerous ancestor) and proves r7 does not pass
559
+ /// by constant behavior: the same fixture flips leader_process↔disabled purely
560
+ /// on the pinned seam. Mirrors the deterministic product contract
561
+ /// `lifecycle/tests/core.rs` (clean mocked ancestry → Disabled).
562
+ ///
563
+ /// NOTE deliberately NOT asserted with the seam UNSET: unpinned, the policy is
564
+ /// a function of the host process tree (the exact non-hermeticity this
565
+ /// revision removes), so an unset-seam assertion would be red on any host
566
+ /// whose own leader runs dangerously.
567
+ #[test]
568
+ fn r7b_fork_policy_under_restricted_leader_ancestry_is_disabled() {
569
+ let case = Case::start_with_ancestry("cf-r7b", RESTRICTED_LEADER_ARGV);
570
+ let fork = case.fork(NEW);
571
+ assert_eq!(
572
+ fork.get("ok").and_then(Value::as_bool),
573
+ Some(true),
574
+ "R7b fixture precondition: the claude fork must complete so a NEW row exists to check the \
575
+ policy (no vacuous control). fork={fork}"
576
+ );
577
+
578
+ let rows = case.team_agent_rows();
579
+ let new_row = rows.get(NEW).unwrap_or_else(|| {
580
+ panic!(
581
+ "R7b fixture precondition: NEW fork row must exist to inspect the policy; rows={rows:?}"
582
+ )
583
+ });
584
+ let policy = new_row.get("effective_approval_policy").unwrap_or_else(|| {
585
+ panic!("NEW fork row must carry an effective_approval_policy; row={new_row}")
586
+ });
587
+ assert_eq!(
588
+ policy.get("source").and_then(Value::as_str),
589
+ Some("disabled"),
590
+ "reverse control: a restricted/clean leader ancestry must resolve to source=disabled \
591
+ (fail-closed), never leader_process. policy={policy}"
592
+ );
593
+ assert_ne!(
594
+ policy
595
+ .get("worker_capability_above_leader")
596
+ .and_then(Value::as_bool),
597
+ Some(true),
598
+ "reverse control: a restricted leader must never yield a fork above the leader; MUST-16. \
599
+ policy={policy}"
600
+ );
601
+ }
602
+
603
+ // ---------------------------------------------------------------------------
604
+ // Minimal in-process Transport for the R8 library-entry test (mirrors the
605
+ // RecordingTransport the existing copilot refusal contract uses). Kept local so
606
+ // R8 exercises the real fork lifecycle capability gate without a live copilot TUI.
607
+ // ---------------------------------------------------------------------------
608
+
609
+ #[derive(Clone, Debug)]
610
+ struct RecordedSpawn {
611
+ argv: Vec<String>,
612
+ env: BTreeMap<String, String>,
613
+ window: String,
614
+ }
615
+
616
+ #[derive(Default)]
617
+ struct RecordingTransport {
618
+ spawns: Mutex<Vec<RecordedSpawn>>,
619
+ session_present: Mutex<bool>,
620
+ }
621
+
622
+ impl Transport for RecordingTransport {
623
+ fn kind(&self) -> BackendKind {
624
+ BackendKind::Tmux
625
+ }
626
+
627
+ fn spawn_first(
628
+ &self,
629
+ session: &SessionName,
630
+ window: &WindowName,
631
+ argv: &[String],
632
+ _cwd: &Path,
633
+ env: &BTreeMap<String, String>,
634
+ ) -> Result<SpawnResult, TransportError> {
635
+ let mut spawns = self.spawns.lock().unwrap();
636
+ spawns.push(RecordedSpawn {
637
+ argv: argv.to_vec(),
638
+ env: env.clone(),
639
+ window: window.as_str().to_string(),
640
+ });
641
+ *self.session_present.lock().unwrap() = true;
642
+ Ok(SpawnResult {
643
+ pane_id: PaneId::new(format!("%{}", spawns.len())),
644
+ session: session.clone(),
645
+ window: window.clone(),
646
+ child_pid: Some(30_000 + spawns.len() as u32),
647
+ })
648
+ }
649
+
650
+ fn spawn_into(
651
+ &self,
652
+ session: &SessionName,
653
+ window: &WindowName,
654
+ argv: &[String],
655
+ cwd: &Path,
656
+ env: &BTreeMap<String, String>,
657
+ ) -> Result<SpawnResult, TransportError> {
658
+ self.spawn_first(session, window, argv, cwd, env)
659
+ }
660
+
661
+ fn inject(
662
+ &self,
663
+ _target: &Target,
664
+ _payload: &InjectPayload,
665
+ _submit: Key,
666
+ _bracketed: bool,
667
+ ) -> Result<InjectReport, TransportError> {
668
+ Ok(InjectReport {
669
+ stage_reached: InjectStage::Submit,
670
+ inject_verification: InjectVerification::CaptureContainsToken,
671
+ submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
672
+ turn_verification: TurnVerification::NotYetObserved,
673
+ attempts: 1,
674
+ submit_diagnostics: None,
675
+ })
676
+ }
677
+
678
+ fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
679
+ Ok(())
680
+ }
681
+
682
+ fn capture(
683
+ &self,
684
+ _target: &Target,
685
+ range: CaptureRange,
686
+ ) -> Result<CapturedText, TransportError> {
687
+ Ok(CapturedText {
688
+ text: "Claude Code\n> \nOpenAI Codex\ncodex>\nCopilot".to_string(),
689
+ range,
690
+ })
691
+ }
692
+
693
+ fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
694
+ match field {
695
+ PaneField::PaneWidth => Ok(Some("120".to_string())),
696
+ _ => Ok(None),
697
+ }
698
+ }
699
+
700
+ fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
701
+ Ok(PaneLiveness::Live)
702
+ }
703
+
704
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
705
+ Ok(Vec::new())
706
+ }
707
+
708
+ fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
709
+ Ok(*self.session_present.lock().unwrap())
710
+ }
711
+
712
+ fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
713
+ Ok(Vec::new())
714
+ }
715
+
716
+ fn set_session_env(
717
+ &self,
718
+ _session: &SessionName,
719
+ _key: &str,
720
+ _value: &str,
721
+ ) -> Result<SetEnvOutcome, TransportError> {
722
+ Ok(SetEnvOutcome::Applied)
723
+ }
724
+
725
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
726
+ *self.session_present.lock().unwrap() = false;
727
+ Ok(())
728
+ }
729
+
730
+ fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
731
+ Ok(())
732
+ }
733
+
734
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
735
+ Ok(AttachOutcome::Attached)
736
+ }
737
+ }