@team-agent/installer 0.5.65 → 0.5.68

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 (208) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +6 -0
  4. package/crates/team-agent/src/cli/adapters.rs +6 -1
  5. package/crates/team-agent/src/cli/diagnose.rs +82 -0
  6. package/crates/team-agent/src/cli/emit.rs +3 -3
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +21 -7
  9. package/crates/team-agent/src/cli/leaders.rs +125 -1
  10. package/crates/team-agent/src/cli/mod.rs +20 -0
  11. package/crates/team-agent/src/cli/send/presentation.rs +7 -1
  12. package/crates/team-agent/src/cli/spec.rs +2 -0
  13. package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
  14. package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
  15. package/crates/team-agent/src/cli/tests/base.rs +26 -3
  16. package/crates/team-agent/src/cli/tests/compile.rs +1 -1
  17. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  18. package/crates/team-agent/src/cli/tests/mod.rs +2 -2
  19. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
  20. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  21. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  22. package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
  23. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  24. package/crates/team-agent/src/compiler/tests.rs +23 -20
  25. package/crates/team-agent/src/compiler.rs +84 -5
  26. package/crates/team-agent/src/conpty/backend.rs +16 -0
  27. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  28. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  29. package/crates/team-agent/src/coordinator/health.rs +173 -0
  30. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  31. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  32. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  33. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  34. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  35. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  36. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  37. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  38. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  39. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  40. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  41. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  42. package/crates/team-agent/src/coordinator/types.rs +49 -0
  43. package/crates/team-agent/src/db/message_store.rs +39 -5
  44. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  45. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  46. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  47. package/crates/team-agent/src/leader/registry.rs +65 -0
  48. package/crates/team-agent/src/leader/start.rs +1080 -58
  49. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
  50. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  51. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  52. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -14
  53. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +69 -2
  54. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +196 -2
  55. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  56. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  57. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  58. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -457
  59. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  60. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  61. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  62. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  63. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  64. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  65. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  66. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  67. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  68. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  69. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  70. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  71. package/crates/team-agent/src/lifecycle/launch/spawn.rs +101 -4
  72. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +244 -48
  73. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -2
  74. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  75. package/crates/team-agent/src/lifecycle/launch.rs +209 -43
  76. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  77. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  78. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  79. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  80. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  81. package/crates/team-agent/src/lifecycle/restart/agent.rs +213 -3
  82. package/crates/team-agent/src/lifecycle/restart/common.rs +373 -18
  83. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  84. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  85. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -15
  86. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  87. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  88. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  89. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  90. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
  91. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +3 -4
  92. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
  93. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
  95. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  96. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +159 -174
  97. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +15 -13
  99. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -18
  100. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
  101. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  102. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  103. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  104. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
  105. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
  106. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  107. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  108. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  109. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  110. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  111. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  112. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
  113. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
  114. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +65 -40
  115. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -32
  116. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +17 -9
  117. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  118. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +24 -23
  119. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +98 -62
  120. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
  121. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
  122. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
  123. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -1
  124. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
  125. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
  126. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
  127. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +41 -8
  128. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
  129. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
  130. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
  131. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
  132. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
  133. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
  134. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
  135. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +60 -2
  136. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
  137. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
  138. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  139. package/crates/team-agent/src/lifecycle/tests.rs +23 -15
  140. package/crates/team-agent/src/lifecycle/types.rs +61 -2
  141. package/crates/team-agent/src/lifecycle/worker_command_context.rs +200 -33
  142. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  143. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  144. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  145. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  146. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  147. package/crates/team-agent/src/messaging/results.rs +4 -0
  148. package/crates/team-agent/src/messaging/send.rs +34 -0
  149. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  150. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  151. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  152. package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
  153. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  154. package/crates/team-agent/src/messaging/types.rs +4 -1
  155. package/crates/team-agent/src/model/enums.rs +14 -5
  156. package/crates/team-agent/src/model/permissions.rs +3 -0
  157. package/crates/team-agent/src/model/spec.rs +129 -6
  158. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
  159. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
  160. package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
  161. package/crates/team-agent/src/os_probe.rs +73 -2
  162. package/crates/team-agent/src/provider/adapter.rs +231 -4
  163. package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
  164. package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
  165. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
  166. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  167. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  168. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  169. package/crates/team-agent/src/provider/bypass_flags.rs +124 -0
  170. package/crates/team-agent/src/provider/classify.rs +4 -2
  171. package/crates/team-agent/src/provider/faults.rs +2 -1
  172. package/crates/team-agent/src/provider/mod.rs +13 -0
  173. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  174. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  175. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  176. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  177. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  178. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  179. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  180. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  181. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  182. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  183. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  184. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  185. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  186. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  187. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  188. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  189. package/crates/team-agent/src/provider/types.rs +4 -2
  190. package/crates/team-agent/src/provider/wire.rs +23 -1
  191. package/crates/team-agent/src/state/persist.rs +301 -8
  192. package/crates/team-agent/src/state/repository.rs +5 -0
  193. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  194. package/crates/team-agent/src/tmux_backend.rs +1083 -75
  195. package/crates/team-agent/src/transport/test_support.rs +20 -0
  196. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  197. package/crates/team-agent/src/transport.rs +265 -1
  198. package/npm/install.mjs +129 -73
  199. package/package.json +4 -4
  200. package/skills/team-agent/SKILL.md +33 -238
  201. package/skills/team-agent/command-coverage.json +31 -0
  202. package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
  203. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  204. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -483
  205. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  206. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
  207. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
  208. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
