@alpsckr/unitycli 0.4.2 → 0.4.4

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 (420) hide show
  1. package/README.md +5 -5
  2. package/contracts/command-contract.json +109 -217
  3. package/contracts/package-files.json +374 -294
  4. package/contracts/stable-session/direct-call-semantics.json +8 -2
  5. package/contracts/tool-completion-contract.json +148 -672
  6. package/dist/bridge/admission.d.ts +17 -1
  7. package/dist/bridge/admission.js +2 -0
  8. package/dist/bridge/admission.js.map +1 -1
  9. package/dist/bridge/domains/asset.js +3 -1
  10. package/dist/bridge/domains/asset.js.map +1 -1
  11. package/dist/bridge/domains/camera.js +39 -7
  12. package/dist/bridge/domains/camera.js.map +1 -1
  13. package/dist/bridge/domains/console.js +2 -1
  14. package/dist/bridge/domains/console.js.map +1 -1
  15. package/dist/bridge/domains/doctor.d.ts +3 -0
  16. package/dist/bridge/domains/doctor.js +8 -0
  17. package/dist/bridge/domains/doctor.js.map +1 -0
  18. package/dist/bridge/domains/editor.js +0 -4
  19. package/dist/bridge/domains/editor.js.map +1 -1
  20. package/dist/bridge/domains/game-object.js +4 -12
  21. package/dist/bridge/domains/game-object.js.map +1 -1
  22. package/dist/bridge/domains/graphics.js +1 -16
  23. package/dist/bridge/domains/graphics.js.map +1 -1
  24. package/dist/bridge/domains/object.js +3 -0
  25. package/dist/bridge/domains/object.js.map +1 -1
  26. package/dist/bridge/domains/physics.js +1 -2
  27. package/dist/bridge/domains/physics.js.map +1 -1
  28. package/dist/bridge/domains/profiler.js +18 -7
  29. package/dist/bridge/domains/profiler.js.map +1 -1
  30. package/dist/bridge/domains/project-tool.js +4 -0
  31. package/dist/bridge/domains/project-tool.js.map +1 -1
  32. package/dist/bridge/domains/reflection.js +10 -2
  33. package/dist/bridge/domains/reflection.js.map +1 -1
  34. package/dist/bridge/domains/runtime-tool.js +3 -0
  35. package/dist/bridge/domains/runtime-tool.js.map +1 -1
  36. package/dist/bridge/domains/scene.js +3 -8
  37. package/dist/bridge/domains/scene.js.map +1 -1
  38. package/dist/bridge/domains/screenshot.js +30 -6
  39. package/dist/bridge/domains/screenshot.js.map +1 -1
  40. package/dist/bridge/domains/ui.js +3 -1
  41. package/dist/bridge/domains/ui.js.map +1 -1
  42. package/dist/bridge/index.d.ts +5 -5
  43. package/dist/bridge/index.js +6 -9
  44. package/dist/bridge/index.js.map +1 -1
  45. package/dist/bridge/registry.js +3 -5
  46. package/dist/bridge/registry.js.map +1 -1
  47. package/dist/bridge/session-supervisor.d.ts +6 -3
  48. package/dist/bridge/session-supervisor.js +122 -34
  49. package/dist/bridge/session-supervisor.js.map +1 -1
  50. package/dist/bridge/types.d.ts +6 -5
  51. package/dist/bridge/types.js +16 -2
  52. package/dist/bridge/types.js.map +1 -1
  53. package/dist/catalog-protocol.d.ts +64 -0
  54. package/dist/catalog-protocol.js +460 -0
  55. package/dist/catalog-protocol.js.map +1 -0
  56. package/dist/cli-commands/command.js +84 -61
  57. package/dist/cli-commands/command.js.map +1 -1
  58. package/dist/cli-commands/extensions.js +70 -50
  59. package/dist/cli-commands/extensions.js.map +1 -1
  60. package/dist/cli-commands/instances.js +1 -1
  61. package/dist/cli-commands/instances.js.map +1 -1
  62. package/dist/cli-process-owners.d.ts +6 -0
  63. package/dist/cli-process-owners.js +27 -0
  64. package/dist/cli-process-owners.js.map +1 -0
  65. package/dist/cli.d.ts +1 -1
  66. package/dist/cli.js +20 -5
  67. package/dist/cli.js.map +1 -1
  68. package/dist/contracts/command-contract.js +14 -4
  69. package/dist/contracts/command-contract.js.map +1 -1
  70. package/dist/doctor.d.ts +4 -6
  71. package/dist/doctor.js +429 -398
  72. package/dist/doctor.js.map +1 -1
  73. package/dist/editor-blocker-observer.d.ts +2 -1
  74. package/dist/editor-blocker-observer.js +40 -13
  75. package/dist/editor-blocker-observer.js.map +1 -1
  76. package/dist/extension-availability.d.ts +27 -9
  77. package/dist/extension-availability.js +96 -56
  78. package/dist/extension-availability.js.map +1 -1
  79. package/dist/extensions/descriptor-loader.d.ts +2 -2
  80. package/dist/extensions/descriptor-loader.js +3 -4
  81. package/dist/extensions/descriptor-loader.js.map +1 -1
  82. package/dist/extensions/index.d.ts +1 -1
  83. package/dist/extensions/index.js +1 -1
  84. package/dist/extensions/index.js.map +1 -1
  85. package/dist/extensions/installer.d.ts +24 -7
  86. package/dist/extensions/installer.js +142 -66
  87. package/dist/extensions/installer.js.map +1 -1
  88. package/dist/extensions/registry.js +3 -5
  89. package/dist/extensions/registry.js.map +1 -1
  90. package/dist/extensions/state-contract.d.ts +11 -0
  91. package/dist/extensions/state-contract.js +50 -0
  92. package/dist/extensions/state-contract.js.map +1 -0
  93. package/dist/instances.d.ts +13 -1
  94. package/dist/instances.js +26 -0
  95. package/dist/instances.js.map +1 -1
  96. package/dist/package-manager.d.ts +0 -24
  97. package/dist/package-manager.js +5 -58
  98. package/dist/package-manager.js.map +1 -1
  99. package/dist/project-tool-discovery.d.ts +5 -2
  100. package/dist/project-tool-discovery.js +22 -18
  101. package/dist/project-tool-discovery.js.map +1 -1
  102. package/dist/runtime-code.js +1 -1
  103. package/dist/runtime-code.js.map +1 -1
  104. package/dist/tool-name.d.ts +4 -0
  105. package/dist/tool-name.js +15 -0
  106. package/dist/tool-name.js.map +1 -0
  107. package/dist/tools/.generated-completion-registry.d.ts +2 -0
  108. package/dist/tools/.generated-completion-registry.js +763 -325
  109. package/dist/tools/.generated-completion-registry.js.map +1 -1
  110. package/dist/tools/.generated-registry.d.ts +6 -11
  111. package/dist/tools/.generated-registry.js +6 -11
  112. package/dist/tools/.generated-registry.js.map +1 -1
  113. package/dist/tools/.generated-schemas.js +11 -16
  114. package/dist/tools/.generated-schemas.js.map +1 -1
  115. package/dist/tools/asset/refresh-status.d.ts +3 -0
  116. package/dist/tools/asset/refresh-status.js +12 -0
  117. package/dist/tools/asset/refresh-status.js.map +1 -0
  118. package/dist/tools/asset/refresh.js +7 -3
  119. package/dist/tools/asset/refresh.js.map +1 -1
  120. package/dist/tools/build/player-status.d.ts +3 -0
  121. package/dist/tools/build/player-status.js +12 -0
  122. package/dist/tools/build/player-status.js.map +1 -0
  123. package/dist/tools/build/run.js +1 -1
  124. package/dist/tools/build/run.js.map +1 -1
  125. package/dist/tools/catalog-control.d.ts +22 -0
  126. package/dist/tools/catalog-control.js +335 -0
  127. package/dist/tools/catalog-control.js.map +1 -0
  128. package/dist/tools/catalog.d.ts +7 -9
  129. package/dist/tools/catalog.js +66 -45
  130. package/dist/tools/catalog.js.map +1 -1
  131. package/dist/tools/commands.d.ts +1 -1
  132. package/dist/tools/commands.js +44 -11
  133. package/dist/tools/commands.js.map +1 -1
  134. package/dist/tools/define.js +3 -2
  135. package/dist/tools/define.js.map +1 -1
  136. package/dist/tools/dispatcher.d.ts +53 -6
  137. package/dist/tools/dispatcher.js +277 -108
  138. package/dist/tools/dispatcher.js.map +1 -1
  139. package/dist/tools/editor/compile.js +1 -1
  140. package/dist/tools/editor/compile.js.map +1 -1
  141. package/dist/tools/editor/play-mode-status.d.ts +3 -0
  142. package/dist/tools/editor/play-mode-status.js +12 -0
  143. package/dist/tools/editor/play-mode-status.js.map +1 -0
  144. package/dist/tools/editor/readiness.d.ts +0 -1
  145. package/dist/tools/editor/readiness.js +0 -1
  146. package/dist/tools/editor/readiness.js.map +1 -1
  147. package/dist/tools/editor/refresh.d.ts +7 -3
  148. package/dist/tools/editor/refresh.js +20 -15
  149. package/dist/tools/editor/refresh.js.map +1 -1
  150. package/dist/tools/editor/state.d.ts +2 -2
  151. package/dist/tools/editor/state.js +13 -12
  152. package/dist/tools/editor/state.js.map +1 -1
  153. package/dist/tools/metadata/derived.js +1 -7
  154. package/dist/tools/metadata/derived.js.map +1 -1
  155. package/dist/tools/metadata/status.js +1 -12
  156. package/dist/tools/metadata/status.js.map +1 -1
  157. package/dist/tools/metadata/types.d.ts +0 -2
  158. package/dist/tools/navmesh/live.js +1 -1
  159. package/dist/tools/navmesh/live.js.map +1 -1
  160. package/dist/tools/occlusion/live.js +1 -1
  161. package/dist/tools/occlusion/live.js.map +1 -1
  162. package/dist/tools/package/deploy.js +2 -2
  163. package/dist/tools/package/deploy.js.map +1 -1
  164. package/dist/tools/package/embed.js +1 -1
  165. package/dist/tools/package/embed.js.map +1 -1
  166. package/dist/tools/package/install-status.d.ts +3 -0
  167. package/dist/tools/package/install-status.js +12 -0
  168. package/dist/tools/package/install-status.js.map +1 -0
  169. package/dist/tools/package/install.js +3 -3
  170. package/dist/tools/package/install.js.map +1 -1
  171. package/dist/tools/package/live-resolve.js +45 -12
  172. package/dist/tools/package/live-resolve.js.map +1 -1
  173. package/dist/tools/package/registry-clear.js.map +1 -1
  174. package/dist/tools/package/registry-set.js.map +1 -1
  175. package/dist/tools/package/remove-status.d.ts +3 -0
  176. package/dist/tools/package/remove-status.js +12 -0
  177. package/dist/tools/package/remove-status.js.map +1 -0
  178. package/dist/tools/package/remove.js +3 -3
  179. package/dist/tools/package/remove.js.map +1 -1
  180. package/dist/tools/package/resolve.js +3 -3
  181. package/dist/tools/package/resolve.js.map +1 -1
  182. package/dist/tools/package/restore.js +2 -2
  183. package/dist/tools/package/restore.js.map +1 -1
  184. package/dist/tools/package/scoped-registry-add.js.map +1 -1
  185. package/dist/tools/status/support.d.ts +13 -0
  186. package/dist/tools/status/support.js +28 -0
  187. package/dist/tools/status/support.js.map +1 -0
  188. package/dist/tools/test/cancel.js +2 -2
  189. package/dist/tools/test/cancel.js.map +1 -1
  190. package/dist/tools/test/run-status.d.ts +3 -0
  191. package/dist/tools/test/run-status.js +12 -0
  192. package/dist/tools/test/run-status.js.map +1 -0
  193. package/dist/tools/test/run.js +7 -4
  194. package/dist/tools/test/run.js.map +1 -1
  195. package/dist/tools/tools/group-summaries.js +10 -14
  196. package/dist/tools/tools/group-summaries.js.map +1 -1
  197. package/docs/architecture.md +4 -4
  198. package/docs/availability-differences.md +4 -4
  199. package/docs/command-execution.md +4 -3
  200. package/docs/error-codes.md +13 -5
  201. package/docs/extensions.md +1 -7
  202. package/docs/risk-and-side-effects.md +4 -3
  203. package/docs/safety-boundaries.md +3 -1
  204. package/docs/target-and-instance.md +1 -1
  205. package/docs/tool-discovery.md +17 -10
  206. package/docs/tool-execution.md +18 -7
  207. package/extensions/cinemachine/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  208. package/extensions/hdrp/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  209. package/extensions/probuilder/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  210. package/extensions/urp/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  211. package/extensions/vfx-graph/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  212. package/package.json +2 -4
  213. package/skills/unitycli/SKILL.md +35 -42
  214. package/skills/unitycli/manifest.json +1 -1
  215. package/skills/unitycli/references/operation-protocol.md +3 -3
  216. package/unitypkg/Editor/AssemblyInfo.cs +5 -0
  217. package/unitypkg/Editor/Bridge/BridgeCommandRegistry.cs +49 -14
  218. package/unitypkg/Editor/Bridge/BridgeHttpServer.cs +9 -35
  219. package/unitypkg/Editor/Bridge/BridgeQueueState.cs +672 -0
  220. package/unitypkg/Editor/Bridge/BridgeWorkQueue.cs +970 -120
  221. package/unitypkg/Editor/BridgeStartupStateMachine.cs +59 -1
  222. package/unitypkg/Editor/Commands/ProjectToolCommands.cs +4 -0
  223. package/unitypkg/Editor/Commands/RuntimeToolCommands.cs +3 -0
  224. package/unitypkg/Editor/Generated/UnityCliToolCompletionRegistry.g.cs +379 -14
  225. package/unitypkg/Editor/Infrastructure/CodeExecSupport.cs +28 -13
  226. package/unitypkg/Editor/Infrastructure/RegistryOwnershipResolver.cs +132 -105
  227. package/unitypkg/Editor/Infrastructure/UnityCliActiveOperation.cs +138 -29
  228. package/unitypkg/Editor/Infrastructure/UnityCliAdmission.cs +1856 -237
  229. package/unitypkg/Editor/Infrastructure/UnityCliDoctorDiagnostics.cs +220 -0
  230. package/unitypkg/Editor/Infrastructure/UnityCliDurableStatusCoordinator.cs +118 -0
  231. package/unitypkg/Editor/Infrastructure/UnityCliEditorStateSnapshot.cs +2 -1
  232. package/unitypkg/Editor/Infrastructure/UnityCliHierarchyPager.cs +203 -0
  233. package/unitypkg/Editor/Infrastructure/UnityCliInstanceCleanup.cs +2 -1
  234. package/unitypkg/Editor/Infrastructure/UnityCliInstanceRegistry.cs +201 -63
  235. package/unitypkg/Editor/Infrastructure/UnityCliJson.cs +2 -3
  236. package/unitypkg/Editor/Infrastructure/UnityCliLiveBuildCoordinator.cs +289 -46
  237. package/unitypkg/Editor/Infrastructure/UnityCliLiveRefreshCoordinator.cs +87 -53
  238. package/unitypkg/Editor/Infrastructure/UnityCliNavMeshBakeCoordinator.cs +4 -4
  239. package/unitypkg/Editor/Infrastructure/UnityCliObjectRef.cs +128 -25
  240. package/unitypkg/Editor/Infrastructure/UnityCliObjectSerializer.cs +435 -327
  241. package/unitypkg/Editor/Infrastructure/UnityCliOcclusionBakeCoordinator.cs +4 -4
  242. package/unitypkg/Editor/Infrastructure/UnityCliOperationCoordinator.cs +351 -53
  243. package/unitypkg/Editor/Infrastructure/UnityCliPackageResolveCoordinator.cs +118 -18
  244. package/unitypkg/Editor/Infrastructure/UnityCliPackageResolveState.cs +20 -1
  245. package/unitypkg/Editor/Infrastructure/UnityCliReadGeneration.cs +97 -0
  246. package/unitypkg/Editor/Infrastructure/UnityCliReflection.cs +644 -133
  247. package/unitypkg/Editor/Infrastructure/UnityCliReloadOperation.cs +128 -41
  248. package/unitypkg/Editor/Infrastructure/UnityCliRuntimeExecutor.cs +14 -84
  249. package/unitypkg/Editor/Infrastructure/UnityCliSerializer.cs +220 -0
  250. package/unitypkg/Editor/Infrastructure/UnityCliSessionMutationLease.cs +69 -29
  251. package/unitypkg/Editor/Infrastructure/UnityCliTestRunCoordinator.cs +440 -54
  252. package/unitypkg/Editor/Infrastructure/UnityCliWaiter.cs +128 -35
  253. package/unitypkg/Editor/ProjectTools/IUnityCliTool.cs +9 -1
  254. package/unitypkg/Editor/ProjectTools/OfficialAttributeToolRegistry.cs +32 -31
  255. package/unitypkg/Editor/ProjectTools/ProjectToolRegistry.cs +607 -458
  256. package/unitypkg/Editor/ProjectTools/ProjectToolSchemaValidator.cs +35 -7
  257. package/unitypkg/Editor/ProjectTools/RegisteredTool.cs +501 -60
  258. package/unitypkg/Editor/ProjectTools/UnityCliProjectToolContext.cs +21 -0
  259. package/unitypkg/Editor/ProjectTools/UnityCliSchemaDeriver.cs +36 -7
  260. package/unitypkg/Editor/ProjectTools/UnityCliToolName.cs +30 -0
  261. package/unitypkg/Editor/Properties/AssemblyInfo.cs +3 -0
  262. package/unitypkg/Editor/Tools/asset/get-data.cs +16 -7
  263. package/unitypkg/Editor/Tools/build/BuildSceneStateTool.cs +23 -3
  264. package/unitypkg/Editor/Tools/build/BuildToolDtos.cs +2 -1
  265. package/unitypkg/Editor/Tools/build/scene/add.cs +6 -3
  266. package/unitypkg/Editor/Tools/build/scene/disable.cs +1 -1
  267. package/unitypkg/Editor/Tools/build/scene/enable.cs +1 -1
  268. package/unitypkg/Editor/Tools/build/scene/list.cs +1 -1
  269. package/unitypkg/Editor/Tools/build/scene/remove.cs +13 -3
  270. package/unitypkg/Editor/Tools/camera/CameraToolSupport.cs +38 -38
  271. package/unitypkg/Editor/Tools/camera/multiview.cs +13 -6
  272. package/unitypkg/Editor/Tools/camera/screenshot.cs +14 -7
  273. package/unitypkg/Editor/Tools/component/get.cs +16 -9
  274. package/unitypkg/Editor/Tools/component/modify.cs +6 -4
  275. package/unitypkg/Editor/Tools/console/clear.cs +7 -6
  276. package/unitypkg/Editor/Tools/console/get.cs +48 -16
  277. package/unitypkg/Editor/Tools/editor/EditorToolSupport.cs +143 -28
  278. package/unitypkg/Editor/Tools/editor/menu-items.cs +1 -1
  279. package/unitypkg/Editor/Tools/editor/play.cs +2 -11
  280. package/unitypkg/Editor/Tools/editor/selection.cs +1 -1
  281. package/unitypkg/Editor/Tools/editor/stop.cs +2 -11
  282. package/unitypkg/Editor/Tools/editor/windows.cs +1 -1
  283. package/unitypkg/Editor/Tools/go/GameObjectToolSupport.cs +54 -4
  284. package/unitypkg/Editor/Tools/go/delete.cs +1 -1
  285. package/unitypkg/Editor/Tools/go/find.cs +167 -70
  286. package/unitypkg/Editor/Tools/graphics/GraphicsToolSupport.cs +25 -0
  287. package/unitypkg/Editor/Tools/graphics/light-probe-group.cs +9 -3
  288. package/unitypkg/Editor/Tools/graphics/pipeline-info.cs +1 -1
  289. package/unitypkg/Editor/Tools/graphics/pipeline-settings.cs +1 -1
  290. package/unitypkg/Editor/Tools/graphics/quality.cs +2 -2
  291. package/unitypkg/Editor/Tools/graphics/reflection-probe.cs +3 -3
  292. package/unitypkg/Editor/Tools/graphics/stats.cs +1 -1
  293. package/unitypkg/Editor/Tools/object/ObjectToolSupport.cs +4 -4
  294. package/unitypkg/Editor/Tools/object/get-data.cs +9 -7
  295. package/unitypkg/Editor/Tools/object/modify.cs +7 -5
  296. package/unitypkg/Editor/Tools/physics/raycast.cs +24 -6
  297. package/unitypkg/Editor/Tools/prefab/stage.cs +1 -1
  298. package/unitypkg/Editor/Tools/profiler/ProfilerFrameDebuggerSupport.cs +94 -21
  299. package/unitypkg/Editor/Tools/profiler/ProfilerMemorySnapshotSupport.cs +397 -10
  300. package/unitypkg/Editor/Tools/profiler/ProfilerToolSupport.cs +18 -4
  301. package/unitypkg/Editor/Tools/profiler/frame-debugger/enable.cs +1 -1
  302. package/unitypkg/Editor/Tools/profiler/frame-debugger/events.cs +88 -87
  303. package/unitypkg/Editor/Tools/profiler/hierarchy.cs +278 -112
  304. package/unitypkg/Editor/Tools/profiler/memory-snapshot/compare.cs +9 -5
  305. package/unitypkg/Editor/Tools/profiler/memory-snapshot/list.cs +37 -35
  306. package/unitypkg/Editor/Tools/profiler/memory-snapshot/take.cs +99 -97
  307. package/unitypkg/Editor/Tools/project/info.cs +1 -1
  308. package/unitypkg/Editor/Tools/project/layers.cs +1 -1
  309. package/unitypkg/Editor/Tools/project/tags.cs +1 -1
  310. package/unitypkg/Editor/Tools/reflection/method/call.cs +9 -7
  311. package/unitypkg/Editor/Tools/reflection/method/find.cs +27 -37
  312. package/unitypkg/Editor/Tools/reflection/type/schema.cs +24 -28
  313. package/unitypkg/Editor/Tools/scene/gameobject/component.cs +8 -5
  314. package/unitypkg/Editor/Tools/scene/gameobject/components.cs +10 -7
  315. package/unitypkg/Editor/Tools/scene/gameobject-support.cs +104 -8
  316. package/unitypkg/Editor/Tools/scene/gameobject.cs +37 -16
  317. package/unitypkg/Editor/Tools/scene/hierarchy.cs +146 -142
  318. package/unitypkg/Editor/Tools/screenshot/EditorElementCaptureToolSupport.cs +77 -25
  319. package/unitypkg/Editor/Tools/screenshot/ScreenshotToolSupport.cs +315 -95
  320. package/unitypkg/Editor/Tools/screenshot/editor-element.cs +7 -5
  321. package/unitypkg/Editor/Tools/screenshot/game-view.cs +35 -21
  322. package/unitypkg/Editor/Tools/screenshot/isolated-object.cs +29 -23
  323. package/unitypkg/Editor/Tools/screenshot/orbit.cs +37 -25
  324. package/unitypkg/Editor/Tools/screenshot/scene-view.cs +25 -18
  325. package/unitypkg/Editor/Tools/screenshot/screen.cs +177 -17
  326. package/unitypkg/Editor/Tools/screenshot/surround.cs +83 -79
  327. package/unitypkg/Editor/Tools/ui/render-preview.cs +16 -11
  328. package/unitypkg/Editor/UnityCliBridge.cs +97 -34
  329. package/unitypkg/Runtime/Infrastructure/UnityCliRuntimeBridge.cs +753 -122
  330. package/unitypkg/Runtime/Properties/AssemblyInfo.cs +5 -0
  331. package/unitypkg/Runtime/UnityCli.Runtime.asmdef +1 -1
  332. package/unitypkg/Shared/AssemblyInfo.cs +4 -0
  333. package/unitypkg/Shared/UnityCliAdmissionPool.cs +265 -75
  334. package/unitypkg/Shared/UnityCliBridgeQueuePolicy.cs +410 -0
  335. package/unitypkg/Shared/UnityCliBridgeWorkItemState.cs +47 -0
  336. package/unitypkg/Shared/UnityCliExceptionProjection.cs +151 -0
  337. package/unitypkg/Shared/UnityCliQueuePublication.cs +30 -0
  338. package/unitypkg/Shared/UnityCliRequestBudget.cs +352 -0
  339. package/unitypkg/Shared/UnityCliRuntimeRequestPublication.cs +107 -0
  340. package/unitypkg/Tests/Editor/UnityCli.Editor.Tests.asmdef +2 -2
  341. package/unitypkg/Tests/Editor/UnityCliAdmissionCoordinatorIntegrationTests.cs +1329 -0
  342. package/unitypkg/Tests/Editor/UnityCliAdmissionPoolTests.cs +311 -114
  343. package/unitypkg/Tests/Editor/UnityCliAdmissionRepairStateTests.cs +94 -0
  344. package/unitypkg/Tests/Editor/UnityCliBridgeAdmissionIntegrationTests.cs +1229 -0
  345. package/unitypkg/Tests/Editor/UnityCliBridgeQueuePolicyTests.cs +412 -0
  346. package/unitypkg/Tests/Editor/UnityCliContinuationIntegrationTests.cs +292 -0
  347. package/unitypkg/Tests/Editor/UnityCliDiagnosticBoundaryTests.cs +120 -0
  348. package/unitypkg/Tests/Editor/UnityCliDurableStatusTests.cs +84 -0
  349. package/unitypkg/Tests/Editor/UnityCliExceptionProjectionTests.cs +72 -0
  350. package/unitypkg/Tests/Editor/UnityCliInstancePublicationRetryTests.cs +180 -97
  351. package/unitypkg/Tests/Editor/UnityCliMemoryReflectionIntegrationTests.cs +434 -0
  352. package/unitypkg/Tests/Editor/UnityCliRequestBudgetTests.cs +615 -0
  353. package/unitypkg/Tests/Editor/UnityCliRuntimeBridgeIntegrationTests.cs +520 -0
  354. package/unitypkg/Tests/Editor/UnityCliToolCatalogTests.cs +562 -0
  355. package/unitypkg/Tests/Runtime/UnityCli.Runtime.Tests.asmdef +23 -0
  356. package/unitypkg/Tests/Runtime/UnityCliRuntimeCatalogValidationTests.cs +29 -0
  357. package/unitypkg/package.json +1 -1
  358. package/contracts/static-capability-catalog.json +0 -48226
  359. package/dist/bridge/domains/diagnose.d.ts +0 -2
  360. package/dist/bridge/domains/diagnose.js +0 -11
  361. package/dist/bridge/domains/diagnose.js.map +0 -1
  362. package/dist/bridge/domains/extension.d.ts +0 -8
  363. package/dist/bridge/domains/extension.js +0 -20
  364. package/dist/bridge/domains/extension.js.map +0 -1
  365. package/dist/capabilities.d.ts +0 -5
  366. package/dist/capabilities.js +0 -84
  367. package/dist/capabilities.js.map +0 -1
  368. package/dist/static-capability-catalog.d.ts +0 -13
  369. package/dist/static-capability-catalog.js +0 -41
  370. package/dist/static-capability-catalog.js.map +0 -1
  371. package/dist/tools/diagnose/capabilities.d.ts +0 -13
  372. package/dist/tools/diagnose/capabilities.js +0 -34
  373. package/dist/tools/diagnose/capabilities.js.map +0 -1
  374. package/dist/tools/package/info.d.ts +0 -19
  375. package/dist/tools/package/info.js +0 -27
  376. package/dist/tools/package/info.js.map +0 -1
  377. package/dist/tools/package/list.d.ts +0 -27
  378. package/dist/tools/package/list.js +0 -21
  379. package/dist/tools/package/list.js.map +0 -1
  380. package/dist/tools/package/registry-get.d.ts +0 -10
  381. package/dist/tools/package/registry-get.js +0 -22
  382. package/dist/tools/package/registry-get.js.map +0 -1
  383. package/dist/tools/package/scoped-registry-list.d.ts +0 -14
  384. package/dist/tools/package/scoped-registry-list.js +0 -22
  385. package/dist/tools/package/scoped-registry-list.js.map +0 -1
  386. package/dist/tools/package/search.d.ts +0 -17
  387. package/dist/tools/package/search.js +0 -39
  388. package/dist/tools/package/search.js.map +0 -1
  389. package/dist/tools/package/status.d.ts +0 -21
  390. package/dist/tools/package/status.js +0 -37
  391. package/dist/tools/package/status.js.map +0 -1
  392. package/dist/tools/tools/describe.d.ts +0 -27
  393. package/dist/tools/tools/describe.js +0 -35
  394. package/dist/tools/tools/describe.js.map +0 -1
  395. package/dist/tools/tools/group-list.d.ts +0 -16
  396. package/dist/tools/tools/group-list.js +0 -38
  397. package/dist/tools/tools/group-list.js.map +0 -1
  398. package/dist/tools/tools/group.d.ts +0 -13
  399. package/dist/tools/tools/group.js +0 -34
  400. package/dist/tools/tools/group.js.map +0 -1
  401. package/dist/tools/tools/pagination.d.ts +0 -17
  402. package/dist/tools/tools/pagination.js +0 -28
  403. package/dist/tools/tools/pagination.js.map +0 -1
  404. package/dist/tools/tools/search.d.ts +0 -15
  405. package/dist/tools/tools/search.js +0 -42
  406. package/dist/tools/tools/search.js.map +0 -1
  407. package/unitypkg/Editor/Tools/diagnose/preflight.cs +0 -269
  408. package/unitypkg/Editor/Tools/editor/console.cs +0 -26
  409. package/unitypkg/Editor/Tools/extension/status.cs +0 -87
  410. package/unitypkg/Editor/Tools/go/inspect.cs +0 -49
  411. package/unitypkg/Editor/Tools/graphics/pipeline-quality.cs +0 -24
  412. package/unitypkg/Editor/Tools/graphics/probe-positions.cs +0 -58
  413. package/unitypkg/Editor/Tools/graphics/settings.cs +0 -104
  414. package/unitypkg/Editor/Tools/graphics/stats-counters.cs +0 -28
  415. package/unitypkg/Editor/Tools/graphics/stats-memory.cs +0 -21
  416. package/unitypkg/Editor/Tools/instances/list.cs +0 -71
  417. package/unitypkg/Editor/Tools/physics/raycast-all.cs +0 -67
  418. package/unitypkg/Editor/Tools/scene/build-settings.cs +0 -69
  419. package/unitypkg/Editor/Tools/scene/data.cs +0 -125
  420. package/unitypkg/Editor/Tools/tests/list.cs +0 -98
