@team-agent/installer 0.2.10 → 0.3.0

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 (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1077 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1141 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +436 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1063 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +487 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +685 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +388 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +537 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +582 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +656 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +90 -106
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -83
  203. package/src/team_agent/coordinator/lifecycle.py +0 -363
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -200
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -111
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -254
  255. package/src/team_agent/messaging/delivery.py +0 -473
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -457
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -86
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1239
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -143
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -602
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -0,0 +1,58 @@
1
+ //! step 5 · state — state.json 持久化 / identity / owner-gate / projection(真相源 `state.py`)。
2
+ //!
3
+ //! §6 step 5:原子写/锁/self-heal(**bug-084** 血泪)/ team projection / leader receiver identity /
4
+ //! owner-gate(trust own-vs-foreign,**§11/bug-064/082**)。owner-gate 的 tmux-liveness 依赖经
5
+ //! **trait 注入**,真探测延 step 9。
6
+ //!
7
+ //! **字节关键**:state.json = `json.dumps(state, indent=2, ensure_ascii=False)` —— pretty 2-空格 +
8
+ //! 非 ASCII 字面 + **无 sort_keys(插入序 → 靠 serde_json preserve_order)**,与 event_log 的
9
+ //! sort_keys/compact 截然不同。
10
+ //!
11
+ //! 本 slice 落地:`persist`(bug-084 韧性)。owner-gate / projection / identity 后续 slice。
12
+ //!
13
+ //! §10:无 unwrap/expect/panic;bug-084 的 os.replace 崩溃经 `Result` + 退避 + self-heal 处理,
14
+ //! 绝不 in-place truncate,绝不让审计/重试失败拖垮可见的原 state。
15
+ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
16
+
17
+ pub mod identity;
18
+ pub mod owner_gate;
19
+ pub mod persist;
20
+ pub mod projection;
21
+ pub mod selector;
22
+
23
+ use serde_json::Value;
24
+ use thiserror::Error;
25
+
26
+ /// Python 真值语义(`null`/`false`/`0`/`""`/`[]`/`{}` → false),`state` 模块共用。
27
+ pub(crate) fn json_truthy(v: &Value) -> bool {
28
+ match v {
29
+ Value::Null => false,
30
+ Value::Bool(b) => *b,
31
+ Value::Number(n) => n.as_f64().is_some_and(|f| f != 0.0),
32
+ Value::String(s) => !s.is_empty(),
33
+ Value::Array(a) => !a.is_empty(),
34
+ Value::Object(o) => !o.is_empty(),
35
+ }
36
+ }
37
+
38
+ #[derive(Debug, Error)]
39
+ pub enum StateError {
40
+ #[error("io: {0}")]
41
+ Io(#[from] std::io::Error),
42
+ #[error("json: {0}")]
43
+ Json(#[from] serde_json::Error),
44
+ #[error("{0} is locked by another team-agent process; serialize team-agent {0} calls and retry")]
45
+ Locked(String),
46
+ /// self-heal 也失败(原 state 仍可见);携带最终错误。
47
+ #[error("state save failed after self-heal: {0}")]
48
+ SaveFailed(String),
49
+ /// `select_runtime_state` team 选择失败(`team_agent.errors.RuntimeError` 等价):
50
+ /// 歧义 / 未找到。携带的 `String` == Python `str(exc)`(供 `resolve_team_scoped_state`
51
+ /// 透传为 `team_target_unresolved` 的 `error` 字段)。
52
+ #[error("{0}")]
53
+ TeamSelect(String),
54
+ /// identity 派生失败(leader_session_uuid 输入含 NUL,`derive_leader_session_uuid`
55
+ /// raise ValueError 等价)。文件系统拒绝 NUL,实践中不会触发。
56
+ #[error("identity: {0}")]
57
+ Identity(#[from] crate::model::errors::ModelError),
58
+ }
@@ -0,0 +1,423 @@
1
+ //! owner-gate:trust own-vs-foreign(§11 / bug-064 / bug-082 血泪;真相源 `state.py`)。
2
+ //!
3
+ //! 三核心纯判定(集成 send_message/MCP 是 step 11/14):
4
+ //! - `check_team_owner`:pane-id / uuid / **tmux-liveness** 身份门(pane-as-identity 模型)。
5
+ //! liveness 依赖经 [`PaneLiveness`] trait 注入,真 tmux 探测延 step 9。
6
+ //! - `apply_first_time_leader_binding` 的 **realpath own-vs-foreign**(`state.py:449`):
7
+ //! `canonicalize(pane_cwd) == canonicalize(workspace)` —— **两边都 canonical 化**,这正是
8
+ //! macOS `/tmp → /private/tmp` 软链不对称(0.2.11 真机抓出、单测漏掉的 bug)的正解。
9
+ //! **禁 basename / startswith / 子串 / 反推 cwd**(§11)。
10
+ //! - `worker_sender_bypasses_owner_gate`:worker peer-send 绕过 owner 门(sender 白名单 + env 一致)。
11
+ //!
12
+ //! own→自动答(本地 tmux 机械动作,MUST-NOT-13 零 provider 调用)+ trust 截断匹配是 **step 11**
13
+ //! messaging delivery(`attempt_trust_auto_answer` / `_tmux_pane_width`),不在此 slice。
14
+ //!
15
+ //! §10:纯判定无 unwrap/expect/panic;realpath 比对 canonicalize 失败时退化为词法绝对路径比较。
16
+
17
+ use std::path::Path;
18
+
19
+ use serde_json::{json, Value};
20
+
21
+ use crate::model::enums::PaneLiveness;
22
+
23
+ /// tmux pane 存活探测(`state.py:_tmux_pane_liveness`)。真实现走 tmux(step 9 transport);
24
+ /// owner-gate 单测用 mock 注入。
25
+ pub trait PaneLivenessProbe {
26
+ fn liveness(&self, pane_id: &str) -> PaneLiveness;
27
+ }
28
+
29
+ /// 调用方身份(`_caller_identity_from_env` 的产物;此处作为注入参数保持判定纯净)。
30
+ #[derive(Debug, Clone, Default)]
31
+ pub struct CallerIdentity {
32
+ pub pane_id: String,
33
+ pub provider: String,
34
+ pub machine_fingerprint: String,
35
+ pub leader_session_uuid: String,
36
+ /// `explicit-override` | `env` | `derived`(Python caller dict 的第 5 字段)。
37
+ pub leader_session_uuid_source: String,
38
+ }
39
+
40
+ /// `check_team_owner`(`state.py:366`)的纯判定版:caller 身份 + 是否有 TEAM_AGENT_ID + liveness 注入。
41
+ /// 返回 `None`=允许(own / 无 owner / 死 owner pane 可接管);`Some(dict)`=拒绝(team_owner_mismatch)。
42
+ pub fn check_team_owner(
43
+ state: &Value,
44
+ caller: &CallerIdentity,
45
+ has_team_agent_id: bool,
46
+ liveness: &dyn PaneLivenessProbe,
47
+ ) -> Option<Value> {
48
+ let owner = crate::state::projection::read_owner(state, None)?;
49
+ if !owner.is_object() || owner.as_object().is_none_or(serde_json::Map::is_empty) {
50
+ return None;
51
+ }
52
+ let owner_uuid = owner.get("leader_session_uuid").and_then(Value::as_str).unwrap_or("");
53
+ let owner_pane = owner.get("pane_id").and_then(Value::as_str).unwrap_or("");
54
+ let caller_uuid = caller.leader_session_uuid.as_str();
55
+ let caller_pane = caller.pane_id.as_str();
56
+
57
+ // 同 pane → own。
58
+ if !caller_pane.is_empty() && caller_pane == owner_pane {
59
+ return None;
60
+ }
61
+ // 死 owner pane + 新 live caller(非 worker)→ 接管(pane-as-identity:死 pane 不锁活 caller)。
62
+ if !caller_pane.is_empty()
63
+ && !has_team_agent_id
64
+ && !owner_pane.is_empty()
65
+ && liveness.liveness(owner_pane) != PaneLiveness::Live
66
+ {
67
+ return None;
68
+ }
69
+ // 同 uuid 且(无 caller_pane 或同 pane)→ own。
70
+ // 安全守卫(对抗 P0-A):**两边非空**才算同 uuid —— 堵住 owner_uuid=="" && caller_uuid=="" 的
71
+ // "" == "" allow-flip(否则缺 uuid 的 owner 会把外来空-uuid caller 误判为 own)。
72
+ // 注(对抗 verifier 确认的 P2 parity gap,安全侧):Python check_team_owner 开头调
73
+ // _migrate_team_identity 把缺 uuid 的 owner 填成 derived(故其 owner_uuid 恒非空);该迁移需
74
+ // env+workspace+可变 state,而本纯判定签名(`&Value` + 注入 liveness)尚不持有这些。
75
+ // identity::migrate_team_identity 已实现,但 check_team_owner 的实际 caller(env/workspace 来源)
76
+ // 在 step 11 messaging 才落地 —— 届时在此先 migrate owner identity 再判定,收口该 gap。
77
+ // 在此之前本守卫**偏严**:缺-uuid owner + 同-identity 异-pane caller 会被判 takeover(Python 判
78
+ // sticky/own)。**只过严不过松**(宁拒不误放),无安全洞。
79
+ let same_uuid = !owner_uuid.is_empty() && !caller_uuid.is_empty() && caller_uuid == owner_uuid;
80
+ if same_uuid && (caller_pane.is_empty() || caller_pane == owner_pane) {
81
+ return None;
82
+ }
83
+ // 否则拒绝。reason_kind / action 按 same_uuid 分流(sticky_bind vs takeover)。
84
+ let (reason_kind, action) = if same_uuid {
85
+ ("sticky_bind_collision", "team-agent claim-leader --confirm")
86
+ } else {
87
+ ("owner_takeover_required", "team-agent takeover --confirm")
88
+ };
89
+ // 键序对齐 Python dict 字面(preserve_order)。
90
+ Some(json!({
91
+ "ok": false,
92
+ "status": "refused",
93
+ "reason": "team_owner_mismatch",
94
+ "reason_kind": reason_kind,
95
+ "error": "not_owner",
96
+ "action": action,
97
+ "team_owner": owner.clone(),
98
+ "caller": caller_to_value(caller),
99
+ }))
100
+ }
101
+
102
+ fn caller_to_value(c: &CallerIdentity) -> Value {
103
+ // 字段集对齐 Python `_caller_identity_from_env`(5 键含 leader_session_uuid_source)。
104
+ json!({
105
+ "pane_id": c.pane_id,
106
+ "provider": c.provider,
107
+ "machine_fingerprint": c.machine_fingerprint,
108
+ "leader_session_uuid": c.leader_session_uuid,
109
+ "leader_session_uuid_source": c.leader_session_uuid_source,
110
+ })
111
+ }
112
+
113
+ /// `worker_sender_bypasses_owner_gate`(`state.py:400`):worker peer-send 绕过 owner 门。
114
+ /// 返回 `Some(agent_id)`=绕过;`None`=不绕过(走 owner 门)。
115
+ pub fn worker_sender_bypasses_owner_gate(
116
+ state: &Value,
117
+ sender: Option<&str>,
118
+ env_agent_id: Option<&str>,
119
+ ) -> Option<String> {
120
+ let sender = sender.filter(|s| !s.is_empty())?;
121
+ let leader_id = state
122
+ .get("leader")
123
+ .and_then(|l| l.get("id"))
124
+ .and_then(Value::as_str)
125
+ .filter(|s| !s.is_empty())
126
+ .unwrap_or("leader");
127
+ // leader 不绕过(走 owner 门)。
128
+ if sender == leader_id || sender == "leader" || sender == "Leader" {
129
+ return None;
130
+ }
131
+ // 必须是已注册 agent。
132
+ let is_agent = state
133
+ .get("agents")
134
+ .and_then(Value::as_object)
135
+ .is_some_and(|a| a.contains_key(sender));
136
+ if !is_agent {
137
+ return None;
138
+ }
139
+ // env TEAM_AGENT_ID 若存在必须与 sender 一致(防伪造)。
140
+ let env_agent_id = env_agent_id.filter(|s| !s.is_empty());
141
+ if let Some(env_id) = env_agent_id {
142
+ if env_id != sender {
143
+ return None;
144
+ }
145
+ return Some(env_id.to_string());
146
+ }
147
+ Some(sender.to_string())
148
+ }
149
+
150
+ /// §11 realpath own-vs-foreign(`state.py:449`):**两边 canonicalize** 后全等才判 own。
151
+ /// macOS `/tmp → /private/tmp` 软链不对称由「两边 canonicalize」消除。canonicalize 失败(路径不存在)
152
+ /// → 退化为词法绝对比较(此时无软链可解,等价)。**禁 basename/startswith/子串/反推**。
153
+ ///
154
+ /// **有意分歧(cr 裁决 2026-06-02,见 `contracts-rust-native.yaml` DECISIONS)**:Python 比的是
155
+ /// 供给的路径**串**(FS-无关、恒大小写敏感),在 case-insensitive FS 上把同一目录的大小写变体误判
156
+ /// 为 foreign(过严)。Rust 用 `canonicalize` 尊重 FS 真实大小写敏感性,两种 FS 下都正确:
157
+ /// - case-insensitive FS(macOS APFS 默认 / Windows NTFS):`Workspace`==`workspace`(同 inode)→ own;
158
+ /// - case-sensitive FS(Linux ext4 / case-sensitive APFS 卷):是两个目录 → foreign(与 Python 同)。
159
+ ///
160
+ /// 安全无洞:自己 workspace 的大小写变体本就是自己;runtime 用精确大小写启动 worker 不触发该场景;
161
+ /// §11 红线仍守(只 realpath 后**全等**才判 own,绝不前缀/子串/反推)。
162
+ /// 双 FS 正确性由 `tests::case_sensitivity_ruling_correct_on_both_fs` 运行时探测 FS 后各自钉死。
163
+ pub fn workspace_paths_match(a: &Path, b: &Path) -> bool {
164
+ realpath_like(a) == realpath_like(b)
165
+ }
166
+
167
+ fn lexical_abs(p: &Path) -> std::path::PathBuf {
168
+ if p.is_absolute() {
169
+ p.to_path_buf()
170
+ } else {
171
+ std::env::current_dir().map(|cwd| cwd.join(p)).unwrap_or_else(|_| p.to_path_buf())
172
+ }
173
+ }
174
+
175
+ /// `os.path.realpath` 等价(对抗 P0-B):全路径存在 → `canonicalize`;否则对**最长存在前缀**
176
+ /// canonicalize(解析其软链)再拼回缺失末段(规范化 `..`/`.`)。这复刻 Python「解析存在部分的
177
+ /// 软链 + 保留不存在末段」,修掉「父目录是软链 + 末段不存在 → 漏判 own」的 §11 血泪。
178
+ pub(crate) fn realpath_like(p: &Path) -> std::path::PathBuf {
179
+ use std::path::{Component, PathBuf};
180
+ if let Ok(c) = std::fs::canonicalize(p) {
181
+ return c; // 全路径存在(canonicalize 已解析所有软链 + 规范化 ..)
182
+ }
183
+ let abs = lexical_abs(p);
184
+ let comps: Vec<Component> = abs.components().collect();
185
+ // 从最长前缀往短试,找第一个可 canonicalize 的存在前缀。
186
+ for split in (1..=comps.len()).rev() {
187
+ let prefix: PathBuf = comps[..split].iter().collect();
188
+ if prefix.as_os_str().is_empty() {
189
+ continue;
190
+ }
191
+ if let Ok(canon) = std::fs::canonicalize(&prefix) {
192
+ let mut result = canon;
193
+ for comp in &comps[split..] {
194
+ match comp {
195
+ Component::ParentDir => {
196
+ result.pop();
197
+ }
198
+ Component::Normal(n) => result.push(n),
199
+ Component::CurDir => {}
200
+ Component::RootDir | Component::Prefix(_) => {}
201
+ }
202
+ }
203
+ return result;
204
+ }
205
+ }
206
+ abs // 无任何存在前缀(极少)→ 词法绝对(已含 lexical 形)
207
+ }
208
+
209
+ // 注:`apply_first_time_leader_binding` 的 realpath+命令双门由 `identity::apply_first_time_leader_binding`
210
+ // 字节对齐实现(含 `pane` 字段 + `repr()` 错误串)。早期此处曾有一个简化抽取 `first_time_binding_gate`
211
+ // (拒绝串用 `{:?}` 非 repr、缺 `pane`/workspace 字段),与 Python 不对拍且无生产 caller —— 对抗 verifier
212
+ // 确认为冗余分歧陷阱,已删除。`workspace_paths_match`/`realpath_like` 作为 §11 realpath 原语保留
213
+ // (step 11 messaging trust auto-answer 复用)。
214
+
215
+ #[cfg(test)]
216
+ mod tests {
217
+ #![allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
218
+ use super::*;
219
+
220
+ struct MockLiveness(PaneLiveness);
221
+ impl PaneLivenessProbe for MockLiveness {
222
+ fn liveness(&self, _pane_id: &str) -> PaneLiveness {
223
+ self.0
224
+ }
225
+ }
226
+ fn caller(pane: &str, uuid: &str) -> CallerIdentity {
227
+ CallerIdentity {
228
+ pane_id: pane.to_string(),
229
+ provider: "codex".to_string(),
230
+ machine_fingerprint: "m".to_string(),
231
+ leader_session_uuid: uuid.to_string(),
232
+ leader_session_uuid_source: "derived".to_string(),
233
+ }
234
+ }
235
+ const LIVE: MockLiveness = MockLiveness(PaneLiveness::Live);
236
+ const DEAD: MockLiveness = MockLiveness(PaneLiveness::Dead);
237
+ const UNKNOWN: MockLiveness = MockLiveness(PaneLiveness::Unknown);
238
+
239
+ fn state_owned(pane: &str, uuid: &str) -> Value {
240
+ json!({"team_owner": {"pane_id": pane, "leader_session_uuid": uuid, "provider": "codex"}})
241
+ }
242
+
243
+ #[test]
244
+ fn no_owner_allows() {
245
+ assert!(check_team_owner(&json!({}), &caller("%c", "u"), true, &LIVE).is_none());
246
+ assert!(check_team_owner(&json!({"team_owner": {}}), &caller("%c", "u"), true, &LIVE).is_none());
247
+ }
248
+
249
+ #[test]
250
+ fn same_pane_is_own() {
251
+ let s = state_owned("%owner", "uuid-owner");
252
+ assert!(check_team_owner(&s, &caller("%owner", "different-uuid"), true, &LIVE).is_none());
253
+ }
254
+
255
+ #[test]
256
+ fn foreign_pane_live_owner_refuses() {
257
+ let s = state_owned("%owner", "uuid-owner");
258
+ let r = check_team_owner(&s, &caller("%intruder", "uuid-intruder"), true, &LIVE).unwrap();
259
+ assert_eq!(r["reason"], json!("team_owner_mismatch"));
260
+ assert_eq!(r["reason_kind"], json!("owner_takeover_required"));
261
+ assert_eq!(r["action"], json!("team-agent takeover --confirm"));
262
+ assert_eq!(r["error"], json!("not_owner"));
263
+ assert_eq!(r["ok"], json!(false));
264
+ }
265
+
266
+ #[test]
267
+ fn dead_owner_pane_lets_new_live_caller_take_over() {
268
+ // bug:死 owner pane 不该锁住新 live caller(pane-as-identity)。
269
+ let s = state_owned("%owner", "uuid-owner");
270
+ // 非 worker(has_team_agent_id=false)+ 死 owner pane → 允许。
271
+ assert!(check_team_owner(&s, &caller("%new", "uuid-new"), false, &DEAD).is_none());
272
+ // 但 worker(has_team_agent_id=true)即使 owner pane 死也不接管 → 拒绝。
273
+ assert!(check_team_owner(&s, &caller("%new", "uuid-new"), true, &DEAD).is_some());
274
+ }
275
+
276
+ #[test]
277
+ fn same_uuid_no_pane_is_own_sticky_bind_when_pane_differs() {
278
+ let s = state_owned("%owner", "uuid-shared");
279
+ // 同 uuid 无 caller_pane → own。
280
+ assert!(check_team_owner(&s, &caller("", "uuid-shared"), true, &LIVE).is_none());
281
+ // 同 uuid 但不同 live pane → sticky_bind_collision 拒绝。
282
+ let r = check_team_owner(&s, &caller("%other", "uuid-shared"), true, &LIVE).unwrap();
283
+ assert_eq!(r["reason_kind"], json!("sticky_bind_collision"));
284
+ assert_eq!(r["action"], json!("team-agent claim-leader --confirm"));
285
+ }
286
+
287
+ #[test]
288
+ fn worker_bypass_rules() {
289
+ let s = json!({"leader": {"id": "leader"}, "agents": {"worker_a": {}, "worker_b": {}}});
290
+ // worker_a + env worker_a → 绕过。
291
+ assert_eq!(worker_sender_bypasses_owner_gate(&s, Some("worker_a"), Some("worker_a")), Some("worker_a".to_string()));
292
+ // leader → 不绕过(走 owner 门)。
293
+ assert_eq!(worker_sender_bypasses_owner_gate(&s, Some("leader"), Some("leader")), None);
294
+ // 未注册 agent → 不绕过。
295
+ assert_eq!(worker_sender_bypasses_owner_gate(&s, Some("ghost"), Some("ghost")), None);
296
+ // sender/env 不一致(伪造)→ 不绕过。
297
+ assert_eq!(worker_sender_bypasses_owner_gate(&s, Some("worker_b"), Some("worker_a")), None);
298
+ // 无 env_agent_id + 已注册 → 绕过(返回 sender)。
299
+ assert_eq!(worker_sender_bypasses_owner_gate(&s, Some("worker_a"), None), Some("worker_a".to_string()));
300
+ }
301
+
302
+ // §11 血泪:/tmp → /private/tmp 软链不对称(macOS 真机 bug)。两边 canonicalize → match。
303
+ #[test]
304
+ fn realpath_tmp_symlink_asymmetry_matches() {
305
+ // 真实存在的目录(canonicalize 需要路径存在)。
306
+ let dir = std::env::temp_dir().join(format!("ta_rs_og_{}", std::process::id()));
307
+ std::fs::create_dir_all(&dir).unwrap();
308
+ // /tmp/x 与 /private/tmp/x:macOS 上 /tmp 是 /private/tmp 的软链。
309
+ let raw = Path::new("/tmp").join(dir.file_name().unwrap());
310
+ let private = Path::new("/private/tmp").join(dir.file_name().unwrap());
311
+ if raw.exists() && private.exists() {
312
+ assert!(workspace_paths_match(&raw, &private), "/tmp 与 /private/tmp 应 canonicalize 相等");
313
+ }
314
+ // 自反:同路径 match。
315
+ assert!(workspace_paths_match(&dir, &dir));
316
+ }
317
+
318
+ #[test]
319
+ fn realpath_sibling_prefix_does_not_match() {
320
+ // §11 禁前缀:/repo 与 /repo-backup 共享前缀但是不同目录 → 不 match。
321
+ let base = std::env::temp_dir().join(format!("ta_rs_sib_{}", std::process::id()));
322
+ let repo = base.join("repo");
323
+ let repo_backup = base.join("repo-backup");
324
+ std::fs::create_dir_all(&repo).unwrap();
325
+ std::fs::create_dir_all(&repo_backup).unwrap();
326
+ assert!(!workspace_paths_match(&repo, &repo_backup), "共享前缀的兄弟目录不得 match");
327
+ // 子目录也不 match(禁子串/反推)。
328
+ let sub = repo.join("sub");
329
+ std::fs::create_dir_all(&sub).unwrap();
330
+ assert!(!workspace_paths_match(&repo, &sub));
331
+ }
332
+
333
+ // 对抗 P1:拒绝 dict 的 caller 含 5 字段(含 leader_session_uuid_source)。
334
+ #[test]
335
+ fn refusal_caller_has_five_fields() {
336
+ let s = state_owned("%owner", "uuid-owner");
337
+ let r = check_team_owner(&s, &caller("%intruder", "uuid-x"), true, &LIVE).unwrap();
338
+ let c = r["caller"].as_object().unwrap();
339
+ assert_eq!(c.len(), 5);
340
+ assert_eq!(r["caller"]["leader_session_uuid_source"], json!("derived"));
341
+ }
342
+
343
+ // 对抗 P0-A(security):owner 无 uuid + caller 无 uuid 不得 "" == "" allow-flip → 必拒绝。
344
+ #[test]
345
+ fn empty_uuids_do_not_allow_flip() {
346
+ // owner 缺 uuid(未迁移形),caller 也缺 uuid,pane 不同 → 必须拒绝(不得误判 own)。
347
+ let s = json!({"team_owner": {"pane_id": "%owner", "provider": "codex"}});
348
+ let mut c = caller("%intruder", "");
349
+ c.leader_session_uuid_source = "derived".to_string();
350
+ let r = check_team_owner(&s, &c, true, &LIVE);
351
+ assert!(r.is_some(), "两边空 uuid 不得 allow;必落拒绝");
352
+ assert_eq!(r.unwrap()["reason_kind"], json!("owner_takeover_required"), "非 sticky_bind(空 uuid 不算同)");
353
+ }
354
+
355
+ // 对抗 P2:owner pane liveness == Unknown 时,非 worker caller 也接管;worker 不接管。
356
+ #[test]
357
+ fn unknown_owner_pane_allows_nonworker_takeover() {
358
+ let s = state_owned("%owner", "uuid-owner");
359
+ assert!(check_team_owner(&s, &caller("%new", "uuid-new"), false, &UNKNOWN).is_none(), "Unknown owner pane + 非 worker → 接管");
360
+ assert!(check_team_owner(&s, &caller("%new", "uuid-new"), true, &UNKNOWN).is_some(), "worker 即便 Unknown 也不接管");
361
+ }
362
+
363
+ // 空 caller_pane + 异 uuid + 死 owner:死-owner 接管分支需 caller_pane 非空,故落拒绝。
364
+ #[test]
365
+ fn empty_caller_pane_diff_uuid_refuses_even_dead_owner() {
366
+ let s = state_owned("%owner", "uuid-owner");
367
+ let r = check_team_owner(&s, &caller("", "uuid-diff"), false, &DEAD);
368
+ assert!(r.is_some(), "空 caller_pane 不触发死-owner 接管;异 uuid → 拒绝");
369
+ }
370
+
371
+ // 对抗 P0-B(§11 血泪):父目录是软链 + 末段不存在 → realpath_like 解析父软链 → 判 own。
372
+ #[test]
373
+ fn parent_symlink_with_missing_leaf_matches() {
374
+ let base = std::env::temp_dir().join(format!("ta_rs_plink_{}", std::process::id()));
375
+ let real = base.join("real");
376
+ std::fs::create_dir_all(&real).unwrap();
377
+ let link = base.join("link");
378
+ let _ = std::fs::remove_file(&link);
379
+ #[cfg(unix)]
380
+ std::os::unix::fs::symlink(&real, &link).unwrap();
381
+ #[cfg(unix)]
382
+ {
383
+ // link/ghost 与 real/ghost:ghost 不存在,但 link 是 real 的软链 → 两边 realpath 相等。
384
+ assert!(
385
+ workspace_paths_match(&link.join("ghost"), &real.join("ghost")),
386
+ "父软链 + 缺失末段应判 own(realpath_like 解析父软链)"
387
+ );
388
+ // 不同末段仍不 match。
389
+ assert!(!workspace_paths_match(&link.join("ghostA"), &real.join("ghostB")));
390
+ }
391
+ }
392
+
393
+ // cr 裁决(2026-06-02)双 FS 正确性钉死(见 contracts-rust-native.yaml DECISIONS):
394
+ // 运行时探测当前 FS 大小写敏感性,对该 FS 断言对应的正确行为 —— 同一测试在两种 FS 都自证,
395
+ // 防「保留 canonicalize」这个有意分歧静默漂移。
396
+ #[test]
397
+ fn case_sensitivity_ruling_correct_on_both_fs() {
398
+ let base = std::env::temp_dir().join(format!("ta_rs_case_{}", std::process::id()));
399
+ let _ = std::fs::remove_dir_all(&base);
400
+ let upper = base.join("Workspace");
401
+ std::fs::create_dir_all(&upper).unwrap();
402
+ let lower_variant = base.join("workspace"); // 仅大小写不同
403
+
404
+ // 探测:小写变体能否解析到已建的大写目录?能 → case-insensitive FS。
405
+ let fs_case_insensitive = lower_variant.exists();
406
+
407
+ if fs_case_insensitive {
408
+ // case-insensitive FS(macOS APFS 默认 / Windows NTFS):同一 inode → 须判 own。
409
+ assert!(
410
+ workspace_paths_match(&upper, &lower_variant),
411
+ "case-insensitive FS:大小写变体是同一目录,canonicalize 须相等 → own"
412
+ );
413
+ } else {
414
+ // case-sensitive FS(Linux ext4 / case-sensitive APFS 卷):真造出第二个目录 → 须判 foreign。
415
+ std::fs::create_dir_all(&lower_variant).unwrap();
416
+ assert!(
417
+ !workspace_paths_match(&upper, &lower_variant),
418
+ "case-sensitive FS:大小写不同是两个目录 → foreign"
419
+ );
420
+ }
421
+ let _ = std::fs::remove_dir_all(&base);
422
+ }
423
+ }