@team-agent/installer 0.5.66 → 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 (164) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -0
  4. package/crates/team-agent/src/cli/adapters.rs +5 -0
  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/missing_subcommands.rs +129 -19
  17. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  18. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  19. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  20. package/crates/team-agent/src/compiler.rs +59 -0
  21. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  22. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  23. package/crates/team-agent/src/coordinator/health.rs +173 -0
  24. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  25. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  26. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  27. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  29. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  30. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  31. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  32. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  33. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  34. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  35. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  36. package/crates/team-agent/src/coordinator/types.rs +49 -0
  37. package/crates/team-agent/src/db/message_store.rs +39 -5
  38. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  39. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  40. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  41. package/crates/team-agent/src/leader/registry.rs +65 -0
  42. package/crates/team-agent/src/leader/start.rs +920 -63
  43. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  44. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  45. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -12
  46. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +62 -0
  47. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +59 -0
  48. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  49. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  50. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  51. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -456
  52. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  53. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  54. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  55. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  56. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  57. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  58. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  59. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  60. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  61. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  62. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  63. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  64. package/crates/team-agent/src/lifecycle/launch/spawn.rs +62 -2
  65. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +139 -0
  66. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
  67. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  68. package/crates/team-agent/src/lifecycle/launch.rs +99 -29
  69. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  70. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  71. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  72. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  73. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  74. package/crates/team-agent/src/lifecycle/restart/agent.rs +124 -0
  75. package/crates/team-agent/src/lifecycle/restart/common.rs +369 -11
  76. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  77. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  78. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
  79. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  80. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  81. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  82. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  83. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
  84. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  85. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
  86. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
  88. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  89. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  90. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  91. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  92. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  93. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  94. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  95. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  96. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  97. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
  98. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
  99. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
  100. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
  103. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +2 -0
  104. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
  105. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
  106. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  107. package/crates/team-agent/src/lifecycle/tests.rs +23 -13
  108. package/crates/team-agent/src/lifecycle/types.rs +61 -0
  109. package/crates/team-agent/src/lifecycle/worker_command_context.rs +85 -11
  110. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  111. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  112. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  113. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  114. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  115. package/crates/team-agent/src/messaging/results.rs +4 -0
  116. package/crates/team-agent/src/messaging/send.rs +34 -0
  117. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  118. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  119. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  121. package/crates/team-agent/src/messaging/types.rs +4 -1
  122. package/crates/team-agent/src/model/enums.rs +14 -5
  123. package/crates/team-agent/src/model/permissions.rs +3 -0
  124. package/crates/team-agent/src/os_probe.rs +73 -2
  125. package/crates/team-agent/src/provider/adapter.rs +231 -4
  126. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  127. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  128. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  129. package/crates/team-agent/src/provider/bypass_flags.rs +46 -2
  130. package/crates/team-agent/src/provider/classify.rs +4 -2
  131. package/crates/team-agent/src/provider/faults.rs +2 -1
  132. package/crates/team-agent/src/provider/mod.rs +11 -0
  133. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  134. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  135. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  136. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  137. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  138. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  139. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  140. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  141. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  142. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  143. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  144. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  145. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  146. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  147. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  148. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  149. package/crates/team-agent/src/provider/types.rs +4 -2
  150. package/crates/team-agent/src/provider/wire.rs +23 -1
  151. package/crates/team-agent/src/state/persist.rs +301 -8
  152. package/crates/team-agent/src/state/repository.rs +5 -0
  153. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  154. package/crates/team-agent/src/tmux_backend.rs +1064 -75
  155. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  156. package/crates/team-agent/src/transport.rs +248 -1
  157. package/npm/install.mjs +129 -73
  158. package/package.json +4 -4
  159. package/skills/team-agent/SKILL.md +33 -238
  160. package/skills/team-agent/command-coverage.json +31 -0
  161. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  162. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
  163. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  164. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