@@ -3,13 +3,11 @@ using System;
3
3
  using System.CodeDom.Compiler;
4
4
  using System.Collections.Generic;
5
5
  using System.Globalization;
6
- using System.IO;
7
6
  using System.Linq;
8
7
  using System.Net;
9
8
  using System.Reflection;
10
9
  using System.Security.Cryptography;
11
10
  using System.Text;
12
- using System.Text.RegularExpressions;
13
11
  using System.Threading;
14
12
  using System.Threading.Tasks;
15
13
  using Microsoft.CSharp;
@@ -29,45 +27,32 @@ namespace UnityCli
29
27
  {
30
28
  public static partial class UnityCliBridge
31
29
  {
32
- private const string ProjectToolFingerprintAlgorithm = "unity-cli-project-tool-fingerprint-v2";
33
30
  private const string OfficialAttributeToolPrefix = "UnityPipeline_";
34
31
  private const string OfficialAttributeToolGroup = "UnityPipeline";
35
- private static readonly Dictionary<string, RegisteredTool> ProjectTools = new Dictionary<string, RegisteredTool>(StringComparer.Ordinal);
36
- private static Dictionary<string, RegisteredTool> discoveryTools;
37
- private static RegistryOwnershipResolver registryOwnership;
32
+ private static UnityCliPublishedToolCatalog publishedProjectToolCatalog;
33
+ private static UnityCliToolCatalogHealthSnapshot projectToolCatalogHealth = new UnityCliToolCatalogHealthSnapshot(
34
+ false, 0, "catalog was not initialized", "Let Unity compile and reload the domain before retrying.");
35
+ private static int projectToolCatalogConstructionCount;
36
+ private static readonly byte[] projectToolCatalogCursorSecret = CreateProjectToolCatalogCursorSecret();
37
+ internal static IUnityCliToolCatalogSnapshotReader ProjectToolCatalogSnapshotReader { get; } = new PublishedToolCatalogSnapshotReader();
38
+ internal static IUnityCliToolCatalogHealthSnapshotReader ProjectToolCatalogHealthSnapshotReader { get; } = new PublishedToolCatalogHealthSnapshotReader();
38
39
 
39
- private static void RegisterProjectTool(RegisteredTool definition)
40
+ private sealed class PublishedToolCatalogSnapshotReader : IUnityCliToolCatalogSnapshotReader
40
41
  {
41
- if (definition == null || string.IsNullOrWhiteSpace(definition.Name) || string.IsNullOrWhiteSpace(definition.Summary) || definition.InvokeAsync == null)
42
- {
43
- throw new ArgumentException("Tool registration requires name, summary, and a typed async invoker.");
44
- }
45
- var target = discoveryTools ?? ProjectTools;
46
- if (target.ContainsKey(definition.Name)) throw new InvalidOperationException("Duplicate tool registration: " + definition.Name);
47
- if (definition.RequestType == null || definition.ResultType == null || string.IsNullOrWhiteSpace(definition.InputSchema) || string.IsNullOrWhiteSpace(definition.OutputSchema))
48
- {
49
- throw new ArgumentException("Tool registration requires request/result types and derived schemas: " + definition.Name);
50
- }
51
- if (definition.RequiredInput == null)
52
- {
53
- definition.RequiredInput = new string[0];
54
- }
55
- definition.Group = string.IsNullOrEmpty(definition.Group) ? ProjectToolGroupFromName(definition.Name) : definition.Group;
56
- definition.Target = string.IsNullOrEmpty(definition.Target) ? "editor" : definition.Target;
57
- definition.AvailabilityScope = string.IsNullOrEmpty(definition.AvailabilityScope) ? (definition.Target == "runtime" ? "playModeSession" : "editorSession") : definition.AvailabilityScope;
58
- definition.PlayModeSupport = NormalizeProjectToolPlayModeSupport(definition.PlayModeSupport, definition.Target);
59
- if (!string.Equals(definition.ContractProfile, "native", StringComparison.Ordinal)
60
- && !string.Equals(definition.ContractProfile, "official-attribute", StringComparison.Ordinal))
61
- {
62
- throw new ArgumentException("Tool registration requires a known contract profile: " + definition.Name);
63
- }
42
+ public UnityCliToolCatalogSnapshot Read() { return ReadPublishedProjectToolCatalogSnapshot(); }
43
+ }
44
+
45
+ private sealed class PublishedToolCatalogHealthSnapshotReader : IUnityCliToolCatalogHealthSnapshotReader
46
+ {
47
+ public UnityCliToolCatalogHealthSnapshot Read() { return Volatile.Read(ref projectToolCatalogHealth); }
48
+ }
49
+
50
+ private static void RegisterProjectTool(UnityCliToolCatalogBuilder builder, RegisteredTool definition, UnityCliToolProviderKind providerKind)
51
+ {
52
+ if (builder == null) throw new ArgumentNullException("builder");
53
+ if (definition == null) throw new ArgumentNullException("definition");
64
54
  ValidateOfficialAttributeNamespace(definition);
65
- definition.DefaultTimeoutMs = definition.DefaultTimeoutMs <= 0 ? 30000 : definition.DefaultTimeoutMs;
66
- definition.MaxTimeoutMs = definition.MaxTimeoutMs <= 0 ? 600000 : definition.MaxTimeoutMs;
67
- definition.RegisteredBy = string.IsNullOrEmpty(definition.RegisteredBy) ? "unity-editor-package" : definition.RegisteredBy;
68
- definition.DescriptorSource = string.IsNullOrEmpty(definition.DescriptorSource) ? "runtime-registration" : definition.DescriptorSource;
69
- RefreshProjectToolFingerprint(definition);
70
- target.Add(definition.Name, definition);
55
+ builder.Add(definition, providerKind);
71
56
  }
72
57
 
73
58
  /// <summary>保留namespace只属于official attribute投影;任一owner违规使整轮pending registry作废。</summary>
@@ -82,51 +67,125 @@ namespace UnityCli
82
67
  throw new InvalidOperationException("Native Tool cannot use the reserved UnityPipeline_ name prefix or UnityPipeline group: " + definition.Name + ".");
83
68
  }
84
69
 
85
- private static void DiscoverProjectTools()
70
+ /// <summary>InitializeOnLoad静态构造函数调用一次;局部pending完整成功后才原子发布。</summary>
71
+ private static void InitializeProjectToolCatalog()
86
72
  {
87
- if (projectToolsDiscovered) return;
88
-
89
- var pending = new Dictionary<string, RegisteredTool>(StringComparer.Ordinal);
73
+ if (projectToolCatalogConstructionCount != 0) return;
74
+ projectToolCatalogConstructionCount = 1;
75
+ var startedAt = DateTime.UtcNow;
76
+ var builder = new UnityCliToolCatalogBuilder();
90
77
  try
91
78
  {
92
- discoveryTools = pending;
93
- var assemblies = AppDomain.CurrentDomain.GetAssemblies();
94
- var officialRoots = ConfigureOfficialAuthoringApi(assemblies);
95
- var candidates = ToolProviderAssemblies(assemblies, officialRoots);
96
- registryOwnership = RegistryOwnershipResolver.Load(candidates);
97
- WriteLog("tool_discovery_started assemblies=" + assemblies.Length.ToString(CultureInfo.InvariantCulture)
98
- + " candidates=" + candidates.Count.ToString(CultureInfo.InvariantCulture));
99
- foreach (var assembly in candidates)
100
- {
101
- Type[] types;
102
- try { types = assembly.GetTypes(); }
103
- catch (ReflectionTypeLoadException exception) { throw new InvalidOperationException("Unable to inspect every type in Tool provider assembly " + assembly.FullName + ".", exception); }
104
- catch (Exception exception) { throw new InvalidOperationException("Unable to inspect Tool provider assembly " + assembly.FullName + ".", exception); }
105
- foreach (var type in types)
106
- {
107
- if (type == null) continue;
108
- TryRegisterInterfaceProjectTool(assembly, type);
109
- TryRegisterOfficialAttributeTools(assembly, type);
110
- }
111
- }
112
- WriteLog("tool_discovery_completed candidates=" + candidates.Count.ToString(CultureInfo.InvariantCulture)
113
- + " tools=" + pending.Count.ToString(CultureInfo.InvariantCulture));
114
-
115
- ProjectTools.Clear();
116
- foreach (var item in pending) ProjectTools.Add(item.Key, item.Value);
117
- projectToolsDiscovered = true;
79
+ CollectProjectToolCatalog(AppDomain.CurrentDomain.GetAssemblies(), builder);
80
+ PublishProjectToolCatalog(builder, (long)Math.Ceiling((DateTime.UtcNow - startedAt).TotalMilliseconds));
118
81
  }
119
82
  catch (Exception exception)
120
83
  {
121
- // 失败前从未写入公开 registry;下一次请求可在 Unity reload 后重新发现。
122
- throw new BridgeCommandException("E_TOOL_REGISTRY_UNHEALTHY", "Tool registry discovery failed: " + exception.Message,
123
- new Dictionary<string, object> { { "registry", "tools" } },
124
- new[] { "Fix the extension manifest or capability implementation, then retry discovery.", "Run unitycli extensions doctor --project <path> to inspect extension installation state." }, true, 1000);
84
+ builder.Abort();
85
+ var failure = BoundedCatalogDiagnostic(ExceptionChain(exception));
86
+ Volatile.Write(ref projectToolCatalogHealth, new UnityCliToolCatalogHealthSnapshot(
87
+ false, 0, failure, "Fix the Tool definition, then let Unity compile and reload the domain before retrying."));
88
+ WriteLog("tool_catalog_initialization_failed error=" + failure);
125
89
  }
126
- finally
90
+ }
91
+
92
+ /// <summary>生产与大目录fixture共享同一assembly筛选、owner解析、类型扫描和pending table构造。</summary>
93
+ private static void CollectProjectToolCatalog(Assembly[] assemblies, UnityCliToolCatalogBuilder builder)
94
+ {
95
+ if (assemblies == null) throw new ArgumentNullException("assemblies");
96
+ if (builder == null) throw new ArgumentNullException("builder");
97
+ var officialRoots = ConfigureOfficialAuthoringApi(assemblies);
98
+ var candidates = ToolProviderAssemblies(assemblies, officialRoots);
99
+ builder.BeginDiscovery(assemblies.Length, candidates.Count);
100
+ var ownership = RegistryOwnershipResolver.Load(candidates);
101
+ foreach (var assembly in candidates)
102
+ {
103
+ builder.RecordProviderMetadata(assembly.GetName().FullName);
104
+ Type[] types;
105
+ try { types = assembly.GetTypes(); }
106
+ catch (ReflectionTypeLoadException exception) { throw new InvalidOperationException("Unable to inspect every type in Tool provider assembly " + assembly.FullName + ".", exception); }
107
+ catch (Exception exception) { throw new InvalidOperationException("Unable to inspect Tool provider assembly " + assembly.FullName + ".", exception); }
108
+ foreach (var type in types)
109
+ {
110
+ if (type == null) continue;
111
+ TryRegisterInterfaceProjectTool(builder, ownership, assembly, type);
112
+ TryRegisterOfficialAttributeTools(builder, ownership, assembly, type);
113
+ }
114
+ }
115
+ ownership.ValidateComplete();
116
+ }
117
+
118
+ private static bool ProjectToolCatalogReady()
119
+ {
120
+ return Volatile.Read(ref publishedProjectToolCatalog) != null;
121
+ }
122
+
123
+ /// <summary>候选完整 Publish 后才替换唯一 slot;任何第 N 项失败都保留既有 publication。</summary>
124
+ private static UnityCliToolCatalogSnapshot PublishProjectToolCatalog(UnityCliToolCatalogBuilder builder, long elapsedMilliseconds)
125
+ {
126
+ var candidate = builder.Publish(elapsedMilliseconds, WriteProjectToolCatalogInitializedLog);
127
+ Volatile.Write(ref publishedProjectToolCatalog, candidate);
128
+ Volatile.Write(ref projectToolCatalogHealth, new UnityCliToolCatalogHealthSnapshot(
129
+ true, candidate.Snapshot.ToolCount, string.Empty, string.Empty));
130
+ return candidate.Snapshot;
131
+ }
132
+
133
+ /// <summary>候选提交前写入唯一 aggregate success log;失败时旧 publication 保持不变。</summary>
134
+ private static void WriteProjectToolCatalogInitializedLog(UnityCliToolCatalogSnapshot snapshot)
135
+ {
136
+ WriteLog("tool_catalog_initialized assemblies=" + snapshot.AssemblyCount.ToString(CultureInfo.InvariantCulture)
137
+ + " providers=" + snapshot.ProviderAssemblyCount.ToString(CultureInfo.InvariantCulture)
138
+ + " tools=" + snapshot.ToolCount.ToString(CultureInfo.InvariantCulture)
139
+ + " builtIn=" + snapshot.BuiltInToolCount.ToString(CultureInfo.InvariantCulture)
140
+ + " extension=" + snapshot.ExtensionToolCount.ToString(CultureInfo.InvariantCulture)
141
+ + " projectInterface=" + snapshot.ProjectInterfaceToolCount.ToString(CultureInfo.InvariantCulture)
142
+ + " officialAttribute=" + snapshot.OfficialAttributeToolCount.ToString(CultureInfo.InvariantCulture)
143
+ + " elapsedMs=" + snapshot.ElapsedMilliseconds.ToString(CultureInfo.InvariantCulture));
144
+ }
145
+
146
+ /// <summary>执行入口读取唯一 published executable table;失败 domain 内绝不重建。</summary>
147
+ private static UnityCliPublishedToolCatalog ReadPublishedProjectToolCatalog()
148
+ {
149
+ var catalog = Volatile.Read(ref publishedProjectToolCatalog);
150
+ if (catalog != null) return catalog;
151
+ var health = ProjectToolCatalogHealthSnapshotReader.Read();
152
+ throw new BridgeCommandException(
153
+ "E_TOOL_REGISTRY_UNHEALTHY",
154
+ "Tool catalog initialization failed in the current Unity domain: " + health.Failure,
155
+ new Dictionary<string, object> { { "registry", "tools" } },
156
+ new[] { health.NextAction },
157
+ true,
158
+ 1000);
159
+ }
160
+
161
+ private static UnityCliToolCatalogSnapshot ReadPublishedProjectToolCatalogSnapshot()
162
+ {
163
+ return ReadPublishedProjectToolCatalog().Snapshot;
164
+ }
165
+
166
+ /// <summary>doctor 专用只读 health,不取得 invoker、schema 对象或 preflight 聚合。</summary>
167
+ private static object ProjectToolCatalogHealth(UnityCli.Protocol.BridgeRequest request)
168
+ {
169
+ RequireProjectToolCatalogInputKeys(request);
170
+ var health = ProjectToolCatalogHealthSnapshotReader.Read();
171
+ var result = new Dictionary<string, object>
172
+ {
173
+ { "ready", health.Ready },
174
+ { "toolCount", (long)health.ToolCount }
175
+ };
176
+ if (!health.Ready)
127
177
  {
128
- discoveryTools = null;
178
+ result["failure"] = BoundedCatalogDiagnostic(health.Failure);
179
+ result["nextActions"] = new[] { health.NextAction };
129
180
  }
181
+ return result;
182
+ }
183
+
184
+ private static string BoundedCatalogDiagnostic(string value)
185
+ {
186
+ const int maxChars = 4096;
187
+ if (string.IsNullOrEmpty(value) || value.Length <= maxChars) return value ?? string.Empty;
188
+ return value.Substring(0, maxChars) + " [truncated]";
130
189
  }
131
190
 
132
191
  /// <summary>
@@ -161,7 +220,7 @@ namespace UnityCli
161
220
  return assemblies.Where(assembly => reachable.Contains(assembly.GetName().FullName)).ToList();
162
221
  }