@@ -1,3 +1,22 @@
1
+ //! ---
2
+ //! purpose: 单个席位在 runtime state 里的行的构造,含 MCP 配置落盘与审批策略投影
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: running_agent_state
6
+ //! what: 构造 running 席位的 state 行,同时写出该席位的 MCP 配置文件
7
+ //! - name: effective_approval_policy
8
+ //! what: 把 bypass 审批结论投影成 state 里的 JSON
9
+ //! - name: effective_approval_policy_from_agent
10
+ //! what: 由席位行自己的 dangerously_skip_permissions 算审批策略
11
+ //! depends:
12
+ //! - crate::provider
13
+ //! - crate::provider::bypass_flags
14
+ //! - crate::lifecycle::launch::mcp_config
15
+ //! boundary:
16
+ //! - 只构造与落 MCP 配置,不 spawn 也不改窗口
17
+ //! - 审批结论只认席位自己的声明,不做全队继承
18
+ //! maturity: wired
19
+ //! ---
1
20
  use std::collections::{BTreeMap, BTreeSet};
2
21
  use std::path::{Path, PathBuf};
3
22
  use std::process::Command;
@@ -14,6 +33,15 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
14
33
 
15
34
  use super::*;
16
35
 
36
+ /// ---
37
+ /// purpose: 构造某席位 running 状态的 state 行
38
+ /// params:
39
+ /// spawn_cwd: 该席位进程的工作目录
40
+ /// started_agent: 本次 spawn 结果,用于取布局窗口
41
+ /// pane_id: 该席位的 pane,空串时不写入
42
+ /// returns: 含状态、provider、模型、auth_mode、窗口、MCP 配置路径、spawn 时间与 pane 信息的行
43
+ /// errors: 取 provider MCP 配置失败返回 Provider,写 MCP 配置文件失败返回 StatePersist
44
+ /// ---
17
45
  pub(super) fn running_agent_state(
18
46
  agent: &Value,
19
47
  id: &str,
@@ -24,7 +52,6 @@ pub(super) fn running_agent_state(
24
52
  team_id: &str,
25
53
  pane_id: Option<&str>,
26
54
  pane_pid: Option<u32>,
27
- safety: &DangerousApproval,
28
55
  started_agent: Option<&StartedAgent>,
29
56
  profile_dir: Option<&Path>,
30
57
  ) -> Result<serde_json::Value, LifecycleError> {
@@ -84,7 +111,16 @@ pub(super) fn running_agent_state(
84
111
  permissions_json(agent, id, provider)
85
112
  .map_err(|e| LifecycleError::Compile(e.to_string()))?,
86
113
  );
87
- persist_effective_approval_policy(&mut state, safety);
114
+ // 0.5.66 bypass 单源:policy 从 agent 字段派生,不再用全队 `DangerousApproval`。
115
+ persist_effective_approval_policy_from_yaml_agent(&mut state, agent, provider);
116
+ // 0.5.66 bypass 单源 §2.6:fresh spawn 记录运行面 bypass 值(供 restart drift 挡板)。
117
+ state.insert(
118
+ "as_launched_dangerously_skip_permissions".to_string(),
119
+ serde_json::json!(matches!(
120
+ agent.get("dangerously_skip_permissions"),
121
+ Some(Value::Bool(true))
122
+ )),
123
+ );
88
124
  state.insert("session_id".to_string(), serde_json::Value::Null);
89
125
  state.insert("rollout_path".to_string(), serde_json::Value::Null);
90
126
  state.insert("captured_at".to_string(), serde_json::Value::Null);
@@ -133,6 +169,10 @@ pub(super) fn running_agent_state(
133
169
  Ok(serde_json::Value::Object(state))
134
170
  }
135
171
 
172
+ /// ---
173
+ /// purpose: 把 bypass 审批结论投影成落盘用的 JSON
174
+ /// returns: 含 enabled、来源、是否继承与显式确认位;来源为 runtime_config 且启用时视为已显式确认
175
+ /// ---
136
176
  pub(crate) fn effective_approval_policy(safety: &DangerousApproval) -> serde_json::Value {
137
177
  serde_json::json!({
138
178
  "enabled": safety.enabled,
@@ -145,6 +185,11 @@ pub(crate) fn effective_approval_policy(safety: &DangerousApproval) -> serde_jso
145
185
  })
146
186
  }
147
187
 
188
+ /// ---
189
+ /// purpose: 把审批策略写进席位 state 行
190
+ /// params:
191
+ /// agent_state: 就地写入 effective_approval_policy 字段
192
+ /// ---
148
193
  pub(crate) fn persist_effective_approval_policy(
149
194
  agent_state: &mut serde_json::Map<String, serde_json::Value>,
150
195
  safety: &DangerousApproval,
@@ -155,6 +200,10 @@ pub(crate) fn persist_effective_approval_policy(
155
200
  );
156
201
  }
157
202
 
203
+ /// ---
204
+ /// purpose: 给出审批来源的稳定 wire 字符串
205
+ /// returns: runtime_config、leader_process 或 disabled
206
+ /// ---
158
207
  pub(super) fn dangerous_approval_source_str(source: DangerousApprovalSource) -> &'static str {
159
208
  match source {
160
209
  DangerousApprovalSource::RuntimeConfig => "runtime_config",
@@ -162,3 +211,148 @@ pub(super) fn dangerous_approval_source_str(source: DangerousApprovalSource) ->
162
211
  DangerousApprovalSource::Disabled => "disabled",
163
212
  }
164
213
  }
214
+
215
+ /// ---
216
+ /// purpose: 由 JSON 席位行自己的 bypass 声明算出审批策略
217
+ /// returns: 未声明时全为关闭;声明为真时记 runtime_config 来源并带该 provider 的 bypass argv
218
+ /// contract_id: lifecycle.agent_state.approval_policy_from_agent
219
+ /// ---
220
+ /// 0.5.66 bypass 单源:从 agent 行 + provider 构造 approval policy JSON
221
+ /// (restart/fork/add 落盘用;取代全队 `DangerousApproval` 派生)。
222
+ pub(crate) fn effective_approval_policy_from_agent(
223
+ agent: &serde_json::Value,
224
+ provider: Provider,
225
+ ) -> serde_json::Value {
226
+ let enabled = agent
227
+ .get("dangerously_skip_permissions")
228
+ .and_then(serde_json::Value::as_bool)
229
+ .unwrap_or(false);
230
+ if !enabled {
231
+ return serde_json::json!({
232
+ "enabled": false,
233
+ "source": "disabled",
234
+ "inherited": false,
235
+ "explicit_yes_confirmed": false,
236
+ "provider": null,
237
+ "flag": null,
238
+ "worker_capability_above_leader": false,
239
+ });
240
+ }
241
+ let flag = crate::provider::bypass_flags::provider_bypass_flag(provider);
242
+ serde_json::json!({
243
+ "enabled": true,
244
+ "source": "runtime_config",
245
+ "inherited": false,
246
+ "explicit_yes_confirmed": true,
247
+ "provider": provider_wire_str(provider),
248
+ "flag": flag,
249
+ "worker_capability_above_leader": false,
250
+ })
251
+ }
252
+
253
+ fn provider_wire_str(provider: Provider) -> &'static str {
254
+ match provider {
255
+ Provider::Claude => "claude",
256
+ Provider::ClaudeCode => "claude_code",
257
+ Provider::Codex => "codex",
258
+ Provider::Copilot => "copilot",
259
+ Provider::GeminiCli => "gemini_cli",
260
+ Provider::Grok => "grok",
261
+ Provider::CursorAgent => "cursor_agent",
262
+ Provider::Fake => "fake",
263
+ }
264
+ }
265
+
266
+ /// ---
267
+ /// purpose: 由席位行自身算出审批策略并写回同一行
268
+ /// params:
269
+ /// agent_state: 既是来源也是写入目标
270
+ /// ---
271
+ /// 0.5.66 bypass 单源:从 agent 行(state Map)+ provider 落盘 approval policy。
272
+ /// 目标与来源是同一 agent 行。
273
+ pub(crate) fn persist_effective_approval_policy_from_agent(
274
+ agent_state: &mut serde_json::Map<String, serde_json::Value>,
275
+ provider: Provider,
276
+ ) {
277
+ let agent = serde_json::Value::Object(agent_state.clone());
278
+ agent_state.insert(
279
+ "effective_approval_policy".to_string(),
280
+ effective_approval_policy_from_agent(&agent, provider),
281
+ );
282
+ }
283
+
284
+ /// ---
285
+ /// purpose: 由 YAML spec 里的 agent 声明算出审批策略并写进 state 行
286
+ /// params:
287
+ /// agent: YAML 侧的 agent 节点,提供 bypass 声明
288
+ /// agent_state: 写入目标
289
+ /// ---
290
+ /// 0.5.66 bypass 单源:yaml spec agent 版(launch/state_projection 用)。
291
+ pub(crate) fn persist_effective_approval_policy_from_yaml_agent(
292
+ agent_state: &mut serde_json::Map<String, serde_json::Value>,
293
+ agent: &Value,
294
+ provider: Provider,
295
+ ) {
296
+ let enabled = matches!(agent.get("dangerously_skip_permissions"), Some(Value::Bool(true)));
297
+ let policy = if !enabled {
298
+ serde_json::json!({
299
+ "enabled": false,
300
+ "source": "disabled",
301
+ "inherited": false,
302
+ "explicit_yes_confirmed": false,
303
+ "provider": null,
304
+ "flag": null,
305
+ "worker_capability_above_leader": false,
306
+ })
307
+ } else {
308
+ let flag = crate::provider::bypass_flags::provider_bypass_flag(provider);
309
+ serde_json::json!({
310
+ "enabled": true,
311
+ "source": "runtime_config",
312
+ "inherited": false,
313
+ "explicit_yes_confirmed": true,
314
+ "provider": provider_wire_str(provider),
315
+ "flag": flag,
316
+ "worker_capability_above_leader": false,
317
+ })
318
+ };
319
+ agent_state.insert("effective_approval_policy".to_string(), policy);
320
+ }
321
+
322
+ #[cfg(test)]
323
+ mod tests {
324
+ #![allow(clippy::unwrap_used)]
325
+ use super::*;
326
+
327
+ fn yaml_agent(bypass: bool) -> Value {
328
+ crate::model::yaml::loads(&format!(
329
+ "id: a\nrole: r\nprovider: codex\ndangerously_skip_permissions: {bypass}\n"
330
+ ))
331
+ .unwrap()
332
+ }
333
+
334
+ // 0.5.66 bypass 单源 §4.1:true 角色 → policy 落盘为 enabled=true / runtime_config,
335
+ // 且 running_agent_state 记录 as_launched_dangerously_skip_permissions=true(审计留痕前提)。
336
+ #[test]
337
+ fn test_audit_log_written_on_true_spawn() {
338
+ let mut state = serde_json::Map::new();
339
+ persist_effective_approval_policy_from_yaml_agent(&mut state, &yaml_agent(true), Provider::Codex);
340
+ let policy = state.get("effective_approval_policy").unwrap();
341
+ assert_eq!(policy["enabled"], serde_json::json!(true));
342
+ assert_eq!(policy["source"], serde_json::json!("runtime_config"));
343
+ assert_eq!(
344
+ policy["flag"],
345
+ serde_json::json!("--dangerously-bypass-approvals-and-sandbox")
346
+ );
347
+ // 运行面记录(as-launched)供 restart drift 挡板。
348
+ let mut run_state = serde_json::Map::new();
349
+ run_state.insert(
350
+ "as_launched_dangerously_skip_permissions".to_string(),
351
+ serde_json::json!(matches!(
352
+ yaml_agent(true).get("dangerously_skip_permissions"),
353
+ Some(Value::Bool(true))
354
+ )),
355
+ );
356
+ assert_eq!(run_state["as_launched_dangerously_skip_permissions"], serde_json::json!(true));
357
+ }
358
+ }
@@ -1,3 +1,14 @@
1
+ //! ---
2
+ //! purpose: 零上下文重读角色文件再起一席,不继承源会话
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: clone_agent
6
+ //! what: 用源席最新角色文件 add-agent,保留源工具集
7
+ //! boundary:
8
+ //! - 不走 fork 的窗口内命令
9
+ //! - 不把 clone 与 fork 合成一条路径
10
+ //! maturity: wired
11
+ //! ---
1
12
  use crate::lifecycle::*;