@@ -1,3 +1,23 @@
1
+ //! ---
2
+ //! purpose: 从 spec 里的一个 agent 定义,编译出 worker 启动所需的系统提示词与工具串
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: WorkerCommandAgent
6
+ //! what: 从 YAML 或 JSON 读出的单 agent 命令上下文
7
+ //! - name: compile_worker_system_prompt
8
+ //! what: 按身份、runtime 契约、通信模式、角色正文、输出契约、权限说明拼系统提示词
9
+ //! - name: resolved_tool_strings_for_command
10
+ //! what: 解析出该 agent 的工具串,声明了 bypass 时追加 dangerous_auto_approve 哨兵
11
+ //! depends:
12
+ //! - crate::model::permissions
13
+ //! - crate::communication_mode
14
+ //! - crate::provider::bypass_flags
15
+ //! boundary:
16
+ //! - 只产出字符串,不 spawn 进程、不写盘
17
+ //! - bypass 只认 agent 自身声明,不从 team/runtime/leader argv 继承
18
+ //! - provider 没有 bypass argv 定义时报错,不静默降级
19
+ //! maturity: wired
20
+ //! ---
1
21
  use std::path::Path;
2
22
 
3
23
  use crate::communication_mode::CommunicationMode;
@@ -13,9 +33,9 @@ output. All communication must go through Team Agent MCP tools.
13
33
 
14
34
  ## Communication (mandatory)
15
35
 
16
- - Coordinate with teammate: team_orchestrator.send_message(to='<agent_id>', content='...')
17
- - Broadcast to all teammates: team_orchestrator.send_message(to='*', content='...')
18
- - Task complete: team_orchestrator.report_result(summary='...') — call exactly once
36
+ - Coordinate with teammate: {send_message}(to='<agent_id>', content='...')
37
+ - Broadcast to all teammates: {send_message}(to='*', content='...')
38
+ - Task complete: {report_result}(summary='...') — call exactly once
19
39
 
20
40
  ## Rules
21
41
 
@@ -29,7 +49,7 @@ output. All communication must go through Team Agent MCP tools.
29
49
  // semantics (leader-attach dependence + fallback status) that the
30
50
  // generic runtime section deliberately leaves out.
31
51
  const RESULT_ENVELOPE_OUTPUT_CONTRACT: &str =
32
- "Final completion must call team_orchestrator.report_result exactly once with a short summary \
52
+ "Final completion must call {report_result} exactly once with a short summary \
33
53
  and optional status/changes/tests; the MCP runtime injects the result into the attached leader pane. \
34
54
  If no leader is attached, the tool returns a fallback/failed result instead of completion.";
35
55
 
@@ -47,6 +67,16 @@ pub(crate) struct WorkerCommandAgent {
47
67
  }
48
68
 
49
69
  impl WorkerCommandAgent {
70
+ /// ---
71
+ /// purpose: 从 spec 的 YAML agent 节点读出命令上下文
72
+ /// params:
73
+ /// agent: 单个 agent 的 YAML 节点
74
+ /// fallback_id: agent 节点没写 id 时的兜底 id
75
+ /// provider: 已解析的 provider
76
+ /// returns: 填好的 WorkerCommandAgent
77
+ /// errors: communication_mode 取值非法时返回 LifecycleError
78
+ /// contract_id: lifecycle.worker_command_agent.from_source
79
+ /// ---
50
80
  pub(crate) fn from_yaml(
51
81
  agent: &crate::model::yaml::Value,
52
82
  fallback_id: Option<&str>,
@@ -101,6 +131,16 @@ impl WorkerCommandAgent {
101
131
  })
102
132
  }
103
133
 
134
+ /// ---
135
+ /// purpose: 从 runtime state 的 JSON agent 节点读出命令上下文
136
+ /// params:
137
+ /// agent: 单个 agent 的 JSON 节点
138
+ /// fallback_id: agent 节点没写 id 时的兜底 id
139
+ /// provider: 已解析的 provider
140
+ /// returns: 填好的 WorkerCommandAgent
141
+ /// errors: communication_mode 取值非法时返回 LifecycleError
142
+ /// contract_id: lifecycle.worker_command_agent.from_source
143
+ /// ---
104
144
  pub(crate) fn from_json(
105
145
  agent: &serde_json::Value,
106
146
  fallback_id: Option<&str>,
@@ -156,6 +196,11 @@ impl WorkerCommandAgent {
156
196
  }
157
197
  }
158
198
 