163
222
 
164
- private static void TryRegisterInterfaceProjectTool(Assembly assembly, Type type)
223
+ private static void TryRegisterInterfaceProjectTool(UnityCliToolCatalogBuilder builder, RegistryOwnershipResolver ownershipResolver, Assembly assembly, Type type)
165
224
  {
166
225
  if (type == null || type.IsAbstract || type.IsInterface) return;
167
226
 
@@ -207,12 +266,12 @@ namespace UnityCli
207
266
  throw new InvalidOperationException("Tool descriptor is incomplete: " + type.FullName + ".");
208
267
 
209
268
  RegisteredTool definition;
210
- var ownership = registryOwnership.Resolve(type);
269
+ var ownership = ownershipResolver.Resolve(type, descriptor.Name);
211
270
  try
212
271
  {
213
272
  var factory = typeof(UnityCliBridge).GetMethod("CreateRegisteredTool", BindingFlags.Static | BindingFlags.NonPublic)
214
273
  .MakeGenericMethod(reqType, resType);
215
- definition = (RegisteredTool)factory.Invoke(null, new[] { instance, descriptor, assembly, type });
274
+ definition = (RegisteredTool)factory.Invoke(null, new object[] { instance, descriptor, ownership });
216
275
  }
217
276
  catch (Exception exception)
218
277
  {
@@ -220,72 +279,54 @@ namespace UnityCli
220
279
  WriteLog("custom_tool_registration_failed type=" + type.FullName + " error=" + diagnostics);
221
280
  throw new InvalidOperationException("Tool registration failed: " + type.FullName + ": " + diagnostics, exception);
222
281
  }
223
- RegisterProjectTool(definition);
224
- WriteLog("custom_tool_registered name=" + descriptor.Name + " source=IUnityCliTool type=" + type.FullName);
282
+ var providerKind = string.Equals(ownership.Source, "built-in", StringComparison.Ordinal)
283
+ ? UnityCliToolProviderKind.BuiltIn
284
+ : string.Equals(ownership.Source, "extension", StringComparison.Ordinal)
285
+ ? UnityCliToolProviderKind.Extension
286
+ : UnityCliToolProviderKind.ProjectInterface;
287
+ RegisterProjectTool(builder, definition, providerKind);
225
288
  }