2
13
  use crate::model::ids::AgentId;
3
14
  use crate::provider::SessionId;
@@ -5,6 +16,15 @@ use std::path::Path;
5
16
 
6
17
  use super::*;
7
18
 
19
+ /// ---
20
+ /// purpose: 以源席最新角色文件另起一席,不继承源会话
21
+ /// params:
22
+ /// source_agent_id: 源席位,只借它的角色文件与工具集
23
+ /// as_agent_id: 新席位名
24
+ /// label: 新席位的角色标签
25
+ /// returns: clone 报告
26
+ /// errors: 选不到 team 返回 TeamSelect,源席不存在返回 RequirementUnmet,角色物化与加席错误透传
27
+ /// ---
8
28
  pub fn clone_agent(
9
29
  workspace: &Path,
10
30
  source_agent_id: &AgentId,
@@ -40,16 +60,10 @@ pub fn clone_agent(
40
60
  as_agent_id,
41
61
  label,
42
62
  )?;
43
- let spec_path = selected
44
- .spec_path
45
- .as_ref()
46
- .ok_or_else(|| LifecycleError::TeamSelect("active team spec not found".to_string()))?;
47
- let spec = crate::model::yaml::loads(
48
- &std::fs::read_to_string(spec_path)
49
- .map_err(|error| LifecycleError::Compile(error.to_string()))?,
50
- )
51
- .map_err(|error| LifecycleError::Compile(error.to_string()))?;
52
- clamp_materialized_role_to_leader(materialized.path(), &spec)?;
63
+ // The materialized role carries the source role's DECLARED tools verbatim —
64
+ // a clone must preserve the source seat's full tools set (add-agent does
65
+ // the same; no leader-ceiling clamp). See role_source.rs for the removed
66
+ // clamp_materialized_role_to_leader.
53
67
  let added = add_agent(
54
68
  &selected.run_workspace,
55
69
  as_agent_id,
@@ -63,6 +77,15 @@ pub fn clone_agent(
63
77
  as_agent_id,
64
78
  source_session_id.as_ref(),
65
79
  );
80
+ if let Some((session_id, backing_path)) = verified.as_ref() {
81
+ persist_clone_session(
82
+ &selected.run_workspace,
83
+ selected.team_key.as_str(),
84
+ as_agent_id,
85
+ session_id,
86
+ backing_path,
87
+ )?;
88
+ }
66
89
  let (session_id, backing_path, backing_state) = match verified {
67
90
  Some((session_id, backing_path)) => (
68
91
  Some(session_id),
@@ -94,13 +117,80 @@ fn read_agent_session(
94
117
  .and_then(|agents| agents.get(agent_id.as_str()))?;
95
118
  let session = agent
96
119
  .get("session_id")
120
+ .or_else(|| agent.get("_pending_session_id"))
97
121
  .and_then(serde_json::Value::as_str)
98
122
  .filter(|value| !value.is_empty())?;
99
123
  let backing = agent
100
124
  .get("rollout_path")
101
125
  .and_then(serde_json::Value::as_str)
102
126
  .filter(|value| !value.is_empty())
103
- .map(std::path::PathBuf::from)?;
127
+ .map(std::path::PathBuf::from)
128
+ .or_else(|| find_claude_backing(agent, session))?;
104
129
  let distinct = source_session_id.is_none_or(|source| source.as_str() != session);
105
130
  (distinct && backing.is_file()).then(|| (SessionId::new(session), backing))
106
131
  }
132
+
133
+ fn find_claude_backing(agent: &serde_json::Value, session: &str) -> Option<std::path::PathBuf> {
134
+ let root = agent
135
+ .get("claude_projects_root")
136
+ .and_then(serde_json::Value::as_str)
137
+ .map(std::path::PathBuf::from)
138
+ .or_else(|| {
139
+ std::env::var_os("HOME")
140
+ .map(|home| std::path::PathBuf::from(home).join(".claude/projects"))
141
+ })?;
142
+ let target = format!("{session}.jsonl");
143
+ let mut pending = vec![root];
144
+ while let Some(dir) = pending.pop() {
145
+ let entries = std::fs::read_dir(&dir).ok()?;
146
+ for entry in entries.flatten() {
147
+ let path = entry.path();
148
+ if path.is_dir() {
149
+ pending.push(path);
150
+ } else if path.file_name().and_then(|name| name.to_str()) == Some(target.as_str()) {
151
+ return Some(path);
152
+ }
153
+ }
154
+ }
155
+ None
156
+ }
157
+
158
+ fn persist_clone_session(
159
+ workspace: &Path,
160
+ team_key: &str,
161
+ agent_id: &AgentId,
162
+ session_id: &SessionId,
163
+ backing_path: &Path,
164
+ ) -> Result<(), LifecycleError> {
165
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
166
+ workspace,
167
+ operation: "clone-session-bind",
168
+ team: Some(team_key),
169
+ agent_id: Some(agent_id),
170
+ })?;
171
+ let mut state = crate::state::projection::select_runtime_state(workspace, Some(team_key))
172
+ .map_err(|error| LifecycleError::TeamSelect(error.to_string()))?;
173
+ let Some(agent) = state
174
+ .get_mut("agents")
175
+ .and_then(serde_json::Value::as_object_mut)
176
+ .and_then(|agents| agents.get_mut(agent_id.as_str()))
177
+ .and_then(serde_json::Value::as_object_mut)
178
+ else {
179
+ return Ok(());
180
+ };
181
+ agent.insert(
182
+ "session_id".to_string(),
183
+ serde_json::json!(session_id.as_str()),
184
+ );
185
+ agent.insert(
186
+ "rollout_path".to_string(),
187
+ serde_json::json!(backing_path.to_string_lossy().to_string()),
188
+ );
189
+ agent.insert("captured_via".to_string(), serde_json::json!("clone-agent"));
190
+ crate::lifecycle::launch::save_launched_team_state_for_key(
191
+ workspace,
192
+ &state,
193
+ Some(team_key),
194
+ Some(agent_id.as_str()),
195
+ )
196
+ }
@@ -0,0 +1,229 @@
1
+ //! ---
2
+ //! purpose: cursor 候选 A——`agent create-chat` 解析首行 chatId 后按记录 PID 杀
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: apply_cursor_create_chat_plan
6
+ //! what: 无 pending 时把 CLI chatId 写入 expected_session_id 并 `--resume` 该 id
7
+ //! - name: create_cursor_chat_id
8
+ //! what: 跑 create-chat,读第一行 UUID,只杀本调用记录的 PID
9
+ //! requires:
10
+ //! - name: cursor-agent-cli
11
+ //! what: PATH 上的 `agent`;不存在或首行非法则返回 None(降级候选 B)
12
+ //! boundary:
13
+ //! - 不发明 --session-id;不 pkill/killall;不 wait 子进程自行退出
14
+ //! - 不读 chats 正文、不打印 proxy 值
15
+ //! - 已有 expected_session_id 或 argv 已含 --resume 则不动
16
+ //! - 2026-08-21 活体:create-chat 后 `--resume` 的存档在 pane stop 后被清;
17
+ //! TUI 自建会话能留盘。fresh 路径不调用本函数(走 scan 唯一 hex)。
18
+ //! maturity: wired
19
+ //! ---
20
+
21
+ use std::io::{BufRead, BufReader};
22
+ use std::path::Path;
23
+ use std::process::{Command, Stdio};
24
+ use std::sync::mpsc;
25
+ use std::thread;
26
+ use std::time::Duration;
27
+
28
+ use crate::provider::types::{CommandPlan, SessionId};
29
+
30
+ const FIRST_LINE_TIMEOUT: Duration = Duration::from_secs(5);
31
+ const TERM_GRACE: Duration = Duration::from_millis(200);
32
+
33
+ /// ---
34
+ /// purpose: 冷启/无 session 的 cursor 计划绑定 create-chat 得到的 chatId
35
+ /// params:
36
+ /// plan: build_command_plan 产出;workspace: 物理 cwd
37
+ /// returns: 成功则 expected_session_id 与 argv `--resume <id>` 齐;失败则计划不变
38
+ /// contract:
39
+ /// provides:
40
+ /// - name: apply_cursor_create_chat_plan
41
+ /// what: 只在无 pending 且 argv 无 --resume 时写入 CLI chatId
42
+ /// boundary:
43
+ /// - 库测试(cfg(test))不调真实 agent,避免订阅/挂起;具名测试打 create_cursor_chat_id
44
+ /// - 不在 launch/restart fresh 调用:活体证实 `--resume` 空聊存档停 pane 后消失
45
+ /// ---
46
+ pub(crate) fn apply_cursor_create_chat_plan(plan: &mut CommandPlan, workspace: &Path) {
47
+ if cfg!(test) {
48
+ return;
49
+ }
50
+ if plan.expected_session_id.is_some() {
51
+ return;
52
+ }
53
+ if plan.argv.iter().any(|flag| flag == "--resume") {
54
+ return;
55
+ }
56
+ let Some(chat_id) = create_cursor_chat_id(workspace) else {
57
+ return;
58
+ };
59
+ plan.expected_session_id = Some(SessionId::new(chat_id.clone()));
60
+ plan.argv.push("--resume".to_string());
61
+ plan.argv.push(chat_id);
62
+ }
63
+
64
+ /// ---
65
+ /// purpose: 跑 `agent create-chat`,解析首行 UUID,按记录 PID 终止挂起进程
66
+ /// params:
67
+ /// workspace: create-chat 的 cwd(物理 workspace)
68
+ /// returns: 合法 UUID;命令缺失/超时/非法首行则 None
69
+ /// contract:
70
+ /// provides:
71
+ /// - name: create_cursor_chat_id
72
+ /// what: 读一行就停;不 wait 退出;只杀本 Child.id()
73
+ /// boundary:
74
+ /// - 禁 pkill/killall;pid 0/1 不杀
75
+ /// ---
76
+ pub(crate) fn create_cursor_chat_id(workspace: &Path) -> Option<String> {
77
+ create_cursor_chat_id_with_command("agent", workspace)
78
+ }
79
+
80
+ fn create_cursor_chat_id_with_command(program: &str, workspace: &Path) -> Option<String> {
81
+ let mut child = Command::new(program)
82
+ .arg("create-chat")
83
+ .current_dir(workspace)
84
+ .stdin(Stdio::null())
85
+ .stdout(Stdio::piped())
86
+ .stderr(Stdio::null())
87
+ .spawn()
88
+ .ok()?;
89
+ let pid = child.id();
90
+ let stdout = child.stdout.take()?;
91
+ let (tx, rx) = mpsc::channel();
92
+ thread::spawn(move || {
93
+ let mut reader = BufReader::new(stdout);
94
+ let mut line = String::new();
95
+ let result = reader.read_line(&mut line).ok().and_then(|_| parse_chat_id_line(&line));
96
+ let _ = tx.send(result);
97
+ });
98
+ let parsed = match rx.recv_timeout(FIRST_LINE_TIMEOUT) {
99
+ Ok(value) => value,
100
+ Err(_) => None,
101
+ };
102
+ kill_recorded_pid(pid);
103
+ let _ = child.wait();
104
+ parsed
105
+ }
106
+
107
+ fn parse_chat_id_line(line: &str) -> Option<String> {
108
+ let token = line
109
+ .trim()
110
+ .split_whitespace()
111
+ .next()
112
+ .unwrap_or("")
113
+ .trim_matches(['"', '\'']);
114
+ is_chat_uuid(token).then(|| token.to_string())
115
+ }
116
+
117
+ fn is_chat_uuid(value: &str) -> bool {
118
+ let bytes = value.as_bytes();
119
+ if bytes.len() != 36 {
120
+ return false;
121
+ }
122
+ for (i, b) in bytes.iter().enumerate() {
123
+ if i == 8 || i == 13 || i == 18 || i == 23 {
124
+ if *b != b'-' {
125
+ return false;
126
+ }
127
+ } else if !b.is_ascii_hexdigit() {
128
+ return false;
129
+ }
130
+ }
131
+ true
132
+ }
133
+
134
+ fn kill_recorded_pid(pid: u32) {
135
+ if pid == 0 || pid == 1 {
136
+ return;
137
+ }
138
+ #[cfg(unix)]
139
+ {
140
+ let raw = pid as i32;
141
+ unsafe {
142
+ libc::kill(raw, libc::SIGTERM);
143
+ }
144
+ thread::sleep(TERM_GRACE);
145
+ unsafe {
146
+ libc::kill(raw, libc::SIGKILL);
147
+ }
148
+ }
149
+ #[cfg(not(unix))]
150
+ {
151
+ let _ = pid;
152
+ }
153
+ }
154
+
155
+ #[cfg(test)]
156
+ mod tests {
157
+ #![allow(clippy::unwrap_used)]
158
+
159
+ use super::*;
160
+ use std::os::unix::fs::PermissionsExt;
161
+ use std::sync::atomic::{AtomicU64, Ordering};
162
+
163
+ fn tmp_root(tag: &str) -> std::path::PathBuf {
164
+ static CTR: AtomicU64 = AtomicU64::new(0);
165
+ let dir = std::env::temp_dir().join(format!(
166
+ "ta-d117-create-chat-{}-{}-{}",
167
+ tag,
168
+ std::process::id(),
169
+ CTR.fetch_add(1, Ordering::Relaxed)
170
+ ));
171
+ let _ = std::fs::remove_dir_all(&dir);
172
+ std::fs::create_dir_all(&dir).unwrap();
173
+ dir
174
+ }
175
+
176
+ fn write_stub(dir: &Path, body: &str) -> std::path::PathBuf {
177
+ let path = dir.join("agent");
178
+ std::fs::write(&path, body).unwrap();
179
+ let mut perm = std::fs::metadata(&path).unwrap().permissions();
180
+ perm.set_mode(0o755);
181
+ std::fs::set_permissions(&path, perm).unwrap();
182
+ path
183
+ }
184
+
185
+ #[test]
186
+ fn create_chat_reads_first_line_and_kills_recorded_pid() {
187
+ let base = tmp_root("ok");
188
+ let ws = base.join("ws");
189
+ std::fs::create_dir_all(&ws).unwrap();
190
+ let bin = write_stub(
191
+ &base,
192
+ "#!/bin/sh\necho '681c8837-d564-44fa-98bf-4ff5a66592e2'\nkill -STOP $$\n",
193
+ );
194
+ let got = create_cursor_chat_id_with_command(bin.to_str().unwrap(), &ws);
195
+ assert_eq!(
196
+ got.as_deref(),
197
+ Some("681c8837-d564-44fa-98bf-4ff5a66592e2")
198
+ );
199
+ let _ = std::fs::remove_dir_all(&base);
200
+ }
201
+
202
+ #[test]
203
+ fn create_chat_rejects_non_uuid_first_line() {
204
+ let base = tmp_root("bad");
205
+ let ws = base.join("ws");
206
+ std::fs::create_dir_all(&ws).unwrap();
207
+ let bin = write_stub(&base, "#!/bin/sh\necho not-a-uuid\nkill -STOP $$\n");
208
+ let got = create_cursor_chat_id_with_command(bin.to_str().unwrap(), &ws);
209
+ assert!(got.is_none());
210
+ let _ = std::fs::remove_dir_all(&base);
211
+ }
212
+
213
+ #[test]
214
+ fn apply_is_noop_when_resume_already_present() {
215
+ let mut plan = CommandPlan {
216
+ argv: vec![
217
+ "agent".into(),
218
+ "--resume".into(),
219
+ "already-bound".into(),
220
+ ],
221
+ expected_session_id: None,
222
+ provider_projects_root: None,
223
+ managed_mcp_config: false,
224
+ };
225
+ apply_cursor_create_chat_plan(&mut plan, Path::new("/tmp"));
226
+ assert!(plan.expected_session_id.is_none());
227
+ assert_eq!(plan.argv.iter().filter(|a| *a == "--resume").count(), 1);
228
+ }
229
+ }