199
+ /// ---
200
+ /// purpose: 拼出 worker 的系统提示词,身份段必须排在最前
201
+ /// returns: 各段以空行分隔的提示词,空段被丢弃
202
+ /// errors: 角色正文读取失败或权限解析失败时返回 LifecycleError
203
+ /// ---
159
204
  pub(crate) fn compile_worker_system_prompt(
160
205
  agent: &WorkerCommandAgent,
161
206
  ) -> Result<String, LifecycleError> {
@@ -163,13 +208,15 @@ pub(crate) fn compile_worker_system_prompt(
163
208
  // identity line anchors the very first section (live Python worker argv confirms).
164
209
  // C-1 cr verdict / B2 灵魂件 — identity 必须 FIRST(MUST-4 行为层守:空白上下文问
165
210
  // "你是谁"必须先答 Team Agent worker 身份)。runtime contract 跟后。
211
+ let send_message = mcp_tool_name(agent.provider, "team_orchestrator", "send_message");
212
+ let report_result = mcp_tool_name(agent.provider, "team_orchestrator", "report_result");
166
213
  let mut chunks = vec![
167
214
  identity_section(agent),
168
- runtime_contract_section(),
169
- agent.communication_mode.runtime_contract().to_string(),
215
+ runtime_contract_section(&send_message, &report_result),
216
+ agent.communication_mode.runtime_contract(&send_message),
170
217
  role_body(agent)?,
171
218
  ];
172
- if let Some(contract) = output_contract(agent) {
219
+ if let Some(contract) = output_contract(agent, &report_result) {
173
220
  chunks.push(contract);
174
221
  }
175
222
  if let Some(notes) = permission_notes(agent)? {
@@ -182,6 +229,13 @@ pub(crate) fn compile_worker_system_prompt(
182
229
  .join("\n\n"))
183
230
  }
184
231
 
232
+ /// ---
233
+ /// purpose: 解析该 agent 最终生效的工具串
234
+ /// params:
235
+ /// provider: 用于查 bypass argv 定义的 provider
236
+ /// returns: 排序后的工具串;agent 声明 bypass 时末尾追加 dangerous_auto_approve
237
+ /// errors: 权限解析失败,或声明了 bypass 但该 provider 没有 bypass argv 定义时返回 RequirementUnmet
238
+ /// ---
185
239
  pub(crate) fn resolved_tool_strings_for_command(
186
240
  agent: &WorkerCommandAgent,
187
241
  provider: Provider,
@@ -212,6 +266,22 @@ pub(crate) fn resolved_tool_strings_for_command(
212
266
  Ok(tools)
213
267
  }
214
268
 
269
+ /// Provider-facing MCP tool name. Only verified call forms are filled in.
270
+ /// Unverified providers keep the historical dotted spelling.
271
+ fn mcp_tool_name(provider: Provider, server: &str, tool: &str) -> String {
272
+ match provider {
273
+ Provider::Claude | Provider::ClaudeCode => format!("mcp__{server}__{tool}"),
274
+ Provider::Grok => format!("{server}__{tool}"),
275
+ // CursorAgent / Codex / Copilot / GeminiCli / Fake: 未验证,沿用现状点号。
276
+ // CursorAgent 不可与 grok 同臂:仓库里没有活转录,`{server}__{tool}` 是推断。
277
+ Provider::CursorAgent
278
+ | Provider::Codex
279
+ | Provider::Copilot
280
+ | Provider::GeminiCli
281
+ | Provider::Fake => format!("{server}.{tool}"),
282
+ }
283
+ }
284
+
215
285
  fn provider_display_name(provider: Provider) -> &'static str {
216
286
  match provider {
217
287
  Provider::Claude => "claude",
@@ -219,6 +289,8 @@ fn provider_display_name(provider: Provider) -> &'static str {
219
289
  Provider::Codex => "codex",
220
290
  Provider::Copilot => "copilot",
221
291
  Provider::GeminiCli => "gemini_cli",
292
+ Provider::Grok => "grok",
293
+ Provider::CursorAgent => "cursor_agent",
222
294
  Provider::Fake => "fake",
223
295
  }
224
296
  }
@@ -236,8 +308,10 @@ fn resolve_agent_permissions(
236
308
  .map_err(|e| LifecycleError::Compile(e.to_string()))
237
309
  }
238
310
 
239
- fn runtime_contract_section() -> String {
240
- RUNTIME_CONTRACT_SECTION.to_string()
311
+ fn runtime_contract_section(send_message: &str, report_result: &str) -> String {
312
+ RUNTIME_CONTRACT_SECTION
313
+ .replace("{send_message}", send_message)
314
+ .replace("{report_result}", report_result)
241
315
  }
242
316
 
243
317
  fn communication_mode(value: Option<&str>) -> Result<CommunicationMode, LifecycleError> {
@@ -271,9 +345,9 @@ fn role_body(agent: &WorkerCommandAgent) -> Result<String, LifecycleError> {
271
345
  Ok(chunks.join("\n\n"))
272
346
  }
273
347
 
274
- fn output_contract(agent: &WorkerCommandAgent) -> Option<String> {
348
+ fn output_contract(agent: &WorkerCommandAgent, report_result: &str) -> Option<String> {
275
349
  (agent.output_contract_format.as_deref() == Some("result_envelope_v1"))
276
- .then(|| RESULT_ENVELOPE_OUTPUT_CONTRACT.to_string())
350
+ .then(|| RESULT_ENVELOPE_OUTPUT_CONTRACT.replace("{report_result}", report_result))
277
351
  }
278
352
 
279
353
  fn permission_notes(agent: &WorkerCommandAgent) -> Result<Option<String>, LifecycleError> {
@@ -1,3 +1,73 @@
1
+ /// ---
2
+ /// purpose: scoped MCP no-compaction update_state filesystem contract
3
+ /// contract:
4
+ /// provides:
5
+ /// - name: update_state_path_contracts
6
+ /// what: proves the raw no-compaction response preserves its state-file path in an owned fixture
7
+ /// depends:
8
+ /// - name: mcp_state_fixture
9
+ /// what: fixture-owned root and provenance from tests.rs
10
+ /// boundary:
11
+ /// - retain the raw {ok,state_file} result shape
12
+ /// - the owned selector checks workspace, state-file, and runtime paths remain fixture-owned
13
+ /// maturity: wired
14
+ /// ---
15
+
16
+ fn verify_cleanup_surviving_receipt(
17
+ receipt_path: &std::path::Path,
18
+ raw_workspace: &std::path::Path,
19
+ canonical_workspace: &std::path::Path,
20
+ state_file: &std::path::Path,
21
+ runtime_state: &std::path::Path,
22
+ ) {
23
+ let bytes = std::fs::read(receipt_path)
24
+ .expect("provenance receipt must survive fixture Drop");
25
+ let receipt: Value = serde_json::from_slice(&bytes).expect("provenance receipt must be JSON");
26
+ assert_eq!(receipt["schema"], json!("mcp-state-provenance-v1"));
27
+ let payload = &receipt["payload"];
28
+ let payload_bytes = serde_json::to_vec(payload).unwrap();
29
+ assert_eq!(
30
+ receipt["payload_sha256"],
31
+ json!(format!("{:x}", sha2::Sha256::digest(&payload_bytes)))
32
+ );
33
+ assert_eq!(
34
+ payload["raw_workspace"],
35
+ json!(raw_workspace.display().to_string())
36
+ );
37
+ assert_eq!(
38
+ payload["canonical_workspace"],
39
+ json!(canonical_workspace.display().to_string())
40
+ );
41
+ assert_eq!(
42
+ payload["resolved_state_file"],
43
+ json!(state_file.display().to_string())
44
+ );
45
+ assert_eq!(
46
+ payload["runtime_state_path"],
47
+ json!(runtime_state.display().to_string())
48
+ );
49
+ let facts = payload["facts"].as_array().unwrap();
50
+ for name in [
51
+ "root",
52
+ "raw_workspace",
53
+ "canonical_workspace",
54
+ "team_dir",
55
+ "runtime_dir",
56
+ "state_parent",
57
+ "runtime_state",
58
+ "state_file",
59
+ ] {
60
+ let fact = facts
61
+ .iter()
62
+ .find(|fact| fact["name"] == json!(name))
63
+ .unwrap_or_else(|| panic!("provenance fact missing: {name}"));
64
+ assert!(fact["path"].as_str().is_some());
65
+ assert!(fact["uid"].as_u64().is_some());
66
+ assert!(fact["mode"].as_u64().is_some());
67
+ assert!(fact["device"].as_u64().is_some());
68
+ }
69
+ }
70
+
1
71
  #[test]
2
72
  fn dispatch_send_message_worker_accepted_returned_verbatim() {
3
73
  // A-7: accepted requires a REAL stored message_id (no fabricated ids), so the
@@ -52,8 +122,10 @@
52
122
  // state_file (not a golden whitelist key), so the key vanishes.
53
123
  #[test]
54
124
  fn update_state_state_file_survives_no_compaction() {
125
+ let fixture = McpStateFixture::new("writable");
126
+ let expected = fixture.state_file("team_state.md");
55
127
  let tools = TeamOrchestratorTools::with_identity(
56
- &unique_ws("update-state-raw"),
128
+ &fixture.workspace,
57
129
  Some(AgentId::new("leader")),
58
130
  None,
59
131
  );
@@ -62,6 +134,33 @@
62
134
  assert!(v.get("state_file").and_then(Value::as_str).is_some(),
63
135
  "state_file must survive (update_state is not _compact_tool_result'd)");
64
136
  assert_eq!(keys(&v), vec!["ok", "state_file"]);
137
+ let returned_state_file = PathBuf::from(v["state_file"].as_str().unwrap());
138
+ assert_eq!(returned_state_file, expected);
139
+ let runtime_state = crate::state::persist::runtime_state_path(&fixture.workspace);
140
+ assert!(fixture.under_root(&returned_state_file));
141
+ assert!(fixture.under_root(&runtime_state));
142
+ let canonical_workspace =
143
+ crate::model::paths::canonical_run_workspace(&fixture.workspace).unwrap();
144
+ let receipt = fixture.record_provenance(&fixture.workspace, &returned_state_file);
145
+ let root = fixture.root.clone();
146
+ drop(fixture);
147
+ assert!(!root.exists(), "fixture root must be removed before receipt verification");
148
+ verify_cleanup_surviving_receipt(
149
+ &receipt,
150
+ &root.join("workspace"),
151
+ &canonical_workspace,
152
+ &returned_state_file,
153
+ &runtime_state,
154
+ );
155
+ let receipt_bytes = std::fs::read(&receipt).unwrap();
156
+ println!(
157
+ "F10_PROVENANCE_RECEIPT path={} sha256={:x} payload={}",
158
+ receipt.display(),
159
+ sha2::Sha256::digest(&receipt_bytes),
160
+ String::from_utf8_lossy(&receipt_bytes)
161
+ );
162
+ std::fs::remove_file(&receipt).unwrap();
163
+ assert!(!receipt.exists(), "cleanup must remove only the exact receipt path");
65
164
  }
66
165
 
67
166
  // ── #36 report_result setdefault: populated envelope keys WIN over args ─────
@@ -1,8 +1,26 @@
1
+ //! ---
2
+ //! purpose: MCP no-compaction contract test with fixture-owned workspace and path provenance
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: mcp_state_fixture
6
+ //! what: supplies explicit process-owned workspace and state-file paths without mutating process-global environment
7
+ //! - name: mcp_state_path_provenance
8
+ //! what: records raw/canonical workspace, resolved state path, and filesystem ownership facts
9
+ //! depends:
10
+ //! - crate::mcp_server::lifecycle_tools::state_status
11
+ //! - crate::state::selector
12
+ //! boundary:
13
+ //! - the owned no-compaction test writes only within its fixture root
14
+ //! - this suite does not alter persistence semantics
15
+ //! maturity: wired
16
+ //! ---
1
17
  //! step 14a · mcp_server::tests — WAVE-2 RED contracts (Python v0.2.11 golden).
2
18
  #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
3
19
 
4
20
  use super::*;
5
21
  use serde_json::json;
22
+ use sha2::{Digest, Sha256};
23
+ use std::path::{Path, PathBuf};
6
24
 
7
25
  // ── helpers ──────────────────────────────────────────────────────────────
8
26
 
@@ -27,8 +45,7 @@ fn unique_ws(tag: &str) -> std::path::PathBuf {
27
45
  static N: AtomicU64 = AtomicU64::new(0);
28
46
  loop {
29
47
  let n = N.fetch_add(1, Ordering::Relaxed);
30
- let p =
31
- std::env::temp_dir().join(format!("ta-rs-mcp-{tag}-{}-{n}", std::process::id()));
48
+ let p = std::env::temp_dir().join(format!("ta-rs-mcp-{tag}-{}-{n}", std::process::id()));
32
49
  match std::fs::create_dir(&p) {
33
50
  Ok(()) => return p,
34
51
  Err(error) if error.kind() == ErrorKind::AlreadyExists => {}
@@ -37,6 +54,183 @@ fn unique_ws(tag: &str) -> std::path::PathBuf {
37
54
  }
38
55
  }
39
56
 
57
+ static MCP_FIXTURE_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
58
+
59
+ /// One process-owned root for update_state tests. All paths are passed explicitly
60
+ /// to the code under test; the fixture never changes process-global environment.
61
+ struct McpStateFixture {
62
+ root: PathBuf,
63
+ workspace: PathBuf,
64
+ receipt: PathBuf,
65
+ }
66
+
67
+ impl McpStateFixture {
68
+ fn new(_tag: &str) -> Self {
69
+ let seq = MCP_FIXTURE_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
70
+ let base = if cfg!(target_os = "macos") {
71
+ PathBuf::from("/private/tmp")
72
+ } else {
73
+ PathBuf::from("/tmp")
74
+ };
75
+ let raw_root = base.join(format!("ta-mcp-{}-{seq}", std::process::id()));
76
+ std::fs::create_dir(&raw_root).unwrap();
77
+ let root = std::fs::canonicalize(raw_root).unwrap();
78
+ let workspace = root.join("workspace");
79
+ let receipt = root
80
+ .parent()
81
+ .unwrap()
82
+ .join(format!("ta-mcp-receipt-{}-{seq}.json", std::process::id()));
83
+ std::fs::create_dir(&workspace).unwrap();
84
+
85
+ let fixture = Self {
86
+ root,
87
+ workspace,
88
+ receipt,
89
+ };
90
+ fixture.seed_spec("team_state.md");
91
+ crate::state::repository::StateRepository::new(&fixture.workspace)
92
+ .save(
93
+ crate::state::repository::StateWriteIntent::FakeE2eSeed,
94
+ &json!({
95
+ "session_name": "mcp-fixture",
96
+ "active_team_key": "current",
97
+ "agents": {},
98
+ "tasks": []
99
+ }),
100
+ )
101
+ .unwrap();
102
+ fixture
103
+ }
104
+
105
+ fn seed_spec(&self, state_file: &str) {
106
+ std::fs::write(
107
+ self.workspace.join("team.spec.yaml"),
108
+ format!(
109
+ "team:\n name: mcp-fixture\n objective: path contract\nagents: []\ntasks: []\ncontext:\n state_file: {state_file}\n"
110
+ ),
111
+ )
112
+ .unwrap();
113
+ }
114
+
115
+ fn state_file(&self, relative: &str) -> PathBuf {
116
+ self.workspace.join(relative)
117
+ }
118
+
119
+ fn record_provenance(&self, raw_workspace: &Path, state_file: &Path) -> PathBuf {
120
+ let canonical_workspace =
121
+ crate::model::paths::canonical_run_workspace(raw_workspace).unwrap();
122
+ assert!(self.under_root(raw_workspace));
123
+ assert!(self.under_root(&canonical_workspace));
124
+ assert!(self.under_root(state_file));
125
+ let team_dir = self.workspace.join(".team");
126
+ let runtime_dir = team_dir.join("runtime");
127
+ let runtime_state = crate::state::persist::runtime_state_path(&self.workspace);
128
+ let paths = [
129
+ ("root", self.root.as_path()),
130
+ ("raw_workspace", raw_workspace),
131
+ ("canonical_workspace", canonical_workspace.as_path()),
132
+ ("team_dir", team_dir.as_path()),
133
+ ("runtime_dir", runtime_dir.as_path()),
134
+ ("state_parent", state_file.parent().unwrap()),
135
+ ("runtime_state", runtime_state.as_path()),
136
+ ];
137
+ let mut facts = paths
138
+ .into_iter()
139
+ .map(|(name, path)| {
140
+ let metadata = std::fs::symlink_metadata(path).unwrap();
141
+ json!({
142
+ "name": name,
143
+ "path": path.display().to_string(),
144
+ "uid": metadata_uid(&metadata),
145
+ "mode": metadata_mode(&metadata),
146
+ "device": metadata_device(&metadata)
147
+ })
148
+ })
149
+ .collect::<Vec<_>>();
150
+ if let Ok(metadata) = std::fs::symlink_metadata(state_file) {
151
+ facts.push(json!({
152
+ "name": "state_file",
153
+ "path": state_file.display().to_string(),
154
+ "uid": metadata_uid(&metadata),
155
+ "mode": metadata_mode(&metadata),
156
+ "device": metadata_device(&metadata)
157
+ }));
158
+ }
159
+ let payload = json!({
160
+ "raw_workspace": raw_workspace.display().to_string(),
161
+ "canonical_workspace": canonical_workspace.display().to_string(),
162
+ "resolved_state_file": state_file.display().to_string(),
163
+ "runtime_state_path": runtime_state.display().to_string(),
164
+ "facts": facts
165
+ });
166
+ let payload_bytes = serde_json::to_vec(&payload).unwrap();
167
+ let payload_sha256 = format!("{:x}", Sha256::digest(&payload_bytes));
168
+ let receipt = json!({
169
+ "schema": "mcp-state-provenance-v1",
170
+ "payload": payload,
171
+ "payload_sha256": payload_sha256
172
+ });
173
+ let receipt_bytes = serde_json::to_vec_pretty(&receipt).unwrap();
174
+ use std::io::Write;
175
+ let mut file = std::fs::OpenOptions::new()
176
+ .write(true)
177
+ .create_new(true)
178
+ .open(&self.receipt)
179
+ .unwrap();
180
+ file.write_all(&receipt_bytes).unwrap();
181
+ file.sync_all().unwrap();
182
+ self.receipt.clone()
183
+ }
184
+
185
+ fn under_root(&self, path: &Path) -> bool {
186
+ let candidate = if path.exists() {
187
+ std::fs::canonicalize(path).unwrap()
188
+ } else {
189
+ path.to_path_buf()
190
+ };
191
+ candidate.starts_with(&self.root)
192
+ }
193
+ }
194
+
195
+ impl Drop for McpStateFixture {
196
+ fn drop(&mut self) {
197
+ let _ = std::fs::remove_dir_all(&self.root);
198
+ }
199
+ }
200
+
201
+ #[cfg(unix)]
202
+ fn metadata_uid(metadata: &std::fs::Metadata) -> u32 {
203
+ use std::os::unix::fs::MetadataExt;
204
+ metadata.uid()
205
+ }
206
+
207
+ #[cfg(not(unix))]
208
+ fn metadata_uid(_: &std::fs::Metadata) -> u32 {
209
+ 0
210
+ }
211
+
212
+ #[cfg(unix)]
213
+ fn metadata_mode(metadata: &std::fs::Metadata) -> u32 {
214
+ use std::os::unix::fs::MetadataExt;
215
+ metadata.mode()
216
+ }
217
+
218
+ #[cfg(not(unix))]
219
+ fn metadata_mode(_: &std::fs::Metadata) -> u32 {
220
+ 0
221
+ }
222
+
223
+ #[cfg(unix)]
224
+ fn metadata_device(metadata: &std::fs::Metadata) -> u64 {
225
+ use std::os::unix::fs::MetadataExt;
226
+ metadata.dev()
227
+ }
228
+
229
+ #[cfg(not(unix))]
230
+ fn metadata_device(_: &std::fs::Metadata) -> u64 {
231
+ 0
232
+ }
233
+
40
234
  include!("tests/normalize.rs");
41
235
  include!("tests/wire.rs");
42
236
  include!("tests/send.rs");