226
289
 
227
290
  // 展开反射包装异常,确保 registry 失败能指向具体 schema/descriptor 根因。
228
291
  private static string ExceptionChain(Exception exception)
229
292
  {
293
+ const int maxDepth = 8;
230
294
  var messages = new List<string>();
231
- for (var current = exception; current != null; current = current.InnerException)
295
+ for (var current = exception; current != null && messages.Count < maxDepth; current = current.InnerException)
232
296
  {
233
297
  messages.Add(current.GetType().Name + ": " + current.Message);
234
298
  }
235
- return string.Join(" -> ", messages.ToArray());
299
+ return BoundedCatalogDiagnostic(string.Join(" -> ", messages.ToArray()));
236
300
  }
237
301
 
238
- private static RegisteredTool CreateRegisteredTool<TReq, TRes>(object instance, ToolDescriptor descriptor, Assembly assembly, Type implementationType)
302
+ private static RegisteredTool CreateRegisteredTool<TReq, TRes>(object instance, ToolDescriptor descriptor, RegistryToolOwnership ownership)
239
303
  {
240
304
  var tool = (IUnityCliTool<TReq, TRes>)instance;
241
- var ownership = registryOwnership.Resolve(implementationType);
242
305
  var completion = ResolveToolCompletionContract(descriptor, ownership);
243
- return new RegisteredTool
244
- {
245
- Name = descriptor.Name,
246
- Summary = descriptor.Summary,
247
- Description = descriptor.Description,
248
- Group = string.IsNullOrEmpty(descriptor.Group) ? "custom" : descriptor.Group,
249
- Descriptor = descriptor,
250
- Source = ownership,
251
- Ownership = ownership,
252
- InputSchema = UnityCliSchemaDeriver.DeriveSchema(typeof(TReq)),
253
- OutputSchema = UnityCliSchemaDeriver.DeriveSchema(typeof(TRes)),
254
- DestructiveHint = descriptor.DestructiveHint,
255
- SideEffects = descriptor.SideEffects ?? Array.Empty<string>(),
256
- SupportsDryRun = descriptor.SupportsDryRun,
257
- Permissions = descriptor.Permissions ?? Array.Empty<string>(),
258
- RequiresPackages = descriptor.RequiresPackages ?? Array.Empty<string>(),
259
- Examples = descriptor.Examples ?? Array.Empty<ToolExample>(),
260
- Target = string.IsNullOrEmpty(descriptor.Target) ? "editor" : descriptor.Target,
261
- PlayModeSupport = string.IsNullOrEmpty(descriptor.PlayModeSupport) ? "editor-only" : descriptor.PlayModeSupport,
262
- ContractProfile = "native",
263
- DefaultTimeoutMs = descriptor.DefaultTimeoutMs <= 0 ? 30000 : descriptor.DefaultTimeoutMs,
264
- MaxTimeoutMs = descriptor.MaxTimeoutMs <= 0 ? 600000 : descriptor.MaxTimeoutMs,
265
- RequiredInput = Array.Empty<string>(),
266
- RegisteredBy = "unity-editor-package",
267
- DescriptorSource = "IUnityCliTool",
268
- Assembly = assembly.GetName().Name,
269
- Method = implementationType.FullName + ".ExecuteAsync",
270
- Fingerprint = descriptor.Fingerprint,
271
- PinnedFingerprint = descriptor.Fingerprint,
272
- ExpectedFingerprint = descriptor.Fingerprint,
273
- RequestType = typeof(TReq),
274
- ResultType = typeof(TRes),
275
- CompletionContract = completion,
276
- InvokeAsync = async (request, context, cancellationToken) => (object)await tool.ExecuteAsync((TReq)request, context, cancellationToken)
277
- };
306
+ return new RegisteredTool(
307
+ descriptor,
308
+ ownership,
309
+ UnityCliSchemaDeriver.DeriveSchema(typeof(TReq)),
310
+ UnityCliSchemaDeriver.DeriveSchema(typeof(TRes)),
311
+ string.IsNullOrEmpty(descriptor.Target) || descriptor.Target != "runtime" ? "editorSession" : "playModeSession",
312
+ "native",
313
+ typeof(TReq),
314
+ typeof(TRes),
315
+ completion,
316
+ null,
317
+ async (request, context, cancellationToken) => (object)await tool.ExecuteAsync((TReq)request, context, cancellationToken));
278
318
  }
279
319
 
280
320
  /// <summary>built-in/Extension 只信任生成 registry;自定义 Tool 必须提供完整机器契约。</summary>
281
- private static UnityCliToolCompletionContract ResolveToolCompletionContract(ToolDescriptor descriptor, string ownership)
321
+ private static UnityCliToolCompletionContract ResolveToolCompletionContract(ToolDescriptor descriptor, RegistryToolOwnership ownership)
282
322
  {
283
323
  UnityCliToolCompletionContract generated;
284
324
  if (UnityCliToolCompletionRegistry.TryGet(descriptor.Name, out generated))
285
325
  {
286
326
  var generatedExtension = generated.DescriptorOwner.StartsWith("extension:", StringComparison.Ordinal);
287
- if ((generatedExtension && !string.Equals(ownership, "extension", StringComparison.Ordinal))
288
- || (!generatedExtension && !string.Equals(generated.DescriptorOwner, ownership, StringComparison.Ordinal)))
327
+ if ((generatedExtension && (!string.Equals(ownership.Source, "extension", StringComparison.Ordinal)
328
+ || !string.Equals(generated.DescriptorOwner, "extension:" + ownership.ExtensionId, StringComparison.Ordinal)))
329
+ || (!generatedExtension && !string.Equals(generated.DescriptorOwner, ownership.Source, StringComparison.Ordinal)))
289
330
  throw new InvalidOperationException("Completion contract owner mismatch for " + descriptor.Name + ".");
290
331
  var descriptorMutation = descriptor.DestructiveHint
291
332
  || descriptor.SupportsDryRun
@@ -314,7 +355,7 @@ namespace UnityCli
314
355
  || (descriptor.SideEffects != null && descriptor.SideEffects.Length > 0)
315
356
  || string.Equals(descriptor.PlayModeSupport, "session-affecting", StringComparison.Ordinal);
316
357
  return new UnityCliToolCompletionContract(
317
- descriptor.Name, "project", "direct-terminal", custom.ExecutionClass, custom.CompletionPredicate,
358
+ descriptor.Name, ownership.Source, "direct-terminal", custom.ExecutionClass, custom.CompletionPredicate,
318
359
  custom.ProofSource, custom.ReloadPolicy, custom.TimeoutError, custom.OutcomeUnknownPolicy,
319
360
  custom.CompletionDriverId, mutation ? "required" : "none", mutation);
320
361
  }
@@ -323,24 +364,373 @@ namespace UnityCli
323
364
  private static UnityCliToolCompletionContract ResolveRegisteredCustomToolCompletionContract(string toolName)
324
365
  {
325
366
  if (string.IsNullOrWhiteSpace(toolName)) return null;
326
- DiscoverProjectTools();
327
367
  RegisteredTool definition;
328
- return ProjectTools.TryGetValue(toolName, out definition) ? definition.CompletionContract : null;
368
+ return ReadPublishedProjectToolCatalog().ExecutableTools.TryGetValue(toolName, out definition) ? definition.CompletionContract : null;
369
+ }
370
+
371
+ private sealed class ProjectToolCatalogArgs
372
+ {
373
+ public string Group { get; set; }
374
+ public string Keyword { get; set; }
375
+ public int Limit { get; set; }
376
+ public string ExpectedCatalogGeneration { get; set; }
377
+ public string[] ReservedNames { get; set; }
378
+ public string[] LocalNames { get; set; }
379
+ public string CursorContext { get; set; }
380
+ public string CursorToken { get; set; }
381
+ public string Name { get; set; }
382
+ public string FallbackName { get; set; }
383
+ }
384
+
385
+ /// <summary>continuation只绑定transport session与listener generation;Tool级identifier不参与stale判定。</summary>
386
+ private static string ProjectToolCatalogGeneration()
387
+ {
388
+ return Sha256Hex(Encoding.UTF8.GetBytes(EditorSessionId() + "\n" + ListenerId + "\n" + CurrentListenerGeneration().ToString(CultureInfo.InvariantCulture)));
329
389
  }
330
390
 
331
- private static string ProjectToolGroupFromName(string name)
391
+ /// <summary>generation变化时 continuation 必须稳定失败,不能混合两代目录。</summary>
392
+ private static void RequireProjectToolCatalogGeneration(ProjectToolCatalogArgs args, string generation)
332
393
  {
333
- if (string.IsNullOrEmpty(name)) return "custom";
334
- var dot = name.IndexOf('.');
335
- return dot > 0 ? name.Substring(0, dot) : "custom";
394
+ if (args != null && Utf8ByteCount(args.ExpectedCatalogGeneration) > 128)
395
+ throw new BridgeCommandException("E_SCHEMA_INVALID", "Catalog generation exceeds its bounded string limit.");
396
+ if (args != null && !string.IsNullOrEmpty(args.ExpectedCatalogGeneration)
397
+ && !string.Equals(args.ExpectedCatalogGeneration, generation, StringComparison.Ordinal))
398
+ throw new BridgeCommandException("E_TOOL_CATALOG_CHANGED", "The Tool catalog generation changed before this continuation page was read.", null,
399
+ new[] { "Restart this discovery query from its first page against the current Unity generation." });
336
400
  }
337
401
 
402
+ /// <summary>package-local names由CLI冻结并传入,Bridge在筛选前执行全registry ownership检查。</summary>
403
+ private static void RequireNoReservedProjectToolNames(UnityCliToolCatalogSnapshot catalog, ProjectToolCatalogArgs args)
404
+ {
405
+ var reservedNames = (args == null ? null : args.ReservedNames) ?? new string[0];
406
+ if (reservedNames.Length > 4096 || reservedNames.Any(name => string.IsNullOrWhiteSpace(name) || Utf8ByteCount(name) > 256))
407
+ throw new BridgeCommandException("E_SCHEMA_INVALID", "Catalog reserved names exceed their bounded item or string limits.");
408
+ var reserved = new HashSet<string>(reservedNames, StringComparer.Ordinal);
409
+ var conflict = catalog.Descriptors.Select(tool => tool.Name).FirstOrDefault(reserved.Contains);
410
+ if (conflict != null)
411
+ throw new BridgeCommandException("E_TOOL_SCHEMA_CONFLICT", "Tool catalog conflict for " + conflict + ": package-local and live registrations use the same name.",
412
+ new Dictionary<string, object> { { "toolName", conflict }, { "reason", "package-local and live registrations use the same name" } },
413
+ new[] { "Fix the duplicate Tool registration in the selected Unity project, then retry discovery." });
414
+ }
415
+
416
+ private static IEnumerable<RegisteredToolDescriptor> ProjectToolCatalogDefinitions(UnityCliToolCatalogSnapshot catalog, bool runtimeOnly)
417
+ {
418
+ return catalog.Descriptors.Where(definition => !runtimeOnly || definition.Target == "runtime" || definition.Target == "both");
419
+ }
420
+
421
+ private static void RequireProjectToolCatalogInputKeys(UnityCli.Protocol.BridgeRequest request, params string[] allowed)
422
+ {
423
+ var input = request.Input as IDictionary<string, object>;
424
+ if (input == null) throw new BridgeCommandException("E_SCHEMA_INVALID", "Catalog input must be an object.");
425
+ var keys = new HashSet<string>(allowed, StringComparer.Ordinal);
426
+ var unknown = input.Keys.FirstOrDefault(key => !keys.Contains(key));
427
+ if (unknown != null) throw new BridgeCommandException("E_SCHEMA_INVALID", "Catalog input contains unknown field " + unknown + ".");
428
+ }
429
+
430
+ /// <summary>groups只读取一次published snapshot并返回group/count,不物化descriptor图。</summary>
431
+ private static object ProjectToolCatalogGroups(UnityCli.Protocol.BridgeRequest request, bool runtimeOnly)
432
+ {
433
+ RequireProjectToolCatalogInputKeys(request, "expectedCatalogGeneration", "reservedNames");
434
+ var catalog = ProjectToolCatalogSnapshotReader.Read();
435
+ var args = request.DeserializeArgs<ProjectToolCatalogArgs>() ?? new ProjectToolCatalogArgs();
436
+ RequireNoReservedProjectToolNames(catalog, args);
437
+ var generation = ProjectToolCatalogGeneration();
438
+ RequireProjectToolCatalogGeneration(args, generation);
439
+ var groups = ProjectToolCatalogDefinitions(catalog, runtimeOnly)
440
+ .GroupBy(definition => definition.Group, StringComparer.Ordinal)
441
+ .OrderBy(group => group.Key, StringComparer.Ordinal)
442
+ .Select(group => (object)new Dictionary<string, object> { { "name", group.Key }, { "toolCount", (long)group.Count() } })
443
+ .ToList();
444
+ if (groups.Count > 4096)
445
+ throw new BridgeCommandException("E_TOOL_REGISTRY_UNHEALTHY", "Tool catalog exceeds the bounded group count.", null,
446
+ new[] { "Reduce invalid project Tool group fragmentation, then let Unity reload the domain." });
447
+ var data = new Dictionary<string, object> { { "catalogGeneration", generation }, { "groups", groups } };
448
+ RequireCatalogDataBudget(data, 2 * 1024 * 1024, "Tool catalog groups exceed the bounded UTF-8 transport size.");
449
+ return data;
450
+ }
451
+
452
+ /// <summary>summary由目标domain签发opaque continuation;CLI不持有可编辑边界。</summary>
453
+ private static object ProjectToolCatalogSummary(UnityCli.Protocol.BridgeRequest request, bool runtimeOnly)
454
+ {
455
+ RequireProjectToolCatalogInputKeys(request, "group", "keyword", "limit", "expectedCatalogGeneration", "reservedNames",
456
+ "localNames", "cursorContext", "cursorToken");
457
+ var summaryInput = request.Input as IDictionary<string, object>;
458
+ if (!summaryInput.ContainsKey("reservedNames") || !summaryInput.ContainsKey("localNames") || !summaryInput.ContainsKey("cursorContext"))
459
+ throw new BridgeCommandException("E_SCHEMA_INVALID", "Catalog summary input is missing its cursor context fields.");
460
+ var catalog = ProjectToolCatalogSnapshotReader.Read();
461
+ var args = request.DeserializeArgs<ProjectToolCatalogArgs>() ?? new ProjectToolCatalogArgs();
462
+ if (args.Limit < 1 || args.Limit > 101)
463
+ throw new BridgeCommandException("E_SCHEMA_INVALID", "Catalog summary limit must be an integer in [1,101].");
464
+ RequireNoReservedProjectToolNames(catalog, args);
465
+ var generation = ProjectToolCatalogGeneration();
466
+ RequireProjectToolCatalogGeneration(args, generation);
467
+ if (Utf8ByteCount(args.Group) > 256 || Utf8ByteCount(args.Keyword) > 2048)
468
+ throw new BridgeCommandException("E_SCHEMA_INVALID", "Catalog summary filters exceed their bounded UTF-8 string limits.");
469
+ var tokens = (args.Keyword ?? string.Empty).Split('|').Select(token => token.Trim()).Where(token => token.Length > 0).ToArray();
470
+ if (tokens.Length > 16 || tokens.Any(token => Utf8ByteCount(token) > 128))
471
+ throw new BridgeCommandException("E_SCHEMA_INVALID", "Catalog search accepts at most 16 keywords of at most 128 UTF-8 bytes each.");
472
+ var matches = ProjectToolCatalogDefinitions(catalog, runtimeOnly)
473
+ .Where(definition => string.IsNullOrEmpty(args.Group) || string.Equals(definition.Group, args.Group, StringComparison.Ordinal))
474
+ .Where(definition => tokens.Length == 0 || tokens.Any(token => ProjectToolCatalogMatches(definition, token)))
475
+ .OrderBy(definition => definition.Name, StringComparer.Ordinal)
476
+ .ToList();
477
+ var nativeNames = new HashSet<string>(catalog.Descriptors.Where(definition => definition.ContractProfile != "official-attribute").Select(definition => definition.Name), StringComparer.Ordinal);
478
+ foreach (var reservedName in args.ReservedNames ?? new string[0]) nativeNames.Add(reservedName);
479
+
480
+ var localNames = ValidateCatalogCursorLocalNames(args);
481
+ var scope = ProjectToolCatalogCursorScope(generation, args, runtimeOnly, localNames);
482
+ int offset;
483
+ string boundary;
484
+ ReadProjectToolCatalogCursor(args.CursorToken, scope, out offset, out boundary);
485
+ var combinedNames = localNames.Concat(matches.Select(definition => definition.Name)).Distinct(StringComparer.Ordinal).OrderBy(name => name, StringComparer.Ordinal).ToList();
486
+ if (offset > combinedNames.Count || offset > 0 && !string.Equals(combinedNames[offset - 1], boundary, StringComparison.Ordinal))
487
+ throw InvalidProjectToolCatalogCursor();
488
+ var pageNames = combinedNames.Skip(offset).Take(args.Limit).ToList();
489
+ var pageSet = new HashSet<string>(pageNames, StringComparer.Ordinal);
490
+ var tools = matches.Where(definition => pageSet.Contains(definition.Name))
491
+ .Select(definition => (object)ProjectToolRoutingSummaryData(definition, runtimeOnly, nativeNames)).ToList();
492
+ return BoundSignedCatalogSummaryData(generation, matches.Count, combinedNames, offset, pageNames, tools, scope);
493
+ }
494
+
495
+ /// <summary>签名页在序列化预算内同步裁剪pageNames与live summaries,token指向真实末项。</summary>
496
+ private static object BoundSignedCatalogSummaryData(
497
+ string generation, int liveTotal, List<string> combinedNames, int offset, List<string> pageNames,
498
+ List<object> tools, string scope)
499
+ {
500
+ Dictionary<string, object> data;
501
+ while (true)
502
+ {
503
+ var end = offset + pageNames.Count;
504
+ var boundary = end > 0 ? combinedNames[end - 1] : string.Empty;
505
+ data = new Dictionary<string, object>
506
+ {
507
+ { "catalogGeneration", generation }, { "total", (long)liveTotal },
508
+ { "hasMore", end < combinedNames.Count }, { "tools", tools },
509
+ { "pageNames", pageNames.Cast<object>().ToList() },
510
+ { "continuationToken", SignProjectToolCatalogCursor(scope, end, boundary) }
511
+ };
512
+ if (CatalogDataUtf8Bytes(data) <= 256 * 1024) return data;
513
+ if (pageNames.Count == 0)
514
+ throw new BridgeCommandException("E_TOOL_REGISTRY_UNHEALTHY", "Tool catalog summary cannot fit the bounded UTF-8 transport size.");
515
+ var removed = pageNames[pageNames.Count - 1];
516
+ pageNames.RemoveAt(pageNames.Count - 1);
517
+ var toolIndex = tools.FindLastIndex(value => string.Equals(((Dictionary<string, object>)value)["name"] as string, removed, StringComparison.Ordinal));
518
+ if (toolIndex >= 0) tools.RemoveAt(toolIndex);
519
+ }
520
+ }
521
+
522
+ /// <summary>校验CLI提供的已筛选local名称只来自reserved ownership集合。</summary>
523
+ private static List<string> ValidateCatalogCursorLocalNames(ProjectToolCatalogArgs args)
524
+ {
525
+ var values = (args.LocalNames ?? new string[0]).ToList();
526
+ if (values.Count > 4096 || values.Any(value => string.IsNullOrWhiteSpace(value) || Utf8ByteCount(value) > 256)
527
+ || !values.SequenceEqual(values.Distinct(StringComparer.Ordinal).OrderBy(value => value, StringComparer.Ordinal)))
528
+ throw new BridgeCommandException("E_SCHEMA_INVALID", "Catalog local names must be a bounded ordinally sorted set.");
529
+ var reserved = new HashSet<string>(args.ReservedNames ?? new string[0], StringComparer.Ordinal);
530
+ if (values.Any(value => !reserved.Contains(value)))
531
+ throw new BridgeCommandException("E_SCHEMA_INVALID", "Catalog local names must be a subset of reserved package-local names.");
532
+ if (Utf8ByteCount(args.CursorContext) != 64 || args.CursorContext.Any(value => !Uri.IsHexDigit(value)))
533
+ throw new BridgeCommandException("E_SCHEMA_INVALID", "Catalog cursor context is invalid.");
534
+ return values;
535
+ }
536
+
537
+ /// <summary>把目标generation、query和local投影视图绑定到签名scope。</summary>
538
+ private static string ProjectToolCatalogCursorScope(string generation, ProjectToolCatalogArgs args, bool runtimeOnly, List<string> localNames)
539
+ {
540
+ return Sha256Hex(Encoding.UTF8.GetBytes(generation + "\n" + (runtimeOnly ? "runtime" : "editor") + "\n" + args.CursorContext + "\n"
541
+ + (args.Group ?? string.Empty) + "\n" + (args.Keyword ?? string.Empty) + "\n"
542
+ + string.Join("\n", localNames) + "\n--reserved--\n" + string.Join("\n", args.ReservedNames ?? new string[0])));
543
+ }
544
+
545
+ /// <summary>验证opaque token并恢复目标签发的序号与前页边界。</summary>
546
+ private static void ReadProjectToolCatalogCursor(string token, string scope, out int offset, out string boundary)
547
+ {
548
+ offset = 0;
549
+ boundary = string.Empty;
550
+ if (string.IsNullOrEmpty(token)) return;
551
+ if (Utf8ByteCount(token) > 8192) throw InvalidProjectToolCatalogCursor();
552
+ try
553
+ {
554
+ var parts = token.Split('.');
555
+ if (parts.Length != 2) throw InvalidProjectToolCatalogCursor();
556
+ var payloadBytes = FromBase64Url(parts[0]);
557
+ var signature = FromBase64Url(parts[1]);
558
+ byte[] expected;
559
+ using (var hmac = new HMACSHA256(projectToolCatalogCursorSecret)) expected = hmac.ComputeHash(Encoding.UTF8.GetBytes(parts[0]));
560
+ if (!FixedTimeEqual(signature, expected)) throw InvalidProjectToolCatalogCursor();
561
+ var fields = Encoding.UTF8.GetString(payloadBytes).Split('\n');
562
+ int parsedOffset;
563
+ if (fields.Length != 3 || !string.Equals(fields[0], scope, StringComparison.Ordinal)
564
+ || !int.TryParse(fields[1], NumberStyles.None, CultureInfo.InvariantCulture, out parsedOffset) || parsedOffset < 0)
565
+ throw InvalidProjectToolCatalogCursor();
566
+ offset = parsedOffset;
567
+ boundary = Encoding.UTF8.GetString(Convert.FromBase64String(fields[2]));
568
+ if (Utf8ByteCount(boundary) > 256 || (offset == 0) != string.IsNullOrEmpty(boundary))
569
+ throw InvalidProjectToolCatalogCursor();
570
+ }
571
+ catch (BridgeCommandException) { throw; }
572
+ catch { throw InvalidProjectToolCatalogCursor(); }
573
+ }
574
+
575
+ /// <summary>使用当前domain secret签发不可由CLI改写的continuation。</summary>
576
+ private static string SignProjectToolCatalogCursor(string scope, int offset, string boundary)
577
+ {
578
+ var payloadText = scope + "\n" + offset.ToString(CultureInfo.InvariantCulture) + "\n"
579
+ + Convert.ToBase64String(Encoding.UTF8.GetBytes(boundary ?? string.Empty));
580
+ var payload = ToBase64Url(Encoding.UTF8.GetBytes(payloadText));
581
+ byte[] signature;
582
+ using (var hmac = new HMACSHA256(projectToolCatalogCursorSecret)) signature = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
583
+ return payload + "." + ToBase64Url(signature);
584
+ }
585
+
586
+ /// <summary>统一返回可机器分支的cursor拒绝错误。</summary>
587
+ private static BridgeCommandException InvalidProjectToolCatalogCursor()
588
+ {
589
+ return new BridgeCommandException("E_DISCOVERY_CURSOR_INVALID", "The target rejected an altered or cross-query discovery continuation.", null,
590
+ new[] { "Restart this exact discovery query from its first page." });
591
+ }
592
+
593
+ /// <summary>secret只活在当前domain内,reload后旧token自然失效。</summary>
594
+ private static byte[] CreateProjectToolCatalogCursorSecret()
595
+ {
596
+ var bytes = new byte[32];
597
+ using (var random = RandomNumberGenerator.Create()) random.GetBytes(bytes);
598
+ return bytes;
599
+ }
600
+
601
+ /// <summary>编码URL安全token片段。</summary>
602
+ private static string ToBase64Url(byte[] value)
603
+ {
604
+ return Convert.ToBase64String(value).TrimEnd('=').Replace('+', '-').Replace('/', '_');
605
+ }
606
+
607
+ /// <summary>解码URL安全token片段。</summary>
608
+ private static byte[] FromBase64Url(string value)
609
+ {
610
+ var normalized = value.Replace('-', '+').Replace('_', '/');
611
+ normalized = normalized.PadRight(normalized.Length + (4 - normalized.Length % 4) % 4, '=');
612
+ return Convert.FromBase64String(normalized);
613
+ }
614
+
615
+ /// <summary>常量步数比较签名,避免提前返回泄露首个差异位置。</summary>
616
+ private static bool FixedTimeEqual(byte[] left, byte[] right)
617
+ {
618
+ if (left == null || right == null || left.Length != right.Length) return false;
619
+ var difference = 0;
620
+ for (var index = 0; index < left.Length; index++) difference |= left[index] ^ right[index];
621
+ return difference == 0;
622
+ }
623
+
624
+ /// <summary>所有catalog string bound统一按wire UTF-8字节计算。</summary>
625
+ private static int Utf8ByteCount(string value)
626
+ {
627
+ return Encoding.UTF8.GetByteCount(value ?? string.Empty);
628
+ }
629
+
630
+ /// <summary>以真实Bridge serializer计算完整data大小。</summary>
631
+ private static int CatalogDataUtf8Bytes(object data)
632
+ {
633
+ return Encoding.UTF8.GetByteCount(UnityCliSerializer.Serialize(data));
634
+ }
635
+
636
+ /// <summary>完整data超出consumer预算时在返回前失败。</summary>
637
+ private static void RequireCatalogDataBudget(object data, int maximumBytes, string message)
638
+ {
639
+ if (CatalogDataUtf8Bytes(data) > maximumBytes) throw new BridgeCommandException("E_TOOL_REGISTRY_UNHEALTHY", message);
640
+ }
641
+
642
+ private static bool ProjectToolCatalogMatches(RegisteredToolDescriptor definition, string token)
643
+ {
644
+ return (definition.Name ?? string.Empty).IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0
645
+ || (definition.Summary ?? string.Empty).IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0
646
+ || (definition.Description ?? string.Empty).IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0
647
+ || (definition.Group ?? string.Empty).IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0;
648
+ }
649
+
650
+ /// <summary>exact describe只读取一次published snapshot;native未命中时可返回一份指定fallback descriptor。</summary>
651
+ private static object ProjectToolCatalogDescribe(UnityCli.Protocol.BridgeRequest request, bool runtimeOnly)
652
+ {
653
+ if (runtimeOnly) RequireProjectToolCatalogInputKeys(request, "name");
654
+ else RequireProjectToolCatalogInputKeys(request, "name", "fallbackName");
655
+ var catalog = ProjectToolCatalogSnapshotReader.Read();
656
+ var args = request.DeserializeArgs<ProjectToolCatalogArgs>() ?? new ProjectToolCatalogArgs();
657
+ if (string.IsNullOrWhiteSpace(args.Name) || Utf8ByteCount(args.Name) > 256)
658
+ throw new BridgeCommandException("E_SCHEMA_INVALID", "Catalog describe name is required and must not exceed 256 UTF-8 bytes.");
659
+ if (args.FallbackName != null && (string.IsNullOrWhiteSpace(args.FallbackName) || Utf8ByteCount(args.FallbackName) > 256
660
+ || string.Equals(args.Name, args.FallbackName, StringComparison.Ordinal)))
661
+ throw new BridgeCommandException("E_SCHEMA_INVALID", "Catalog describe fallbackName must be distinct and must not exceed 256 UTF-8 bytes.");
662
+ var generation = ProjectToolCatalogGeneration();
663
+ Func<RegisteredToolDescriptor, bool> availableForTarget = tool => !runtimeOnly || tool.Target == "runtime" || tool.Target == "both";
664
+ var definition = catalog.Descriptors.FirstOrDefault(tool => string.Equals(tool.Name, args.Name, StringComparison.Ordinal) && availableForTarget(tool));
665
+ if (definition == null && args.FallbackName != null)
666
+ definition = catalog.Descriptors.FirstOrDefault(tool => string.Equals(tool.Name, args.FallbackName, StringComparison.Ordinal) && availableForTarget(tool));
667
+ var found = definition != null;
668
+ var result = new Dictionary<string, object> { { "catalogGeneration", generation }, { "found", found } };
669
+ if (found)
670
+ {
671
+ result["descriptor"] = ProjectToolDescriptorData(definition);
672
+ }
673
+ if (CatalogDataUtf8Bytes(result) > 3 * 1024 * 1024)
674
+ throw new BridgeCommandException("E_TOOL_REGISTRY_UNHEALTHY", "Exact Tool descriptor exceeds the bounded UTF-8 transport size.", null,
675
+ new[] { "Reduce the Tool schema or examples, then let Unity reload the domain." });
676
+ return result;
677
+ }
678
+
679
+ private static Dictionary<string, object> ProjectToolRoutingSummaryData(RegisteredToolDescriptor definition, bool runtimeOnly, HashSet<string> nativeNames)
680
+ {
681
+ var commandName = definition.Name;
682
+ if (definition.ContractProfile == "official-attribute" && definition.Name.StartsWith(OfficialAttributeToolPrefix, StringComparison.Ordinal))
683
+ {
684
+ var originalName = definition.Name.Substring(OfficialAttributeToolPrefix.Length);
685
+ if (!nativeNames.Contains(originalName)) commandName = originalName;
686
+ }
687
+ var available = ProjectToolAvailableForCatalog(definition, runtimeOnly);
688
+ var data = new Dictionary<string, object>
689
+ {
690
+ { "name", definition.Name }, { "summary", definition.Summary }, { "group", definition.Group },
691
+ { "target", definition.Target }, { "contractProfile", definition.ContractProfile },
692
+ { "commandName", commandName }
693
+ };
694
+ if (!available)
695
+ {
696
+ data["available"] = false;
697
+ data["unavailableReason"] = ProjectToolUnavailableForCatalog(definition, runtimeOnly);
698
+ }
699
+ return data;
700
+ }
701
+
702
+ private static bool ProjectToolAvailableForCatalog(RegisteredToolDescriptor definition, bool runtimeOnly)
703
+ {
704
+ return runtimeOnly ? playModeReady : ToolAvailableNow(definition);
705
+ }
706
+
707
+ private static string ProjectToolUnavailableForCatalog(RegisteredToolDescriptor definition, bool runtimeOnly)
708
+ {
709
+ return runtimeOnly && !playModeReady
710
+ ? "E_RUNTIME_NOT_READY: Enter Play Mode before executing this Tool."
711
+ : ToolUnavailableReason(definition);
712
+ }
713
+
714
+ private static bool IsProjectToolCatalogReadCommand(string command)
715
+ {
716
+ return command == "project-tool.health" || command == "project-tool.groups" || command == "project-tool.summary" || command == "project-tool.describe" || command == "project-tool.list"
717
+ || command == "runtime-tool.groups" || command == "runtime-tool.summary" || command == "runtime-tool.describe" || command == "runtime-tool.list";
718
+ }
719
+
720
+ private static object ProjectToolGroups(UnityCli.Protocol.BridgeRequest request) { return ProjectToolCatalogGroups(request, false); }
721
+ private static object ProjectToolSummary(UnityCli.Protocol.BridgeRequest request) { return ProjectToolCatalogSummary(request, false); }
722
+ private static object ProjectToolDescribe(UnityCli.Protocol.BridgeRequest request) { return ProjectToolCatalogDescribe(request, false); }
723
+ private static object RuntimeToolGroups(UnityCli.Protocol.BridgeRequest request) { InitializeRuntimeExecutor(); return ProjectToolCatalogGroups(request, true); }
724
+ private static object RuntimeToolSummary(UnityCli.Protocol.BridgeRequest request) { InitializeRuntimeExecutor(); return ProjectToolCatalogSummary(request, true); }
725
+ private static object RuntimeToolDescribe(UnityCli.Protocol.BridgeRequest request) { InitializeRuntimeExecutor(); return ProjectToolCatalogDescribe(request, true); }
726
+
727
+ /// <summary>完整descriptor导出仅供显式doctor/static capture,不进入公开discovery。</summary>
338
728
  private static object ProjectToolList(UnityCli.Protocol.BridgeRequest request)
339
729
  {
340
- DiscoverProjectTools();
730
+ var catalog = ReadPublishedProjectToolCatalogSnapshot();
341
731
  return new Dictionary<string, object>
342
732
  {
343
- { "tools", ProjectTools.Values.OrderBy(tool => tool.Name, StringComparer.Ordinal).Select(ProjectToolDescriptorData).ToList() },
733
+ { "tools", catalog.Descriptors.Select(ProjectToolDescriptorData).ToList() },
344
734
  { "logPath", LogPath() }
345
735
  };
346
736
  }
@@ -349,33 +739,21 @@ namespace UnityCli
349
739
  {
350
740
  public string Name { get; set; }
351
741
  [UnityCliAnyJson] public object Input { get; set; }
352
- public string ExpectedDescriptorFingerprint { get; set; }
353
742
  public string ArtifactDir { get; set; }
354
743
  public string ProjectPath { get; set; }
355
744
  }
356
745
 
357
746
  private static async ValueTask<object> ProjectToolRun(UnityCli.Protocol.BridgeRequest request, CancellationToken cancellationToken)
358
747
  {
359
- DiscoverProjectTools();
748
+ var catalog = ReadPublishedProjectToolCatalog();
360
749
  var args = request.DeserializeArgs<ProjectToolRunArgs>();
361
750
  var name = args.Name;
362
- if (string.IsNullOrEmpty(name) || !ProjectTools.ContainsKey(name))
751
+ if (string.IsNullOrEmpty(name) || !catalog.ExecutableTools.ContainsKey(name))
363
752
  {
364
753
  throw new BridgeCommandException("E_TOOL_NOT_FOUND", "Tool not found: " + name, "Run unitycli tools group --instance <id|projectPath|alias> or tools search <keyword> to inspect the connected instance catalog.");
365
754
  }
366
755
 
367
- var definition = ProjectTools[name];
368
- // 门禁前刷新 live handler+descriptor 指纹,旧 discovery proof 在 domain reload/descriptor 变化后必须失效。
369
- var fingerprintCheck = RefreshProjectToolFingerprint(definition);
370
- if (!string.IsNullOrEmpty(args.ExpectedDescriptorFingerprint)
371
- && !string.Equals(args.ExpectedDescriptorFingerprint, definition.Fingerprint, StringComparison.Ordinal))
372
- {
373
- throw new BridgeCommandException(
374
- "E_TOOL_DESCRIPTOR_CHANGED",
375
- "The Tool descriptor changed after discovery: " + name,
376
- new Dictionary<string, object> { { "toolName", name }, { "expectedFingerprint", args.ExpectedDescriptorFingerprint }, { "actualFingerprint", definition.Fingerprint } },
377
- new[] { "Run tools describe again against the selected Unity generation before constructing a new request." });
378
- }
756
+ var definition = catalog.ExecutableTools[name];
379
757
  if (EditorApplication.isPlaying && string.Equals(definition.PlayModeSupport, "editor-only", StringComparison.OrdinalIgnoreCase))
380
758
  {
381
759
  throw new BridgeCommandException("E_TOOL_PLAYMODE_UNAVAILABLE",
@@ -410,11 +788,6 @@ namespace UnityCli
410
788
  new[] { "Use runtime-tool.run <name> or tools run <name> with a runtime target to execute this tool in Play Mode." });
411
789
  }
412
790
  var dryRun = request.Execution != null && request.Execution.DryRun == true;
413
- if (fingerprintCheck.Blocked)
414
- {
415
- WriteLog("custom_tool_fingerprint_blocked requestId=" + request.RequestId + " name=" + name + " expected=" + fingerprintCheck.Expected + " actual=" + fingerprintCheck.Actual);
416
- throw new BridgeCommandException("E_TOOL_FINGERPRINT_MISMATCH", fingerprintCheck.Message, ProjectToolFingerprintDetails(definition), ProjectToolFingerprintRePinSteps(definition));
417
- }
418
791
  var timeoutMs = request.ExecutionTimeoutMs(definition.DefaultTimeoutMs, definition.MaxTimeoutMs);
419
792
 
420
793
  var inputJson = request.RawJsonArg("input") ?? "{}";
@@ -425,11 +798,11 @@ namespace UnityCli
425
798
  object result;
426
799
  object parsedInput;
427
800
  UnityCliProjectToolContext executionContext = null;
801
+ using (var toolTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
802
+ {
428
803
  try
429
804
  {
430
805
  WriteLog("custom_tool_run requestId=" + request.RequestId + " name=" + name + " timeoutMs=" + timeoutMs.ToString(CultureInfo.InvariantCulture));
431
- using (var toolTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
432
- {
433
806
  toolTimeout.CancelAfter(timeoutMs);
434
807
  parsedInput = UnityCliSerializer.Deserialize(inputJson, definition.RequestType);
435
808
  var effectPreparation = PrepareToolEffect(request, definition.CompletionContract, parsedInput);
@@ -448,7 +821,6 @@ namespace UnityCli
448
821
  result = await definition.InvokeAsync(parsedInput, executionContext, toolTimeout.Token);
449
822
  if (toolTimeout.IsCancellationRequested && !executionContext.EffectCommitted)
450
823
  throw new OperationCanceledException(toolTimeout.Token);
451
- }
452
824
  }
453
825
  catch (BridgeCommandException bce)
454
826
  {
@@ -480,6 +852,12 @@ namespace UnityCli
480
852
  throw PostEffectExecutionTimeout(name, "invoke", ProjectToolFailureReason(definition, inner.GetType().Name + ": " + inner.Message));
481
853
  throw new BridgeCommandException("E_TOOL_EXECUTION_FAILED", inner.GetType().Name + ": " + inner.Message, "Check Unity Console and UnityCliBridge.log with the returned requestId.");
482
854
  }
855
+ catch (UnityCliRequestBudgetExceededException exception)
856
+ {
857
+ if (executionContext != null && executionContext.EffectCommitted)
858
+ throw PostEffectExecutionTimeout(name, "request-budget", exception.Message);
859
+ throw RequestBudgetExceeded(name, exception.Dimension);
860
+ }
483
861
  catch (OperationCanceledException)
484
862
  {
485
863
  if (executionContext != null && executionContext.EffectCommitted)
@@ -496,7 +874,10 @@ namespace UnityCli
496
874
  // method 返回后仍可能在序列化 getter、schema 或 completion driver 中失败;整个 postcondition 区域必须服从同一 effect 边界。
497
875
  try
498
876
  {
499
- var outputJson = UnityCliSerializer.Serialize(result);
877
+ var terminalBudget = executionContext.ReadBudget ?? executionContext.UseReadBudget();
878
+ terminalBudget.CheckpointOrThrow();
879
+ var outputJson = UnityCliSerializer.Serialize(result, terminalBudget, toolTimeout.Token);
880
+ terminalBudget.CheckpointOrThrow();
500
881
  var outputError = ValidateProjectToolOutput(definition, outputJson);
501
882
  if (outputError != null)
502
883
  {
@@ -506,9 +887,14 @@ namespace UnityCli
506
887
  new Dictionary<string, object> { { "toolName", definition.Name }, { "path", outputError.Path }, { "expected", outputError.Expected }, { "actual", outputError.Actual } },
507
888
  new[] { "Fix the Tool result DTO or completion driver before retrying." });
508
889
  }
890
+ terminalBudget.CheckpointOrThrow();
509
891
  var terminalData = CompleteProjectToolSuccess(request, definition, result);
892
+ terminalBudget.CheckpointOrThrow();
510
893
  RecordToolCompletionEvidence(request, definition, result, terminalData);
511
- return terminalData;
894
+ terminalBudget.CheckpointOrThrow();
895
+ var terminalJson = SerializeTerminalSuccess(request.RequestId, terminalData, terminalBudget, toolTimeout.Token);
896
+ terminalBudget.CheckpointOrThrow();
897
+ return new SerializedBridgeEnvelope(terminalJson);
512
898
  }
513
899
  catch (BridgeCommandException exception)
514
900
  {
@@ -517,6 +903,12 @@ namespace UnityCli
517
903
  throw PostEffectExecutionTimeout(name, "postcondition", ProjectToolFailureReason(definition, exception.Code + ": " + exception.Message));
518
904
  throw;
519
905
  }
906
+ catch (UnityCliRequestBudgetExceededException exception)
907
+ {
908
+ if (executionContext != null && executionContext.EffectCommitted)
909
+ throw PostEffectExecutionTimeout(name, "request-budget", exception.Message);
910
+ throw RequestBudgetExceeded(name, exception.Dimension);
911
+ }
520
912
  catch (Exception exception)
521
913
  {
522
914
  if (executionContext != null && executionContext.EffectCommitted)
@@ -527,6 +919,34 @@ namespace UnityCli
527
919
  new Dictionary<string, object> { { "toolName", definition.Name }, { "stage", "postcondition" } },
528
920
  new[] { "Fix the Tool result DTO or completion driver before retrying." });
529
921
  }
922
+ }
923
+ }
924
+
925
+ /// <summary>最终 envelope 的自描述字节数最多经过8次定点收敛,且每次都服从同一 deadline 与预算。</summary>
926
+ internal static string SerializeTerminalSuccess(string requestId, object terminalData, UnityCliRequestBudget budget, CancellationToken cancellationToken)
927
+ {
928
+ if (budget == null) throw new ArgumentNullException("budget");
929
+ for (var attempt = 0; attempt < 8; attempt++)
930
+ {
931
+ budget.CheckpointOrThrow();
932
+ long actualBytes;
933
+ var json = UnityCliSerializer.Serialize(
934
+ UnityCli.Protocol.ProtocolEnvelopeFactory.Success(requestId, terminalData),
935
+ budget,
936
+ cancellationToken,
937
+ out actualBytes);
938
+ if (actualBytes == budget.ActualSerializedUtf8Bytes) return json;
939
+ budget.ObserveActualSerializedUtf8Bytes(actualBytes);
940
+ }
941
+
942
+ throw new InvalidOperationException("Final envelope UTF-8 byte accounting did not converge.");
943
+ }
944
+
945
+ /// <summary>成功 Tool 已在自身 deadline 与预算内完成最终 envelope 序列化。</summary>
946
+ private sealed class SerializedBridgeEnvelope
947
+ {
948
+ internal SerializedBridgeEnvelope(string json) { Json = json; }
949
+ internal string Json { get; private set; }
530
950
  }
531
951
 
532
952
  /// <summary>内部 raw discovery 需要 completion contract;公开投影与 static capture 会移除此字段。</summary>
@@ -546,6 +966,31 @@ namespace UnityCli
546
966
  };
547
967
  }
548
968
 
969
+ private static Dictionary<string, object> CompletionContractData(PublishedToolCompletionContract contract)
970
+ {
971
+ return new Dictionary<string, object>
972
+ {
973
+ { "executionClass", contract.ExecutionClass },
974
+ { "completionPredicate", contract.CompletionPredicate },
975
+ { "proofSource", contract.ProofSource },
976
+ { "reloadPolicy", contract.ReloadPolicy },
977
+ { "timeoutError", contract.TimeoutError },
978
+ { "outcomeUnknownPolicy", contract.OutcomeUnknownPolicy },
979
+ { "completionDriverId", contract.CompletionDriverId },
980
+ { "journalPolicy", contract.JournalPolicy },
981
+ { "mutation", contract.Mutation }
982
+ };
983
+ }
984
+
985
+ private static BridgeCommandException RequestBudgetExceeded(string toolName, string dimension)
986
+ {
987
+ return new BridgeCommandException(
988
+ "E_REQUEST_BUDGET_EXCEEDED",
989
+ "Tool " + toolName + " exhausted its request-wide Unity main-thread budget before a complete result was available.",
990
+ new Dictionary<string, object> { { "toolName", toolName }, { "dimension", dimension } },
991
+ new[] { "Retry with a narrower selector or continue from the returned continuation when available." });
992
+ }
993
+
549
994
  /// <summary>effect 后无法确认终态的异常统一收敛为执行超时。</summary>
550
995
  private static BridgeCommandException PostEffectExecutionTimeout(string toolName, string stage, string reason)
551
996
  {
@@ -556,7 +1001,7 @@ namespace UnityCli
556
1001
  new[] { "Inspect the affected business state before submitting a new request." });
557
1002
  }
558
1003
 
559
- private static Dictionary<string, object> ProjectToolDescriptorData(RegisteredTool definition)
1004
+ private static Dictionary<string, object> ProjectToolDescriptorData(RegisteredToolDescriptor definition)
560
1005
  {
561
1006
  var data = new Dictionary<string, object>
562
1007
  {
@@ -567,22 +1012,21 @@ namespace UnityCli
567
1012
  { "supportsDryRun", definition.SupportsDryRun },
568
1013
  { "contractProfile", definition.ContractProfile },
569
1014
  { "target", definition.Target }, { "availabilityScope", definition.AvailabilityScope }, { "playModeSupport", definition.PlayModeSupport },
570
- { "examples", definition.Examples },
1015
+ { "examples", definition.CloneExamples() },
571
1016
  { "defaultTimeoutMs", definition.DefaultTimeoutMs },
572
1017
  { "completionContract", CompletionContractData(definition.CompletionContract) },
573
- { "internalDescriptorFingerprint", definition.Fingerprint },
574
1018
  { "available", ToolAvailableNow(definition) }
575
1019
  };
576
1020
  if (!(bool)data["available"])
577
1021
  data["unavailableReason"] = ToolUnavailableReason(definition);
578
- if (definition.SideEffects != null && definition.SideEffects.Length > 0) data["sideEffects"] = definition.SideEffects;
579
- if (definition.Permissions != null && definition.Permissions.Length > 0) data["permissions"] = definition.Permissions;
580
- if (definition.RequiresPackages != null && definition.RequiresPackages.Length > 0) data["requiresPackages"] = definition.RequiresPackages;
1022
+ if (definition.SideEffects.Count > 0) data["sideEffects"] = definition.SideEffects.ToArray();
1023
+ if (definition.Permissions.Count > 0) data["permissions"] = definition.Permissions.ToArray();
1024
+ if (definition.RequiresPackages.Count > 0) data["requiresPackages"] = definition.RequiresPackages.ToArray();
581
1025
  return data;
582
1026
  }
583
1027
 
584
1028
  /// <summary>目录始终显示 Tool;available 只描述当前 mode/API 状态,不参与过滤。</summary>
585
- private static bool ToolAvailableNow(RegisteredTool definition)
1029
+ private static bool ToolAvailableNow(RegisteredToolDescriptor definition)
586
1030
  {
587
1031
  if (BuildProfileApiRequired(definition) && BuildToolSupport.BuildProfileType() == null)
588
1032
  return false;
@@ -596,7 +1040,7 @@ namespace UnityCli
596
1040
  }
597
1041
 
598
1042
  /// <summary>按执行门禁优先级返回与 available=false 对应的稳定原因。</summary>
599
- private static string ToolUnavailableReason(RegisteredTool definition)
1043
+ private static string ToolUnavailableReason(RegisteredToolDescriptor definition)
600
1044
  {
601
1045
  if (definition.Target == "runtime" || definition.PlayModeSupport == "runtime-only")
602
1046
  return "E_RUNTIME_NOT_READY: Enter Play Mode before executing this Tool.";
@@ -610,14 +1054,14 @@ namespace UnityCli
610
1054
  }
611
1055
 
612
1056
  /// <summary>这些 Tool 在所有版本注册;版本差异仅通过 availability 与执行错误表达。</summary>
613
- private static bool BuildProfileApiRequired(RegisteredTool definition)
1057
+ private static bool BuildProfileApiRequired(RegisteredToolDescriptor definition)
614
1058
  {
615
1059
  return definition.Name == "build.profile.create"
616
1060
  || definition.Name == "build.profile.modify"
617
1061
  || definition.Name == "build.profile.switch";
618
1062
  }
619
1063
 
620
- private static bool EditorElementCaptureApiRequired(RegisteredTool definition)
1064
+ private static bool EditorElementCaptureApiRequired(RegisteredToolDescriptor definition)
621
1065
  {
622
1066
  return definition.Name == "screenshot.editor-element";
623
1067
  }
@@ -638,301 +1082,6 @@ namespace UnityCli
638
1082
  return new Dictionary<string, object> { { "result", result } };
639
1083
  }
640
1084
 
641
- private sealed class ProjectToolFingerprintCheck
642
- {
643
- public bool Blocked;
644
- public string Expected;
645
- public string Actual;
646
- public string Message;
647
- }
648
-
649
- private sealed class ProjectToolPinnedFingerprint
650
- {
651
- public string Owner;
652
- public string Value;
653
- public string Algorithm;
654
- public string Format;
655
- public string Sha256;
656
- public bool HardMismatch;
657
- }
658
-
659
- private static ProjectToolFingerprintCheck RefreshProjectToolFingerprint(RegisteredTool definition)
660
- {
661
- var descriptor = ProjectToolDescriptorFingerprintMaterial(definition);
662
- definition.DescriptorSha256 = Sha256Hex(descriptor);
663
- definition.HandlerSha256 = Sha256Hex(ProjectToolHandlerFingerprintMaterial(definition));
664
- definition.Fingerprint = Sha256Hex(ProjectToolFingerprintAlgorithm + "\nhandler=" + definition.HandlerSha256 + "\ndescriptor=" + definition.DescriptorSha256);
665
-
666
- ProjectToolPinnedFingerprint pin;
667
- if (string.Equals(definition.Ownership, "project", StringComparison.Ordinal) && !string.IsNullOrEmpty(definition.PinnedFingerprint))
668
- {
669
- pin = ParseProjectToolPinnedFingerprint("descriptor", definition.PinnedFingerprint);
670
- }
671
- else if (string.Equals(definition.Ownership, "project", StringComparison.Ordinal))
672
- {
673
- var baseline = LoadProjectToolExpectedFingerprint(definition.Name);
674
- if (string.IsNullOrEmpty(baseline))
675
- {
676
- // 首次 project baseline 只能写入携带算法的 v2 pin,无算法 pin 绝不生成。
677
- baseline = FormatProjectToolFingerprint(definition.Fingerprint);
678
- SaveProjectToolExpectedFingerprint(definition.Name, baseline);
679
- }
680
- pin = ParseProjectToolPinnedFingerprint("project-baseline", baseline);
681
- }
682
- else
683
- {
684
- // Built-in 与 Extension 的 expected 由当前安装包生成,不写项目 baseline。
685
- pin = ParseProjectToolPinnedFingerprint("installed-package", FormatProjectToolFingerprint(definition.Fingerprint));
686
- }
687
-
688
- definition.PinnedFingerprintOwner = pin.Owner;
689
- definition.PinnedFingerprintAlgorithm = pin.Algorithm;
690
- definition.PinnedFingerprintFormat = pin.Format;
691
- definition.ExpectedFingerprint = pin.Value;
692
- var matches = !pin.HardMismatch && string.Equals(pin.Sha256, definition.Fingerprint, StringComparison.OrdinalIgnoreCase);
693
- // 任一 pin 格式、算法或内容不匹配都使用同一阻断路径。
694
- definition.FingerprintStatus = matches ? "match" : "blocked";
695
- definition.FingerprintMessage = matches
696
- ? "Custom tool fingerprint matches handler code + descriptor."
697
- : pin.HardMismatch
698
- ? "Custom tool fingerprint pin uses an unsupported algorithm or format and must be re-pinned."
699
- : "Custom tool fingerprint content differs from its v2 pin.";
700
- return new ProjectToolFingerprintCheck
701
- {
702
- Blocked = !matches,
703
- Expected = definition.ExpectedFingerprint,
704
- Actual = definition.Fingerprint,
705
- Message = definition.FingerprintMessage
706
- };
707
- }
708
-
709
- /// <summary>解析 project Tool pin;裸 SHA、未知算法和畸形值都不能被当作 v2 expected。</summary>
710
- private static ProjectToolPinnedFingerprint ParseProjectToolPinnedFingerprint(string owner, string value)
711
- {
712
- var pin = new ProjectToolPinnedFingerprint { Owner = owner, Value = value ?? string.Empty, Format = "malformed" };
713
- var match = Regex.Match(pin.Value, "^([^:]+):([0-9a-fA-F]{64})$");
714
- if (!match.Success)
715
- {
716
- pin.Format = Regex.IsMatch(pin.Value, "^[0-9a-fA-F]{64}$") ? "bare-sha256" : "malformed";
717
- pin.Algorithm = pin.Format == "bare-sha256" ? "none" : "invalid";
718
- pin.HardMismatch = true;
719
- return pin;
720
- }
721
- pin.Algorithm = match.Groups[1].Value;
722
- pin.Sha256 = match.Groups[2].Value.ToLowerInvariant();
723
- pin.Format = string.Equals(pin.Algorithm, ProjectToolFingerprintAlgorithm, StringComparison.Ordinal) ? "valid-v2" : "algorithm-mismatch";
724
- pin.HardMismatch = !string.Equals(pin.Algorithm, ProjectToolFingerprintAlgorithm, StringComparison.Ordinal);
725
- return pin;
726
- }
727
-
728
- private static string FormatProjectToolFingerprint(string sha256)
729
- {
730
- return ProjectToolFingerprintAlgorithm + ":" + (sha256 ?? string.Empty).ToLowerInvariant();
731
- }
732
-
733
-
734
- private static string LoadProjectToolExpectedFingerprint(string name)
735
- {
736
- try
737
- {
738
- var path = ProjectToolExpectedFingerprintPath(name);
739
- return File.Exists(path) ? File.ReadAllText(path).Trim() : null;
740
- }
741
- catch (Exception exception)
742
- {
743
- WriteLog("custom_tool_fingerprint_baseline_read_failed name=" + name + " error=" + exception.GetType().Name + ": " + exception.Message);
744
- return null;
745
- }
746
- }
747
-
748
- private static void SaveProjectToolExpectedFingerprint(string name, string fingerprint)
749
- {
750
- try
751
- {
752
- var path = ProjectToolExpectedFingerprintPath(name);
753
- Directory.CreateDirectory(Path.GetDirectoryName(path));
754
- File.WriteAllText(path, fingerprint ?? string.Empty);
755
- }
756
- catch (Exception exception)
757
- {
758
- WriteLog("custom_tool_fingerprint_baseline_write_failed name=" + name + " error=" + exception.GetType().Name + ": " + exception.Message);
759
- }
760
- }
761
-
762
- private static string ProjectToolExpectedFingerprintPath(string name)
763
- {
764
- return Path.Combine(UnityCliTempRoot(), "fingerprints", "tools", SafeProjectToolFingerprintFileName(name) + ".sha256");
765
- }
766
-
767
- private static string SafeProjectToolFingerprintFileName(string name)
768
- {
769
- return Regex.Replace(name ?? "project-tool", "[^A-Za-z0-9_.-]", "_");
770
- }
771
-
772
- private static string ProjectToolDescriptorFingerprintMaterial(RegisteredTool definition)
773
- {
774
- return "name=" + (definition.Name ?? string.Empty)
775
- + "\nsummary=" + (definition.Summary ?? string.Empty)
776
- + "\ndescription=" + (definition.Description ?? string.Empty)
777
- + "\ngroup=" + (definition.Group ?? string.Empty)
778
- + "\ninputSchema=" + NormalizeFingerprintJson(definition.InputSchema)
779
- + "\noutputSchema=" + NormalizeFingerprintJson(definition.OutputSchema)
780
- + "\ndestructiveHint=" + Bool(definition.DestructiveHint)
781
- + "\nsupportsDryRun=" + Bool(definition.SupportsDryRun)
782
- + "\nsideEffects=" + string.Join(",", (definition.SideEffects ?? new string[0]).OrderBy(value => value, StringComparer.Ordinal).ToArray())
783
- + "\npermissions=" + string.Join(",", (definition.Permissions ?? new string[0]).OrderBy(value => value, StringComparer.Ordinal).ToArray())
784
- + "\nrequiresPackages=" + string.Join(",", (definition.RequiresPackages ?? new string[0]).OrderBy(value => value, StringComparer.Ordinal).ToArray())
785
- + "\ntarget=" + (definition.Target ?? string.Empty)
786
- + "\navailabilityScope=" + (definition.AvailabilityScope ?? string.Empty)
787
- + "\nplayModeSupport=" + (definition.PlayModeSupport ?? string.Empty)
788
- + "\ncontractProfile=" + (definition.ContractProfile ?? string.Empty)
789
- + "\ndefaultTimeoutMs=" + definition.DefaultTimeoutMs.ToString(CultureInfo.InvariantCulture)
790
- + "\nmaxTimeoutMs=" + definition.MaxTimeoutMs.ToString(CultureInfo.InvariantCulture)
791
- + "\nrequiredInput=" + string.Join(",", (definition.RequiredInput ?? new string[0]).OrderBy(value => value, StringComparer.Ordinal).ToArray())
792
- + "\nexamples=" + NormalizeFingerprintJson(UnityCliSerializer.Serialize(definition.Examples ?? Array.Empty<ToolExample>()))
793
- + "\ncompletion=" + CompletionFingerprintMaterial(definition.CompletionContract)
794
- + "\ndescriptorSource=" + (definition.DescriptorSource ?? string.Empty);
795
- }
796
-
797
- /// <summary>把 sealed completion contract 纳入 descriptor integrity proof。</summary>
798
- private static string CompletionFingerprintMaterial(UnityCliToolCompletionContract contract)
799
- {
800
- if (contract == null) return string.Empty;
801
- return string.Join("|", new[]
802
- {
803
- contract.ExecutionClass, contract.CompletionPredicate, contract.ProofSource,
804
- contract.ReloadPolicy, contract.TimeoutError, contract.OutcomeUnknownPolicy,
805
- contract.CompletionDriverId, contract.JournalPolicy, contract.Mutation ? "mutation" : "read-only"
806
- }.Select(value => value ?? string.Empty).ToArray());
807
- }
808
-
809
- private static string NormalizeProjectToolPlayModeSupport(string value, string target)
810
- {
811
- if (string.Equals(value, "supported", StringComparison.OrdinalIgnoreCase)) return "supported";
812
- if (string.Equals(value, "runtime-only", StringComparison.OrdinalIgnoreCase)) return "runtime-only";
813
- if (string.Equals(value, "session-affecting", StringComparison.OrdinalIgnoreCase)) return "session-affecting";
814
- if (string.Equals(value, "editor-only", StringComparison.OrdinalIgnoreCase)) return "editor-only";
815
- if (string.Equals(value, "unknown", StringComparison.OrdinalIgnoreCase)) return "unknown";
816
- return string.Equals(target, "runtime", StringComparison.OrdinalIgnoreCase) ? "runtime-only" : "editor-only";
817
- }
818
-
819
- private static string ProjectToolHandlerFingerprintMaterial(RegisteredTool definition)
820
- {
821
- return "assembly=" + (definition.Assembly ?? string.Empty)
822
- + "\nmethod=" + (definition.Method ?? string.Empty)
823
- + "\nhandler=" + ProjectToolHandlerSource(definition);
824
- }
825
-
826
- private static string ProjectToolHandlerSource(RegisteredTool definition)
827
- {
828
- try
829
- {
830
- var typeName = definition.Method;
831
- if (string.IsNullOrEmpty(typeName)) return string.Empty;
832
- var dot = typeName.LastIndexOf('.');
833
- if (dot <= 0) return string.Empty;
834
- typeName = typeName.Substring(0, dot);
835
- var fileName = typeName.Substring(typeName.LastIndexOf('.') + 1) + ".cs";
836
- foreach (var guid in AssetDatabase.FindAssets(Path.GetFileNameWithoutExtension(fileName) + " t:MonoScript"))
837
- {
838
- var path = AssetDatabase.GUIDToAssetPath(guid);
839
- if (!string.Equals(Path.GetFileName(path), fileName, StringComparison.OrdinalIgnoreCase)) continue;
840
- var text = File.ReadAllText(path);
841
- if (text.IndexOf(definition.Method.Substring(dot + 1), StringComparison.Ordinal) >= 0)
842
- {
843
- return NormalizeFingerprintText(text);
844
- }
845
- }
846
- }
847
- catch (Exception exception)
848
- {
849
- WriteLog("custom_tool_fingerprint_source_failed name=" + definition.Name + " error=" + exception.GetType().Name + ": " + exception.Message);
850
- }
851
- return string.Empty;
852
- }
853
-
854
- private static string NormalizeFingerprintText(string value)
855
- {
856
- return (value ?? string.Empty).Replace("\r\n", "\n").Replace("\r", "\n");
857
- }
858
-
859
- private static string NormalizeFingerprintJson(string value)
860
- {
861
- return Regex.Replace((value ?? string.Empty).Trim(), "\\s+", " ");
862
- }
863
-
864
- private static string Sha256Hex(string value)
865
- {
866
- using (var sha = SHA256.Create())
867
- {
868
- var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(value ?? string.Empty));
869
- var builder = new StringBuilder(bytes.Length * 2);
870
- foreach (var b in bytes)
871
- {
872
- builder.Append(b.ToString("x2", CultureInfo.InvariantCulture));
873
- }
874
- return builder.ToString();
875
- }
876
- }
877
-
878
- private static Dictionary<string, object> ProjectToolFingerprintData(RegisteredTool definition)
879
- {
880
- return new Dictionary<string, object>
881
- {
882
- { "currentAlgorithm", ProjectToolFingerprintAlgorithm }, { "actualSha256", definition.Fingerprint },
883
- { "pinnedOwner", definition.PinnedFingerprintOwner }, { "pinnedAlgorithm", definition.PinnedFingerprintAlgorithm },
884
- { "pinnedFormat", definition.PinnedFingerprintFormat }, { "pinnedValue", definition.ExpectedFingerprint },
885
- { "status", definition.FingerprintStatus }, { "message", definition.FingerprintMessage },
886
- { "rePinValue", FormatProjectToolFingerprint(definition.Fingerprint) },
887
- { "rePinSteps", ProjectToolFingerprintRePinSteps(definition) },
888
- { "descriptorSha256", definition.DescriptorSha256 }, { "handlerSha256", definition.HandlerSha256 }
889
- };
890
- }
891
-
892
- private static string[] ProjectToolFingerprintRePinSteps(RegisteredTool definition)
893
- {
894
- var value = FormatProjectToolFingerprint(definition.Fingerprint);
895
- return definition.PinnedFingerprintOwner == "descriptor"
896
- ? new[] { "Review the Tool change.", "Set ToolDescriptor.Fingerprint to " + value + ".", "Run the Tool again." }
897
- : new[] { "Review the Tool change.", "Replace the persisted project Tool fingerprint with " + value + ".", "Run the Tool again." };
898
- }
899
-
900
-
901
- private static Dictionary<string, object> ProjectToolFingerprintSummary()
902
- {
903
- var items = new List<object>();
904
- var blocked = 0;
905
- foreach (var pair in ProjectTools)
906
- {
907
- var definition = pair.Value;
908
- RefreshProjectToolFingerprint(definition);
909
- if (definition.FingerprintStatus == "blocked") blocked++;
910
- items.Add(new Dictionary<string, object>
911
- {
912
- { "name", definition.Name },
913
- { "fingerprint", ProjectToolFingerprintData(definition) }
914
- });
915
- }
916
- items.Sort((left, right) => StringComparer.Ordinal.Compare(
917
- ((Dictionary<string, object>)left)["name"] as string,
918
- ((Dictionary<string, object>)right)["name"] as string));
919
- return new Dictionary<string, object>
920
- {
921
- { "algorithm", ProjectToolFingerprintAlgorithm },
922
- { "toolCount", (long)ProjectTools.Count },
923
- { "blockedCount", (long)blocked },
924
- { "tools", items }
925
- };
926
- }
927
-
928
- private static Dictionary<string, object> ProjectToolFingerprintDetails(RegisteredTool definition)
929
- {
930
- return new Dictionary<string, object>
931
- {
932
- { "tool", definition.Name },
933
- { "fingerprint", ProjectToolFingerprintData(definition) }
934
- };
935
- }
936
1085
  }
937
1086
  }
938
1087
  #endif