@atelierai/uco 1.0.6 → 1.0.8

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 (280) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +7 -8
  3. package/bin/server.mjs +1 -1
  4. package/bin/stub-plugin.mjs +3 -3
  5. package/dist/codegen/coerce.js +1 -1
  6. package/dist/codegen/coerce.js.map +1 -1
  7. package/dist/codegen/emit.js +2 -2
  8. package/dist/codegen/emit.js.map +1 -1
  9. package/dist/codegen/refs.js +1 -1
  10. package/dist/codegen/refs.js.map +1 -1
  11. package/dist/codegen/types.js +1 -1
  12. package/dist/codegen/types.js.map +1 -1
  13. package/dist/commands/call.js +1 -1
  14. package/dist/commands/call.js.map +1 -1
  15. package/dist/commands/devops/configure.js +1 -1
  16. package/dist/commands/devops/configure.js.map +1 -1
  17. package/dist/commands/devops/index.js +0 -4
  18. package/dist/commands/devops/index.js.map +1 -1
  19. package/dist/commands/devops/install-plugin.js +1 -1
  20. package/dist/commands/devops/install-plugin.js.map +1 -1
  21. package/dist/commands/devops/open.js +7 -7
  22. package/dist/commands/devops/open.js.map +1 -1
  23. package/dist/commands/devops/status.js +3 -3
  24. package/dist/commands/devops/status.js.map +1 -1
  25. package/dist/commands/exec.js +1 -1
  26. package/dist/commands/exec.js.map +1 -1
  27. package/dist/commands/install.js +2 -2
  28. package/dist/commands/install.js.map +1 -1
  29. package/dist/commands/prompt.js +2 -2
  30. package/dist/commands/prompt.js.map +1 -1
  31. package/dist/commands/resource.js +2 -2
  32. package/dist/commands/resource.js.map +1 -1
  33. package/dist/commands/update.js +1 -26
  34. package/dist/commands/update.js.map +1 -1
  35. package/dist/config/port.js +2 -2
  36. package/dist/config/port.js.map +1 -1
  37. package/dist/config/resolve.d.ts +1 -2
  38. package/dist/config/resolve.js +0 -12
  39. package/dist/config/resolve.js.map +1 -1
  40. package/dist/devops/lib/configure.d.ts +1 -1
  41. package/dist/devops/lib/configure.js +2 -2
  42. package/dist/devops/lib/configure.js.map +1 -1
  43. package/dist/devops/lib/install-plugin.d.ts +1 -1
  44. package/dist/devops/lib/install-plugin.js +1 -1
  45. package/dist/devops/lib/install-plugin.js.map +1 -1
  46. package/dist/devops/lib/install.d.ts +2 -2
  47. package/dist/devops/lib/install.js +5 -5
  48. package/dist/devops/lib/install.js.map +1 -1
  49. package/dist/devops/lib/open.js +11 -32
  50. package/dist/devops/lib/open.js.map +1 -1
  51. package/dist/devops/lib/remove-plugin.d.ts +1 -1
  52. package/dist/devops/lib/remove-plugin.js +3 -3
  53. package/dist/devops/lib/remove-plugin.js.map +1 -1
  54. package/dist/devops/lib/run-tool.d.ts +2 -2
  55. package/dist/devops/lib/run-tool.js +2 -2
  56. package/dist/devops/lib/types.d.ts +17 -54
  57. package/dist/devops/lib/types.js +2 -2
  58. package/dist/devops/lib/types.js.map +1 -1
  59. package/dist/devops/lib/update.d.ts +0 -15
  60. package/dist/devops/lib/update.js +2 -91
  61. package/dist/devops/lib/update.js.map +1 -1
  62. package/dist/devops/utils/agents.d.ts +0 -28
  63. package/dist/devops/utils/agents.js +0 -462
  64. package/dist/devops/utils/agents.js.map +1 -1
  65. package/dist/devops/utils/config.d.ts +9 -18
  66. package/dist/devops/utils/config.js +7 -20
  67. package/dist/devops/utils/config.js.map +1 -1
  68. package/dist/devops/utils/editor-cache.js +1 -1
  69. package/dist/devops/utils/editor-cache.js.map +1 -1
  70. package/dist/devops/utils/launch-error-dismiss.d.ts +2 -2
  71. package/dist/devops/utils/launch-error-dismiss.js +5 -5
  72. package/dist/devops/utils/manifest.d.ts +2 -2
  73. package/dist/devops/utils/manifest.js +3 -3
  74. package/dist/devops/utils/manifest.js.map +1 -1
  75. package/dist/devops/utils/port.d.ts +1 -1
  76. package/dist/devops/utils/port.js +1 -1
  77. package/dist/devops/utils/probe.d.ts +1 -1
  78. package/dist/devops/utils/probe.js +1 -1
  79. package/dist/devops/utils/ui.js +2 -2
  80. package/dist/devops/utils/ui.js.map +1 -1
  81. package/dist/devops/utils/unity-editor.js +4 -4
  82. package/dist/devops/utils/unity-editor.js.map +1 -1
  83. package/dist/devops/utils/unity-hub.js +1 -1
  84. package/dist/devops/utils/unity-hub.js.map +1 -1
  85. package/dist/devops/utils/unity-shutdown.d.ts +1 -1
  86. package/dist/devops/utils/unity-shutdown.js +1 -1
  87. package/dist/devops/utils/update-check.js +3 -3
  88. package/dist/devops/utils/update-check.js.map +1 -1
  89. package/dist/devops/utils/vendor.d.ts +2 -2
  90. package/dist/devops/utils/vendor.js +4 -4
  91. package/dist/devops/utils/vendor.js.map +1 -1
  92. package/dist/generated/tools.js +1 -1
  93. package/dist/generated/tools.js.map +1 -1
  94. package/dist/program.js +3 -3
  95. package/dist/program.js.map +1 -1
  96. package/dist/server/app.d.ts +5 -5
  97. package/dist/server/app.js +3 -3
  98. package/dist/server/app.js.map +1 -1
  99. package/dist/server/index.d.ts +2 -2
  100. package/dist/server/index.js +2 -2
  101. package/dist/server/index.js.map +1 -1
  102. package/dist/server/rest/auth.d.ts +1 -1
  103. package/dist/server/rest/auth.js +3 -3
  104. package/dist/server/rest/auth.js.map +1 -1
  105. package/dist/server/rest/context.d.ts +2 -2
  106. package/dist/server/rest/forward.d.ts +2 -2
  107. package/dist/server/session/store.d.ts +3 -3
  108. package/dist/server/session/store.js +4 -4
  109. package/dist/server/session/store.js.map +1 -1
  110. package/dist/server/stub-client/scenarios.js +8 -8
  111. package/dist/server/stub-client/scenarios.js.map +1 -1
  112. package/dist/server/types.d.ts +11 -11
  113. package/dist/server/types.js +9 -9
  114. package/dist/server/types.js.map +1 -1
  115. package/dist/server/ws/hub.d.ts +8 -8
  116. package/dist/server/ws/hub.js +13 -13
  117. package/dist/server/ws/hub.js.map +1 -1
  118. package/dist/server/ws/rpc.d.ts +3 -3
  119. package/dist/server/ws/rpc.js +4 -4
  120. package/dist/server/ws/rpc.js.map +1 -1
  121. package/dist/transport/index.d.ts +1 -1
  122. package/dist/transport/index.js +3 -3
  123. package/dist/transport/index.js.map +1 -1
  124. package/dist/transport/rest.js +1 -1
  125. package/dist/transport/rest.js.map +1 -1
  126. package/docs/INSTALL.zh-CN.md +2 -3
  127. package/package.json +1 -1
  128. package/skills/uco-setup/SKILL.md +1 -1
  129. package/skills/unity-editor/references/diagnostics.md +24 -24
  130. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetConfig.cs +132 -132
  131. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDependencyResolver.cs +237 -237
  132. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDownloader.cs +82 -82
  133. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/MenuItems.cs +247 -247
  134. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/ToolGroups.cs +153 -153
  135. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/SystemTool/Skills.Create.SkillBody.cs +155 -155
  136. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Console.GetLogs.cs +95 -95
  137. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Docs.HttpFetcher.cs +139 -139
  138. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Instance.ListAll.cs +72 -72
  139. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/PathReadHelper.cs +106 -106
  140. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Script.Validate.cs +252 -252
  141. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests/TestResultCollector.cs +537 -537
  142. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests.cs +400 -400
  143. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Branding/ProductInfo.cs +62 -62
  144. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +1034 -1034
  145. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Skills/Skill_InitialSetup.cs +197 -197
  146. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.Editor.cs +201 -201
  147. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.cs +67 -67
  148. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Components/ExtensionPanel.cs +379 -379
  149. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/MenuItems.cs +72 -72
  150. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/UnityCopilotProjectSettingsProvider.cs +64 -64
  151. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotListWindowBase.cs +393 -393
  152. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotWindowBase.cs +113 -113
  153. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.Connection.cs +3 -3
  154. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotFeatures.cs +195 -195
  155. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +372 -372
  156. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs +354 -354
  157. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.cs +78 -78
  158. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowInitializer.cs +47 -47
  159. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/SerializationCheckWindow.cs +190 -190
  160. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Build.cs +91 -91
  161. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Config.cs +298 -298
  162. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Static.cs +296 -296
  163. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.cs +69 -69
  164. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PackageUtils.cs +168 -168
  165. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PrefsKeyMigration.cs +119 -119
  166. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/SafeDefaultsGuard.cs +180 -180
  167. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ScriptUtils.cs +229 -229
  168. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolsManifestGenerator.cs +200 -200
  169. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringAdapters.cs +1467 -1467
  170. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringSafetyReloadGuard.cs +65 -65
  171. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityCopilotUpdateProjectSettings.cs +66 -66
  172. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/ReflectorNet.dll +0 -0
  173. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.Common.dll +0 -0
  174. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.dll +0 -0
  175. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Converter/Json/Structures/EntityIdConverter.cs +120 -120
  176. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.cs +110 -110
  177. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.pre-Unity.6.5.cs +110 -110
  178. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/FileLogStorage.cs +547 -547
  179. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Build.cs +243 -243
  180. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Config.cs +175 -175
  181. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +295 -295
  182. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginBuilder.cs +105 -105
  183. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.Static.cs +155 -155
  184. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.cs +59 -59
  185. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Utils/EnvironmentUtils.cs +368 -368
  186. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/AtomicApiTestFixtures.cs +99 -99
  187. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/TryModifyAtTests.cs +227 -227
  188. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AuthoringSafetyPilotTests.cs +1050 -1050
  189. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetLegacyMigrationTests.cs +304 -304
  190. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +4 -4
  191. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/BlacklistTypeTests.cs +416 -416
  192. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs +172 -172
  193. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.pre-Unity.6.5.cs +169 -169
  194. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +182 -182
  195. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.pre-Unity.6.5.cs +179 -179
  196. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs +175 -175
  197. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.pre-Unity.6.5.cs +174 -174
  198. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs +171 -171
  199. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.pre-Unity.6.5.cs +170 -170
  200. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsCreateFolderTests.cs +243 -243
  201. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsMaterialTests.cs +101 -101
  202. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.cs +348 -348
  203. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.pre-Unity.6.5.cs +348 -348
  204. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsShaderTests.cs +194 -194
  205. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.cs +573 -573
  206. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.pre-Unity.6.5.cs +513 -513
  207. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.cs +154 -154
  208. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.pre-Unity.6.5.cs +154 -154
  209. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotGameViewTests.cs +164 -164
  210. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotSceneViewTests.cs +155 -155
  211. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolParameterTests.cs +56 -56
  212. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.cs +651 -651
  213. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.pre-Unity.6.5.cs +651 -651
  214. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ToolBatchExecutionIntegrationTests.cs +407 -407
  215. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs → UcoPluginTests.cs} +105 -105
  216. package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
  217. package/dist/commands/devops/login.d.ts +0 -2
  218. package/dist/commands/devops/login.js +0 -29
  219. package/dist/commands/devops/login.js.map +0 -1
  220. package/dist/commands/devops/setup-mcp.d.ts +0 -2
  221. package/dist/commands/devops/setup-mcp.js +0 -41
  222. package/dist/commands/devops/setup-mcp.js.map +0 -1
  223. package/dist/devops/commands-upstream/bootstrap-local.d.ts +0 -17
  224. package/dist/devops/commands-upstream/bootstrap-local.js +0 -87
  225. package/dist/devops/commands-upstream/bootstrap-local.js.map +0 -1
  226. package/dist/devops/commands-upstream/close.d.ts +0 -57
  227. package/dist/devops/commands-upstream/close.js +0 -188
  228. package/dist/devops/commands-upstream/close.js.map +0 -1
  229. package/dist/devops/commands-upstream/configure.d.ts +0 -2
  230. package/dist/devops/commands-upstream/configure.js +0 -87
  231. package/dist/devops/commands-upstream/configure.js.map +0 -1
  232. package/dist/devops/commands-upstream/create-project.d.ts +0 -2
  233. package/dist/devops/commands-upstream/create-project.js +0 -43
  234. package/dist/devops/commands-upstream/create-project.js.map +0 -1
  235. package/dist/devops/commands-upstream/install-plugin.d.ts +0 -2
  236. package/dist/devops/commands-upstream/install-plugin.js +0 -64
  237. package/dist/devops/commands-upstream/install-plugin.js.map +0 -1
  238. package/dist/devops/commands-upstream/install-unity.d.ts +0 -2
  239. package/dist/devops/commands-upstream/install-unity.js +0 -72
  240. package/dist/devops/commands-upstream/install-unity.js.map +0 -1
  241. package/dist/devops/commands-upstream/login.d.ts +0 -2
  242. package/dist/devops/commands-upstream/login.js +0 -33
  243. package/dist/devops/commands-upstream/login.js.map +0 -1
  244. package/dist/devops/commands-upstream/open.d.ts +0 -32
  245. package/dist/devops/commands-upstream/open.js +0 -282
  246. package/dist/devops/commands-upstream/open.js.map +0 -1
  247. package/dist/devops/commands-upstream/remove-plugin.d.ts +0 -2
  248. package/dist/devops/commands-upstream/remove-plugin.js +0 -41
  249. package/dist/devops/commands-upstream/remove-plugin.js.map +0 -1
  250. package/dist/devops/commands-upstream/run-system-tool.d.ts +0 -1
  251. package/dist/devops/commands-upstream/run-system-tool.js +0 -13
  252. package/dist/devops/commands-upstream/run-system-tool.js.map +0 -1
  253. package/dist/devops/commands-upstream/run-tool-builder.d.ts +0 -25
  254. package/dist/devops/commands-upstream/run-tool-builder.js +0 -125
  255. package/dist/devops/commands-upstream/run-tool-builder.js.map +0 -1
  256. package/dist/devops/commands-upstream/run-tool.d.ts +0 -1
  257. package/dist/devops/commands-upstream/run-tool.js +0 -13
  258. package/dist/devops/commands-upstream/run-tool.js.map +0 -1
  259. package/dist/devops/commands-upstream/setup-mcp.d.ts +0 -2
  260. package/dist/devops/commands-upstream/setup-mcp.js +0 -68
  261. package/dist/devops/commands-upstream/setup-mcp.js.map +0 -1
  262. package/dist/devops/commands-upstream/setup-skills.d.ts +0 -2
  263. package/dist/devops/commands-upstream/setup-skills.js +0 -167
  264. package/dist/devops/commands-upstream/setup-skills.js.map +0 -1
  265. package/dist/devops/commands-upstream/status.d.ts +0 -2
  266. package/dist/devops/commands-upstream/status.js +0 -87
  267. package/dist/devops/commands-upstream/status.js.map +0 -1
  268. package/dist/devops/commands-upstream/update.d.ts +0 -2
  269. package/dist/devops/commands-upstream/update.js +0 -77
  270. package/dist/devops/commands-upstream/update.js.map +0 -1
  271. package/dist/devops/commands-upstream/wait-for-ready.d.ts +0 -2
  272. package/dist/devops/commands-upstream/wait-for-ready.js +0 -93
  273. package/dist/devops/commands-upstream/wait-for-ready.js.map +0 -1
  274. package/dist/devops/lib/setup-mcp.d.ts +0 -35
  275. package/dist/devops/lib/setup-mcp.js +0 -177
  276. package/dist/devops/lib/setup-mcp.js.map +0 -1
  277. package/dist/devops/utils/cloud-login.d.ts +0 -7
  278. package/dist/devops/utils/cloud-login.js +0 -56
  279. package/dist/devops/utils/cloud-login.js.map +0 -1
  280. /package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs.meta → UcoPluginTests.cs.meta} +0 -0
@@ -1,1467 +1,1467 @@
1
- /*
2
- ┌──────────────────────────────────────────────────────────────────┐
3
- │ Author: AtelierAI │
4
- │ Copyright (c) 2025 AtelierAI │
5
- │ Licensed under the MIT License. │
6
- └──────────────────────────────────────────────────────────────────┘
7
- */
8
- #nullable enable
9
-
10
- using System;
11
- using System.Collections.Generic;
12
- using System.Linq;
13
- using System.Text.Json;
14
- using AIGD;
15
- using com.AtelierAI.Uco.Framework;
16
- using com.AtelierAI.Uco.Framework.Common.Model;
17
- using com.IvanMurzak.ReflectorNet.Utils;
18
- using com.AtelierAI.Unity.Copilot.Editor.API;
19
- using com.AtelierAI.Unity.Copilot.Runtime.Extensions;
20
- using com.AtelierAI.Unity.Copilot.Runtime.Utils;
21
- using UnityEditor;
22
- using UnityEditor.SceneManagement;
23
- using UnityEngine;
24
-
25
- namespace com.AtelierAI.Unity.Copilot.Editor.Utils
26
- {
27
- /// <summary>
28
- /// Read-only validator shared by the Scene/GameObject/Component pilot.
29
- /// It resolves the real Unity targets (objects, components, scenes,
30
- /// scene assets, and canonical paths) on the main thread without
31
- /// creating, modifying, destroying, dirtying, or saving anything. The
32
- /// reflected runner is deliberately never called from this adapter.
33
- /// </summary>
34
- public sealed class UnityPilotAuthoringValidator : IAuthoringValidator
35
- {
36
- public AuthoringValidationResult Validate(AuthoringInvocation invocation)
37
- {
38
- if (invocation == null)
39
- throw new ArgumentNullException(nameof(invocation));
40
-
41
- var inspection = UnityPilotAuthoringInspection.Inspect(invocation);
42
- return new AuthoringValidationResult
43
- {
44
- Valid = inspection.Errors.Count == 0,
45
- FailureCode = inspection.Errors.Count == 0 ? null : "validation_failed",
46
- Targets = inspection.Targets.Take(32).ToArray(),
47
- Details = inspection.Errors.Count == 0
48
- ? null
49
- : new System.Text.Json.Nodes.JsonObject
50
- {
51
- ["reason"] = inspection.Errors[0],
52
- ["errorCount"] = inspection.Errors.Count,
53
- },
54
- };
55
- }
56
- }
57
-
58
- /// <summary>
59
- /// Result of one read-only pilot inspection. Targets carry a state
60
- /// fingerprint derived from the live Unity object (identity, hierarchy
61
- /// path, owning scene/stage, component set) so a later change to the
62
- /// inspected target makes an issued confirmation plan stale.
63
- /// </summary>
64
- internal sealed class UnityPilotInspection
65
- {
66
- public List<AuthoringTargetSummary> Targets { get; } = new List<AuthoringTargetSummary>();
67
- public List<string> Errors { get; } = new List<string>();
68
- public List<string> Effects { get; } = new List<string>();
69
- public System.Text.Json.Nodes.JsonObject Before { get; } = new System.Text.Json.Nodes.JsonObject();
70
- public System.Text.Json.Nodes.JsonObject After { get; } = new System.Text.Json.Nodes.JsonObject();
71
- }
72
-
73
- /// <summary>
74
- /// Main-thread, read-only inspection of the pilot tools' targets. Every
75
- /// lookup uses the same resolution helpers the runners use
76
- /// (<c>GameObjectUtils.FindBy</c>, <c>FindAssetObject</c>,
77
- /// <c>ComponentRef.Matches</c>), but no Unity API with a side effect is
78
- /// reachable from this type.
79
- /// </summary>
80
- internal static class UnityPilotAuthoringInspection
81
- {
82
- private const int MaxListedTargets = 32;
83
-
84
- public static UnityPilotInspection Inspect(AuthoringInvocation invocation)
85
- => MainThread.Instance.Run(() => InspectOnMainThread(invocation));
86
-
87
- private static UnityPilotInspection InspectOnMainThread(AuthoringInvocation invocation)
88
- {
89
- var result = new UnityPilotInspection();
90
- foreach (var binding in invocation.PathBindings.Values)
91
- {
92
- if (binding.Resolution.IsReadOnly
93
- && binding.Resolution.Intent != ProjectPathAccessIntent.Read)
94
- result.Errors.Add("path_read_only");
95
- }
96
-
97
- switch (invocation.Name)
98
- {
99
- case Tool_GameObject.GameObjectCreateToolId:
100
- InspectGameObjectCreate(invocation, result);
101
- break;
102
- case Tool_GameObject.GameObjectDestroyToolId:
103
- InspectSingleGameObject(invocation, result, "gameObjectRef", "destroy");
104
- break;
105
- case Tool_GameObject.GameObjectModifyToolId:
106
- InspectGameObjectList(invocation, result, "modify");
107
- break;
108
- case Tool_GameObject.GameObjectDuplicateToolId:
109
- InspectGameObjectList(invocation, result, "duplicate");
110
- break;
111
- case Tool_GameObject.GameObjectSetParentToolId:
112
- InspectGameObjectList(invocation, result, "parent");
113
- InspectOptionalOrRequiredGameObject(invocation, result, "parentGameObjectRef", required: true, kind: "Parent");
114
- break;
115
- case Tool_GameObject.GameObjectComponentAddToolId:
116
- InspectComponentAdd(invocation, result);
117
- break;
118
- case Tool_GameObject.GameObjectComponentModifyToolId:
119
- InspectComponentModify(invocation, result);
120
- break;
121
- case Tool_GameObject.GameObjectComponentDestroyToolId:
122
- InspectComponentDestroy(invocation, result);
123
- break;
124
- case Tool_Scene.SceneOpenToolId:
125
- InspectSceneAsset(invocation, result, requireOpened: false, effect: "open scene");
126
- break;
127
- case Tool_Scene.SceneSetActiveToolId:
128
- InspectSceneAsset(invocation, result, requireOpened: true, effect: "set active scene");
129
- break;
130
- case Tool_Scene.SceneCreateToolId:
131
- InspectSceneCreate(invocation, result);
132
- break;
133
- case Tool_Scene.SceneSaveToolId:
134
- InspectSceneSave(invocation, result);
135
- break;
136
- case Tool_Scene.SceneUnloadToolId:
137
- InspectSceneUnload(invocation, result);
138
- break;
139
- default:
140
- InspectArgumentTargets(invocation, result);
141
- break;
142
- }
143
-
144
- result.Before["targetCount"] = result.Targets.Count;
145
- result.Before["activeScene"] = SafeSceneDisplay(EditorSceneManager.GetActiveScene());
146
- result.After["targetCount"] = result.Targets.Count;
147
- result.After["predictedEffects"] = result.Effects.Take(32).Aggregate(
148
- new System.Text.Json.Nodes.JsonArray(),
149
- (array, effect) =>
150
- {
151
- array.Add(effect);
152
- return array;
153
- });
154
- return result;
155
- }
156
-
157
- private static void InspectGameObjectCreate(AuthoringInvocation invocation, UnityPilotInspection result)
158
- {
159
- TryGetString(invocation.Arguments, "name", out var name);
160
- if (string.IsNullOrEmpty(name))
161
- result.Errors.Add("name_missing");
162
-
163
- var parentFingerprint = string.Empty;
164
- if (TryGetArgument(invocation.Arguments, "parentGameObjectRef", out var parentElement)
165
- && parentElement.ValueKind == JsonValueKind.Object)
166
- {
167
- var parentRef = Deserialize<GameObjectRef>(parentElement);
168
- if (parentRef != null && parentRef.IsValid(out _))
169
- {
170
- var parent = GameObjectUtils.FindBy(parentRef, out var error);
171
- if (parent == null)
172
- {
173
- result.Errors.Add("parent_not_found");
174
- }
175
- else
176
- {
177
- var target = GameObjectTarget(parent, "Parent");
178
- parentFingerprint = target.Fingerprint ?? string.Empty;
179
- result.Targets.Add(target);
180
- }
181
- }
182
- }
183
-
184
- var container = ContainerFingerprint();
185
- result.Targets.Add(new AuthoringTargetSummary
186
- {
187
- Kind = "GameObject",
188
- Name = Bound(name),
189
- Fingerprint = Hash("create|" + name + "|" + container + "|" + parentFingerprint),
190
- });
191
- result.Effects.Add("create GameObject '" + Bound(name) + "'");
192
- }
193
-
194
- private static void InspectSingleGameObject(
195
- AuthoringInvocation invocation,
196
- UnityPilotInspection result,
197
- string argumentName,
198
- string effect)
199
- {
200
- var go = ResolveRequiredGameObject(invocation, result, argumentName, "GameObject");
201
- if (go == null)
202
- return;
203
- result.Effects.Add(effect + " GameObject '" + Bound(go.name) + "' (" + go.transform.childCount + " children)");
204
- }
205
-
206
- private static void InspectOptionalOrRequiredGameObject(
207
- AuthoringInvocation invocation,
208
- UnityPilotInspection result,
209
- string argumentName,
210
- bool required,
211
- string kind)
212
- {
213
- if (!TryGetArgument(invocation.Arguments, argumentName, out var element)
214
- || element.ValueKind != JsonValueKind.Object)
215
- {
216
- if (required)
217
- result.Errors.Add("target_missing");
218
- return;
219
- }
220
-
221
- var reference = Deserialize<GameObjectRef>(element);
222
- if (reference == null || !reference.IsValid(out _))
223
- {
224
- if (required)
225
- result.Errors.Add("target_invalid");
226
- return;
227
- }
228
-
229
- var go = GameObjectUtils.FindBy(reference, out _);
230
- if (go == null)
231
- {
232
- result.Errors.Add("target_not_found");
233
- return;
234
- }
235
- result.Targets.Add(GameObjectTarget(go, kind));
236
- }
237
-
238
- private static void InspectGameObjectList(
239
- AuthoringInvocation invocation,
240
- UnityPilotInspection result,
241
- string effect)
242
- {
243
- if (!TryGetArgument(invocation.Arguments, "gameObjectRefs", out var element)
244
- || element.ValueKind != JsonValueKind.Array)
245
- {
246
- result.Errors.Add("target_missing");
247
- return;
248
- }
249
-
250
- var list = Deserialize<GameObjectRefList>(element);
251
- if (list == null || list.Count == 0)
252
- {
253
- result.Errors.Add("target_missing");
254
- return;
255
- }
256
-
257
- var resolved = 0;
258
- foreach (var reference in list)
259
- {
260
- if (reference == null || !reference.IsValid(out _))
261
- {
262
- result.Errors.Add("target_invalid");
263
- continue;
264
- }
265
- var go = GameObjectUtils.FindBy(reference, out _);
266
- if (go == null)
267
- {
268
- result.Errors.Add("target_not_found");
269
- continue;
270
- }
271
- resolved++;
272
- if (result.Targets.Count < MaxListedTargets)
273
- result.Targets.Add(GameObjectTarget(go, "GameObject"));
274
- }
275
- result.Effects.Add(effect + " " + resolved + " GameObject(s)");
276
- }
277
-
278
- private static void InspectComponentAdd(AuthoringInvocation invocation, UnityPilotInspection result)
279
- {
280
- var go = ResolveRequiredGameObject(invocation, result, "gameObjectRef", "GameObject");
281
- if (!TryGetArgument(invocation.Arguments, "componentNames", out var namesElement)
282
- || namesElement.ValueKind != JsonValueKind.Array)
283
- {
284
- result.Errors.Add("component_names_missing");
285
- return;
286
- }
287
-
288
- var count = 0;
289
- foreach (var item in namesElement.EnumerateArray())
290
- {
291
- if (item.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(item.GetString()))
292
- {
293
- result.Errors.Add("component_name_invalid");
294
- continue;
295
- }
296
-
297
- var componentName = item.GetString()!;
298
- var type = TypeUtils.GetType(componentName)
299
- ?? Tool_GameObject.AllComponentTypes.FirstOrDefault(t => t.Name == componentName);
300
- if (type == null)
301
- {
302
- result.Errors.Add("component_type_not_found");
303
- continue;
304
- }
305
- if (!typeof(UnityEngine.Component).IsAssignableFrom(type))
306
- {
307
- result.Errors.Add("component_type_not_component");
308
- continue;
309
- }
310
- count++;
311
- result.Targets.Add(new AuthoringTargetSummary
312
- {
313
- Kind = "ComponentType",
314
- Name = Bound(type.FullName),
315
- Fingerprint = Hash("component-type|" + type.AssemblyQualifiedName),
316
- });
317
- }
318
-
319
- if (count == 0 && result.Errors.Count == 0)
320
- result.Errors.Add("component_names_missing");
321
- if (go != null)
322
- result.Effects.Add("add " + count + " component(s) to GameObject '" + Bound(go.name) + "'");
323
- }
324
-
325
- private static void InspectComponentModify(AuthoringInvocation invocation, UnityPilotInspection result)
326
- {
327
- var go = ResolveRequiredGameObject(invocation, result, "gameObjectRef", "GameObject");
328
- if (go == null)
329
- return;
330
-
331
- if (!TryGetArgument(invocation.Arguments, "componentRef", out var element)
332
- || element.ValueKind != JsonValueKind.Object)
333
- {
334
- result.Errors.Add("component_missing");
335
- return;
336
- }
337
-
338
- var reference = Deserialize<ComponentRef>(element);
339
- if (reference == null || !reference.IsValid(out _))
340
- {
341
- result.Errors.Add("component_invalid");
342
- return;
343
- }
344
-
345
- var component = FindComponent(go, reference);
346
- if (component == null)
347
- {
348
- result.Errors.Add("component_not_found");
349
- return;
350
- }
351
-
352
- result.Targets.Add(ComponentTarget(component));
353
- result.Effects.Add("modify component '" + Bound(component.GetType().Name) + "' on GameObject '" + Bound(go.name) + "'");
354
- }
355
-
356
- private static void InspectComponentDestroy(AuthoringInvocation invocation, UnityPilotInspection result)
357
- {
358
- var go = ResolveRequiredGameObject(invocation, result, "gameObjectRef", "GameObject");
359
- if (go == null)
360
- return;
361
-
362
- if (!TryGetArgument(invocation.Arguments, "destroyComponentRefs", out var element)
363
- || element.ValueKind != JsonValueKind.Array)
364
- {
365
- result.Errors.Add("component_missing");
366
- return;
367
- }
368
-
369
- var references = Deserialize<ComponentRefList>(element);
370
- if (references == null || references.Count == 0)
371
- {
372
- result.Errors.Add("component_missing");
373
- return;
374
- }
375
-
376
- var count = 0;
377
- foreach (var reference in references)
378
- {
379
- if (reference == null || !reference.IsValid(out _))
380
- {
381
- result.Errors.Add("component_invalid");
382
- continue;
383
- }
384
- var component = FindComponent(go, reference);
385
- if (component == null)
386
- {
387
- result.Errors.Add("component_not_found");
388
- continue;
389
- }
390
- count++;
391
- if (result.Targets.Count < MaxListedTargets)
392
- result.Targets.Add(ComponentTarget(component));
393
- }
394
- result.Effects.Add("destroy " + count + " component(s) on GameObject '" + Bound(go.name) + "'");
395
- }
396
-
397
- private static void InspectSceneAsset(
398
- AuthoringInvocation invocation,
399
- UnityPilotInspection result,
400
- bool requireOpened,
401
- string effect)
402
- {
403
- if (!TryGetArgument(invocation.Arguments, "sceneRef", out var element)
404
- || element.ValueKind != JsonValueKind.Object)
405
- {
406
- result.Errors.Add("target_missing");
407
- return;
408
- }
409
-
410
- var reference = Deserialize<AssetObjectRef>(element);
411
- var sceneAsset = reference == null ? null : reference.FindAssetObject<SceneAsset>();
412
- if (sceneAsset == null)
413
- {
414
- result.Errors.Add("scene_asset_not_found");
415
- return;
416
- }
417
-
418
- var assetPath = AssetDatabase.GetAssetPath(sceneAsset);
419
- var guid = AssetDatabase.AssetPathToGUID(assetPath);
420
- if (requireOpened)
421
- {
422
- var opened = UnityEngine.SceneManagement.SceneManager.GetSceneByPath(assetPath);
423
- if (!opened.IsValid())
424
- opened = UnityEngine.SceneManagement.SceneManager.GetSceneByName(sceneAsset.name);
425
- if (!opened.IsValid() || !opened.isLoaded)
426
- result.Errors.Add("scene_not_opened");
427
- }
428
-
429
- result.Targets.Add(new AuthoringTargetSummary
430
- {
431
- Kind = "SceneAsset",
432
- Name = Bound(sceneAsset.name),
433
- RelativePath = SafeProjectRelative(assetPath),
434
- Fingerprint = Hash("scene-asset|" + assetPath + "|" + guid),
435
- });
436
- result.Effects.Add(effect + " '" + Bound(sceneAsset.name) + "'");
437
- }
438
-
439
- private static void InspectSceneCreate(AuthoringInvocation invocation, UnityPilotInspection result)
440
- {
441
- if (!invocation.PathBindings.TryGetValue("path", out var binding))
442
- {
443
- result.Errors.Add("path_missing");
444
- return;
445
- }
446
-
447
- var relative = binding.RelativePath;
448
- if (!relative.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
449
- result.Errors.Add("scene_extension");
450
- // Creating over an existing scene file is an overwrite, which
451
- // the pilot never performs implicitly.
452
- if (binding.Resolution.Exists)
453
- result.Errors.Add("scene_exists");
454
-
455
- result.Targets.Add(new AuthoringTargetSummary
456
- {
457
- Kind = "SceneFile",
458
- Name = Bound(System.IO.Path.GetFileName(relative)),
459
- RelativePath = relative,
460
- Fingerprint = Hash("scene-file|" + binding.CanonicalPath + "|exists=" + binding.Resolution.Exists),
461
- });
462
- result.Effects.Add("create scene file '" + relative + "'");
463
- }
464
-
465
- private static void InspectSceneSave(AuthoringInvocation invocation, UnityPilotInspection result)
466
- {
467
- TryGetString(invocation.Arguments, "openedSceneName", out var openedSceneName);
468
- var scene = string.IsNullOrEmpty(openedSceneName)
469
- ? EditorSceneManager.GetActiveScene()
470
- : SceneUtils.GetAllOpenedScenes().FirstOrDefault(candidate => candidate.name == openedSceneName);
471
- if (!scene.IsValid())
472
- {
473
- result.Errors.Add("scene_not_found");
474
- return;
475
- }
476
-
477
- string? destination = null;
478
- if (invocation.PathBindings.TryGetValue("path", out var binding))
479
- {
480
- destination = binding.RelativePath;
481
- if (!destination.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
482
- result.Errors.Add("scene_extension");
483
- }
484
- else if (!string.IsNullOrEmpty(scene.path))
485
- {
486
- destination = SafeProjectRelative(scene.path);
487
- }
488
-
489
- if (string.IsNullOrEmpty(destination))
490
- result.Errors.Add("scene_path_missing");
491
-
492
- result.Targets.Add(new AuthoringTargetSummary
493
- {
494
- Kind = "Scene",
495
- Name = Bound(scene.name),
496
- RelativePath = destination,
497
- Fingerprint = Hash("scene|" + scene.name + "|" + scene.path + "|" + scene.rootCount + "|" + destination),
498
- });
499
- result.Effects.Add("save scene '" + Bound(scene.name) + "' to '" + (destination ?? "?") + "'");
500
- }
501
-
502
- private static void InspectSceneUnload(AuthoringInvocation invocation, UnityPilotInspection result)
503
- {
504
- TryGetString(invocation.Arguments, "name", out var name);
505
- if (string.IsNullOrEmpty(name))
506
- {
507
- result.Errors.Add("target_missing");
508
- return;
509
- }
510
-
511
- var scene = SceneUtils.GetAllOpenedScenes().FirstOrDefault(candidate => candidate.name == name);
512
- if (!scene.IsValid())
513
- {
514
- result.Errors.Add("scene_not_found");
515
- return;
516
- }
517
-
518
- result.Targets.Add(new AuthoringTargetSummary
519
- {
520
- Kind = "Scene",
521
- Name = Bound(scene.name),
522
- RelativePath = SafeProjectRelative(scene.path),
523
- Fingerprint = Hash("scene|" + scene.name + "|" + scene.path + "|" + scene.rootCount),
524
- });
525
- result.Effects.Add("unload scene '" + Bound(scene.name) + "'");
526
- }
527
-
528
- /// <summary>
529
- /// Fallback for pilot-attributed tools without a dedicated inspector:
530
- /// the binding still covers every reference-like argument.
531
- /// </summary>
532
- private static void InspectArgumentTargets(AuthoringInvocation invocation, UnityPilotInspection result)
533
- {
534
- foreach (var argument in invocation.Arguments)
535
- {
536
- if (!LooksLikeTarget(argument.Key) && !string.Equals(argument.Key, "path", StringComparison.OrdinalIgnoreCase))
537
- continue;
538
- result.Targets.Add(new AuthoringTargetSummary
539
- {
540
- Kind = argument.Key,
541
- Name = DisplayValue(argument.Value),
542
- Fingerprint = AuthoringConfirmationBinding.ComputeArgumentsHash(
543
- new Dictionary<string, JsonElement> { [argument.Key] = argument.Value.Clone() }),
544
- });
545
- }
546
- var kind = invocation.Descriptor?.MutationKind ?? AuthoringMutationKind.Unknown;
547
- result.Effects.Add(kind == AuthoringMutationKind.Unknown ? "authoring change" : kind.ToString().ToLowerInvariant());
548
- }
549
-
550
- private static GameObject? ResolveRequiredGameObject(
551
- AuthoringInvocation invocation,
552
- UnityPilotInspection result,
553
- string argumentName,
554
- string kind)
555
- {
556
- if (!TryGetArgument(invocation.Arguments, argumentName, out var element)
557
- || element.ValueKind != JsonValueKind.Object)
558
- {
559
- result.Errors.Add("target_missing");
560
- return null;
561
- }
562
-
563
- var reference = Deserialize<GameObjectRef>(element);
564
- if (reference == null || !reference.IsValid(out _))
565
- {
566
- result.Errors.Add("target_invalid");
567
- return null;
568
- }
569
-
570
- var go = GameObjectUtils.FindBy(reference, out _);
571
- if (go == null)
572
- {
573
- result.Errors.Add("target_not_found");
574
- return null;
575
- }
576
-
577
- result.Targets.Add(GameObjectTarget(go, kind));
578
- return go;
579
- }
580
-
581
- private static UnityEngine.Component? FindComponent(GameObject go, ComponentRef reference)
582
- {
583
- var components = go.GetComponents<UnityEngine.Component>();
584
- for (var index = 0; index < components.Length; index++)
585
- {
586
- var component = components[index];
587
- if (component != null && reference.Matches(component, index))
588
- return component;
589
- }
590
- return null;
591
- }
592
-
593
- private static AuthoringTargetSummary GameObjectTarget(GameObject go, string kind)
594
- {
595
- var components = go.GetComponents<UnityEngine.Component>();
596
- var componentTypes = string.Join(",", components.Select(component => component == null ? "missing" : component.GetType().FullName));
597
- var path = go.GetPath();
598
- var scene = go.scene.IsValid() ? go.scene.path + "|" + go.scene.name : "no-scene";
599
- return new AuthoringTargetSummary
600
- {
601
- Kind = kind,
602
- Name = Bound(go.name),
603
- RelativePath = SafeHierarchyPath(path),
604
- Fingerprint = Hash(
605
- "gameobject|" + StableId(go)
606
- + "|" + path
607
- + "|" + scene
608
- + "|" + go.transform.childCount
609
- + "|" + componentTypes
610
- + "|" + go.activeSelf),
611
- };
612
- }
613
-
614
- private static AuthoringTargetSummary ComponentTarget(UnityEngine.Component component)
615
- {
616
- var go = component.gameObject;
617
- var index = Array.IndexOf(go.GetComponents<UnityEngine.Component>(), component);
618
- return new AuthoringTargetSummary
619
- {
620
- Kind = "Component",
621
- Name = Bound(component.GetType().FullName),
622
- RelativePath = SafeHierarchyPath(go.GetPath()),
623
- Fingerprint = Hash(
624
- "component|" + StableId(component)
625
- + "|" + component.GetType().AssemblyQualifiedName
626
- + "|" + index
627
- + "|" + StableId(go)),
628
- };
629
- }
630
-
631
- /// <summary>Identity of the active scene or open prefab stage.</summary>
632
- private static string ContainerFingerprint()
633
- {
634
- var stage = UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage();
635
- if (stage != null)
636
- return "prefab-stage|" + stage.assetPath;
637
- var scene = EditorSceneManager.GetActiveScene();
638
- return "scene|" + scene.path + "|" + scene.name;
639
- }
640
-
641
- private static string SafeSceneDisplay(UnityEngine.SceneManagement.Scene scene)
642
- => scene.IsValid() ? (SafeProjectRelative(scene.path) ?? Bound(scene.name) ?? "untitled") : "none";
643
-
644
- private static string StableId(UnityEngine.Object value)
645
- {
646
- #if UNITY_6000_5_OR_NEWER
647
- return value.GetEntityId().ToString();
648
- #else
649
- return value.GetInstanceID().ToString(System.Globalization.CultureInfo.InvariantCulture);
650
- #endif
651
- }
652
-
653
- private static string Hash(string material)
654
- {
655
- using var sha = System.Security.Cryptography.SHA256.Create();
656
- var bytes = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(material));
657
- var builder = new System.Text.StringBuilder(bytes.Length * 2);
658
- foreach (var value in bytes)
659
- builder.Append(value.ToString("x2", System.Globalization.CultureInfo.InvariantCulture));
660
- return "sha256-" + builder;
661
- }
662
-
663
- private static T? Deserialize<T>(JsonElement element) where T : class
664
- {
665
- try
666
- {
667
- var reflector = UnityCopilotPluginEditor.HasInstance
668
- ? UnityCopilotPluginEditor.Instance.Reflector
669
- : null;
670
- if (reflector != null)
671
- return reflector.JsonSerializer.Deserialize<T>(reflector, element);
672
- return System.Text.Json.JsonSerializer.Deserialize<T>(element.GetRawText());
673
- }
674
- catch
675
- {
676
- return null;
677
- }
678
- }
679
-
680
- private static bool TryGetArgument(
681
- IReadOnlyDictionary<string, JsonElement> arguments,
682
- string name,
683
- out JsonElement value)
684
- {
685
- if (arguments.TryGetValue(name, out value))
686
- return true;
687
- foreach (var pair in arguments)
688
- {
689
- if (string.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase))
690
- {
691
- value = pair.Value;
692
- return true;
693
- }
694
- }
695
- value = default;
696
- return false;
697
- }
698
-
699
- private static bool TryGetString(
700
- IReadOnlyDictionary<string, JsonElement> arguments,
701
- string name,
702
- out string? value)
703
- {
704
- value = null;
705
- if (TryGetArgument(arguments, name, out var element) && element.ValueKind == JsonValueKind.String)
706
- {
707
- value = element.GetString();
708
- return true;
709
- }
710
- return false;
711
- }
712
-
713
- private static bool LooksLikeTarget(string name)
714
- => name.EndsWith("Ref", StringComparison.Ordinal)
715
- || name.EndsWith("Refs", StringComparison.Ordinal)
716
- || name.IndexOf("component", StringComparison.OrdinalIgnoreCase) >= 0;
717
-
718
- private static string? DisplayValue(JsonElement value)
719
- {
720
- if (value.ValueKind == JsonValueKind.String)
721
- return Bound(value.GetString());
722
- if (value.ValueKind == JsonValueKind.Object)
723
- {
724
- foreach (var name in new[] { "path", "name", "assetPath", "assetGuid", "instanceID", "entityId" })
725
- {
726
- foreach (var property in value.EnumerateObject())
727
- {
728
- if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
729
- return DisplayValue(property.Value);
730
- }
731
- }
732
- }
733
- return null;
734
- }
735
-
736
- private static string? SafeHierarchyPath(string? path)
737
- {
738
- if (string.IsNullOrEmpty(path)) return null;
739
- var trimmed = path!.Replace('\\', '/').TrimStart('/');
740
- return trimmed.Length == 0 ? null : Bound(trimmed);
741
- }
742
-
743
- public static string? SafeProjectRelative(string? path)
744
- {
745
- if (string.IsNullOrWhiteSpace(path)) return null;
746
- var dataPath = Application.dataPath.Replace('\\', '/');
747
- var normalized = path!.Replace('\\', '/');
748
- if (normalized.StartsWith(dataPath + "/", StringComparison.OrdinalIgnoreCase))
749
- return "Assets/" + normalized.Substring(dataPath.Length + 1);
750
- if (normalized.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
751
- return normalized;
752
- return null;
753
- }
754
-
755
- private static string? Bound(string? value)
756
- => value == null ? null : value.Length <= 160 ? value : value.Substring(0, 160);
757
- }
758
-
759
- /// <summary>
760
- /// Batch-level validator/planner. It inspects every child registration
761
- /// through the existing tool manager, but never invokes a child runner.
762
- /// The parent batch therefore cannot obtain a plan for an opaque or
763
- /// unsupported child and then execute it as if it were safe.
764
- /// </summary>
765
- public sealed class UnityBatchAuthoringValidator : IAuthoringValidator
766
- {
767
- public AuthoringValidationResult Validate(AuthoringInvocation invocation)
768
- {
769
- if (invocation == null)
770
- throw new ArgumentNullException(nameof(invocation));
771
-
772
- var errors = UnityBatchAuthoringPlanning.Validate(invocation);
773
- return new AuthoringValidationResult
774
- {
775
- Valid = errors.Count == 0,
776
- FailureCode = errors.Count == 0 ? null : errors[0],
777
- Details = errors.Count == 0
778
- ? null
779
- : new System.Text.Json.Nodes.JsonObject { ["reason"] = errors[0] },
780
- };
781
- }
782
- }
783
-
784
- public sealed class UnityBatchAuthoringPlanner : IAuthoringPlanner
785
- {
786
- public AuthoringPlanSummary Plan(AuthoringInvocation invocation)
787
- {
788
- if (invocation == null)
789
- throw new ArgumentNullException(nameof(invocation));
790
- return UnityBatchAuthoringPlanning.Plan(invocation);
791
- }
792
- }
793
-
794
- internal static class UnityBatchAuthoringPlanning
795
- {
796
- public static List<string> Validate(AuthoringInvocation invocation)
797
- {
798
- var errors = new List<string>();
799
- if (!TryReadCommands(invocation, out var commands, out var error))
800
- {
801
- errors.Add(error ?? "validation_failed");
802
- return errors;
803
- }
804
-
805
- if (commands.Count > Tool_Batch.HardMaxCommands)
806
- {
807
- errors.Add("validation_failed");
808
- return errors;
809
- }
810
-
811
- for (var index = 0; index < commands.Count; index++)
812
- {
813
- var command = commands[index];
814
- var runner = FindRunner(command.Tool);
815
- if (runner == null)
816
- {
817
- errors.Add("safety_unsupported");
818
- continue;
819
- }
820
-
821
- if (string.Equals(command.Tool, Tool_Batch.BatchExecuteToolId, StringComparison.Ordinal))
822
- {
823
- errors.Add("safety_unsupported");
824
- continue;
825
- }
826
-
827
- _ = AuthoringSafetyPolicy.Classify(runner);
828
- }
829
-
830
- return errors;
831
- }
832
-
833
- public static AuthoringPlanSummary Plan(AuthoringInvocation invocation)
834
- {
835
- if (!TryReadCommands(invocation, out var commands, out var error))
836
- throw new ToolCallControlException(
837
- ToolCallErrorCodes.InvalidControl,
838
- error ?? "Batch commands are invalid.");
839
-
840
- if (commands.Count > Tool_Batch.HardMaxCommands)
841
- throw new ToolCallControlException(
842
- ToolCallErrorCodes.InvalidControl,
843
- "The batch contains too many commands.",
844
- details: new System.Text.Json.Nodes.JsonObject
845
- {
846
- ["reason"] = "max_commands",
847
- });
848
-
849
- var targets = new List<AuthoringTargetSummary>();
850
- var effects = new List<string>();
851
- var childRecords = new List<AuthoringChildPlanSummary>();
852
- var aggregateUndo = AuthoringUndoLevel.Full;
853
- var supportsSharedTransaction = true;
854
- var hasMutatingChild = false;
855
- var policy = AuthoringSafetyPolicyContext.Current;
856
- if (policy == null)
857
- {
858
- throw new ToolCallControlException(
859
- ToolCallErrorCodes.SafetyUnsupported,
860
- "Batch planning requires the live authoring safety policy.",
861
- details: new System.Text.Json.Nodes.JsonObject { ["reason"] = "policy_scope_missing" });
862
- }
863
-
864
- var existingChildren = invocation.PlanBeingVerified?.ChildRecords;
865
- if (existingChildren != null && existingChildren.Count != commands.Count)
866
- {
867
- throw new ToolCallControlException(
868
- ToolCallErrorCodes.ConfirmationStale,
869
- "The batch child confirmation records no longer match the command list.");
870
- }
871
-
872
- for (var index = 0; index < commands.Count; index++)
873
- {
874
- var command = commands[index];
875
- var runner = FindRunner(command.Tool);
876
- if (runner == null
877
- || string.Equals(command.Tool, Tool_Batch.BatchExecuteToolId, StringComparison.Ordinal))
878
- {
879
- throw new ToolCallControlException(
880
- ToolCallErrorCodes.SafetyUnsupported,
881
- "A batch child does not expose a supported authoring capability.",
882
- details: new System.Text.Json.Nodes.JsonObject { ["reason"] = "child_capability_missing" });
883
- }
884
-
885
- var childContext = ToolCallContextNormalizer.DeriveChild(
886
- invocation.Context,
887
- new ToolCallControl
888
- {
889
- CallId = "batch-plan-" + index.ToString(System.Globalization.CultureInfo.InvariantCulture),
890
- },
891
- requestId: "batch-plan-request-" + index.ToString(System.Globalization.CultureInfo.InvariantCulture),
892
- cancellationToken: invocation.Context.CancellationToken);
893
- var childInvocation = policy.PrepareInvocation(new AuthoringInvocation(
894
- childContext,
895
- runner.Name,
896
- command.Parameters,
897
- runner));
898
- var descriptor = childInvocation.Descriptor;
899
- var risk = AuthoringSafetyPolicy.Classify(
900
- childInvocation.Runner.ReadOnlyHint,
901
- childInvocation.Runner.DestructiveHint,
902
- descriptor);
903
- if (descriptor != null
904
- && ((descriptor.SupportsPlanning && descriptor.Planner == null)
905
- || (descriptor.SupportsValidation && descriptor.Validator == null
906
- && descriptor.Planner == null)))
907
- {
908
- throw new ToolCallControlException(
909
- ToolCallErrorCodes.SafetyUnsupported,
910
- "A batch child has inconsistent authoring capability data.",
911
- details: new System.Text.Json.Nodes.JsonObject
912
- {
913
- ["reason"] = "binding_unavailable",
914
- ["stage"] = "declared_adapter_unavailable",
915
- });
916
- }
917
-
918
- AuthoringValidationResult? childValidation = null;
919
- if (descriptor?.Validator != null)
920
- {
921
- childValidation = descriptor.Validator.Validate(childInvocation);
922
- if (childValidation == null || !childValidation.Valid)
923
- {
924
- var failureCode = childValidation?.FailureCode;
925
- var safeFailureCode = string.IsNullOrWhiteSpace(failureCode)
926
- ? "validation_failed"
927
- : failureCode ?? "validation_failed";
928
- throw new ToolCallControlException(
929
- safeFailureCode,
930
- "A batch child did not pass validation.");
931
- }
932
- }
933
-
934
- var childPlan = descriptor?.Planner?.Plan(childInvocation);
935
- if (descriptor?.Planner != null && childPlan == null)
936
- {
937
- throw new ToolCallControlException(
938
- ToolCallErrorCodes.SafetyUnsupported,
939
- "A batch child planner returned no plan.",
940
- details: new System.Text.Json.Nodes.JsonObject
941
- {
942
- ["reason"] = "binding_unavailable",
943
- ["stage"] = "planner_returned_null",
944
- });
945
- }
946
-
947
- var validationTargets = childValidation?.Targets == null
948
- ? Array.Empty<AuthoringTargetSummary>()
949
- : childValidation.Targets.Where(target => target != null).Take(8)
950
- .Select(target => target.CloneSafe()).ToArray();
951
- var plannedTargets = childPlan?.Targets == null
952
- ? Array.Empty<AuthoringTargetSummary>()
953
- : childPlan.Targets.Where(target => target != null).Take(8)
954
- .Select(target => target.CloneSafe()).ToArray();
955
- IReadOnlyList<AuthoringTargetSummary> childTargets = plannedTargets.Length == 0
956
- ? validationTargets
957
- : plannedTargets;
958
- var plannedEffects = childPlan?.PredictedEffects == null
959
- ? Array.Empty<string>()
960
- : childPlan.PredictedEffects.Where(effect => !string.IsNullOrWhiteSpace(effect)).Take(8)
961
- .Select(effect => effect.Length <= 160 ? effect : effect.Substring(0, 160)).ToArray();
962
- IReadOnlyList<string> childEffects = plannedEffects.Length > 0
963
- ? plannedEffects
964
- : new[]
965
- {
966
- descriptor == null || descriptor.IsOpaque
967
- ? "undeclared"
968
- : descriptor.MutationKind == AuthoringMutationKind.Unknown
969
- ? "authoring change"
970
- : descriptor.MutationKind.ToString().ToLowerInvariant(),
971
- };
972
-
973
- AuthoringChildPlanSummary? existingAuthority = null;
974
- if (existingChildren != null)
975
- {
976
- existingAuthority = existingChildren[index];
977
- if (existingAuthority == null
978
- || existingAuthority.Index != index
979
- || !string.Equals(existingAuthority.ToolName, runner.Name, StringComparison.Ordinal))
980
- {
981
- throw new ToolCallControlException(
982
- ToolCallErrorCodes.ConfirmationStale,
983
- "The batch child confirmation records no longer match the command list.");
984
- }
985
- }
986
-
987
- var childRecord = policy.CreateAggregateChildRecord(
988
- index,
989
- childInvocation,
990
- childValidation,
991
- childPlan,
992
- childTargets,
993
- childEffects,
994
- existingAuthority);
995
- childRecords.Add(childRecord);
996
-
997
- if (risk != AuthoringRiskLevel.Read)
998
- {
999
- hasMutatingChild = true;
1000
- aggregateUndo = (AuthoringUndoLevel)Math.Min(
1001
- (int)aggregateUndo,
1002
- (int)childRecord.UndoLevel);
1003
- supportsSharedTransaction &= childRecord.SupportsSharedTransaction;
1004
- }
1005
-
1006
- targets.Add(new AuthoringTargetSummary
1007
- {
1008
- Kind = "batch-child",
1009
- Name = runner.Name,
1010
- Fingerprint = AuthoringConfirmationBinding.ComputeArgumentsHash(
1011
- new Dictionary<string, JsonElement>
1012
- {
1013
- ["index"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.Index),
1014
- ["tool"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.ToolName),
1015
- ["argumentsHash"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.ArgumentsHash),
1016
- ["risk"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.Risk),
1017
- ["undo"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.Undo),
1018
- ["planLevel"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.PlanLevel),
1019
- }),
1020
- });
1021
- targets.AddRange(childRecord.Targets.Select(target => target.CloneSafe()));
1022
- effects.AddRange(childRecord.PredictedEffects);
1023
- }
1024
-
1025
- if (effects.Count == 0)
1026
- effects.Add("authoring change");
1027
-
1028
- return new AuthoringPlanSummary
1029
- {
1030
- UndoLevel = aggregateUndo,
1031
- HasUndoOverride = true,
1032
- SupportsSharedTransaction = hasMutatingChild && supportsSharedTransaction,
1033
- Targets = targets.Take(32).Select(target => target.CloneSafe()).ToArray(),
1034
- PredictedEffects = effects.Take(32).ToArray(),
1035
- ChildRecords = childRecords.Take(Tool_Batch.HardMaxCommands)
1036
- .Select(child => child.CloneBounded()).ToArray(),
1037
- BeforeSummary = new System.Text.Json.Nodes.JsonObject
1038
- {
1039
- ["childCount"] = commands.Count,
1040
- },
1041
- AfterSummary = new System.Text.Json.Nodes.JsonObject
1042
- {
1043
- ["childCount"] = commands.Count,
1044
- ["predictedEffects"] = effects.Take(32).Aggregate(
1045
- new System.Text.Json.Nodes.JsonArray(),
1046
- (array, effect) =>
1047
- {
1048
- array.Add(effect);
1049
- return array;
1050
- }),
1051
- },
1052
- };
1053
- }
1054
-
1055
- private sealed class ParsedCommand
1056
- {
1057
- public string Tool { get; }
1058
- public Dictionary<string, JsonElement> Parameters { get; }
1059
-
1060
- public ParsedCommand(string tool, Dictionary<string, JsonElement> parameters)
1061
- {
1062
- Tool = tool;
1063
- Parameters = parameters;
1064
- }
1065
- }
1066
-
1067
- private static bool TryReadCommands(
1068
- AuthoringInvocation invocation,
1069
- out List<ParsedCommand> commands,
1070
- out string? error)
1071
- {
1072
- commands = new List<ParsedCommand>();
1073
- error = null;
1074
- if (!TryGetArgument(invocation.Arguments, "commands", out var value)
1075
- || value.ValueKind != JsonValueKind.Array)
1076
- {
1077
- error = "validation_failed";
1078
- return false;
1079
- }
1080
-
1081
- foreach (var item in value.EnumerateArray())
1082
- {
1083
- if (item.ValueKind != JsonValueKind.Object)
1084
- {
1085
- error = "validation_failed";
1086
- return false;
1087
- }
1088
- if (!TryGetProperty(item, "tool", out var toolValue)
1089
- || toolValue.ValueKind != JsonValueKind.String
1090
- || string.IsNullOrWhiteSpace(toolValue.GetString()))
1091
- {
1092
- error = "validation_failed";
1093
- return false;
1094
- }
1095
-
1096
- var parameters = new Dictionary<string, JsonElement>(StringComparer.Ordinal);
1097
- if (TryGetProperty(item, "params", out var paramsValue)
1098
- && paramsValue.ValueKind != JsonValueKind.Null)
1099
- {
1100
- if (paramsValue.ValueKind != JsonValueKind.Object)
1101
- {
1102
- error = "validation_failed";
1103
- return false;
1104
- }
1105
- foreach (var property in paramsValue.EnumerateObject())
1106
- parameters[property.Name] = property.Value.Clone();
1107
- }
1108
-
1109
- commands.Add(new ParsedCommand(toolValue.GetString()!, parameters));
1110
- }
1111
-
1112
- if (commands.Count == 0)
1113
- {
1114
- error = "validation_failed";
1115
- return false;
1116
- }
1117
- return true;
1118
- }
1119
-
1120
- private static IRunTool? FindRunner(string name)
1121
- {
1122
- if (!UnityCopilotPluginEditor.HasInstance)
1123
- return null;
1124
- var manager = UnityCopilotPluginEditor.Instance.Tools;
1125
- if (manager == null)
1126
- return null;
1127
- return manager.GetAllTools()
1128
- .FirstOrDefault(item => item != null && string.Equals(item.Name, name, StringComparison.Ordinal));
1129
- }
1130
-
1131
- private static bool TryGetArgument(
1132
- IReadOnlyDictionary<string, JsonElement> arguments,
1133
- string name,
1134
- out JsonElement value)
1135
- {
1136
- if (arguments.TryGetValue(name, out value)) return true;
1137
- foreach (var pair in arguments)
1138
- {
1139
- if (string.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase))
1140
- {
1141
- value = pair.Value;
1142
- return true;
1143
- }
1144
- }
1145
- value = default;
1146
- return false;
1147
- }
1148
-
1149
- private static bool TryGetProperty(JsonElement element, string name, out JsonElement value)
1150
- {
1151
- foreach (var property in element.EnumerateObject())
1152
- {
1153
- if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
1154
- {
1155
- value = property.Value;
1156
- return true;
1157
- }
1158
- }
1159
- value = default;
1160
- return false;
1161
- }
1162
- }
1163
-
1164
- /// <summary>
1165
- /// Read-only planner for the pilot. It performs the same live target
1166
- /// inspection as the validator and adds predicted effects plus bounded
1167
- /// before/after summaries. A plan for an invalid call is refused rather
1168
- /// than issued, so a confirmation token can only ever describe an action
1169
- /// whose targets were actually observed.
1170
- /// </summary>
1171
- public sealed class UnityPilotAuthoringPlanner : IAuthoringPlanner
1172
- {
1173
- public AuthoringPlanSummary Plan(AuthoringInvocation invocation)
1174
- {
1175
- if (invocation == null)
1176
- throw new ArgumentNullException(nameof(invocation));
1177
-
1178
- var inspection = UnityPilotAuthoringInspection.Inspect(invocation);
1179
- if (inspection.Errors.Count > 0)
1180
- {
1181
- throw new ToolCallControlException(
1182
- "validation_failed",
1183
- "The authoring call did not pass validation.",
1184
- details: new System.Text.Json.Nodes.JsonObject
1185
- {
1186
- ["reason"] = inspection.Errors[0],
1187
- ["errorCount"] = inspection.Errors.Count,
1188
- });
1189
- }
1190
-
1191
- var kind = invocation.Descriptor?.MutationKind ?? AuthoringMutationKind.Unknown;
1192
- var effects = inspection.Effects.Count == 0
1193
- ? new List<string> { kind == AuthoringMutationKind.Unknown ? "authoring change" : kind.ToString().ToLowerInvariant() }
1194
- : inspection.Effects;
1195
- return new AuthoringPlanSummary
1196
- {
1197
- Targets = inspection.Targets.Take(32).ToArray(),
1198
- PredictedEffects = effects.Take(32).ToArray(),
1199
- BeforeSummary = inspection.Before,
1200
- AfterSummary = inspection.After,
1201
- };
1202
- }
1203
- }
1204
-
1205
- /// <summary>Unity Undo-backed transaction factory attached to pilot runners.</summary>
1206
- public sealed class UnityAuthoringTransactionFactory : IAuthoringTransactionFactory
1207
- {
1208
- public IAuthoringTransaction Begin(AuthoringInvocation invocation)
1209
- {
1210
- if (invocation == null)
1211
- throw new ArgumentNullException(nameof(invocation));
1212
-
1213
- return BeginStandalone(
1214
- "MCP: " + invocation.Name,
1215
- invocation.EffectiveUndoLevel
1216
- ?? invocation.Descriptor?.UndoLevel
1217
- ?? AuthoringUndoLevel.Partial);
1218
- }
1219
-
1220
- /// <summary>
1221
- /// Opens one named Unity Undo group and returns the transaction that
1222
- /// owns it. Complete collapses the group to one user-visible step;
1223
- /// Abort reverts only this group (never unrelated earlier history).
1224
- /// </summary>
1225
- public static IAuthoringTransaction BeginStandalone(string label, AuthoringUndoLevel advertisedUndo)
1226
- {
1227
- if (string.IsNullOrWhiteSpace(label))
1228
- throw new ArgumentException("Transaction label must be non-empty.", nameof(label));
1229
-
1230
- // The middleware may run on the transport thread; every Undo API
1231
- // call is marshalled to the main thread (inline when already there).
1232
- var group = MainThread.Instance.Run(() =>
1233
- {
1234
- Undo.IncrementCurrentGroup();
1235
- var current = Undo.GetCurrentGroup();
1236
- Undo.SetCurrentGroupName(label);
1237
- return current;
1238
- });
1239
-
1240
- return new UnityAuthoringTransaction(
1241
- label,
1242
- advertisedUndo,
1243
- group,
1244
- recordCreated: value => MainThread.Instance.Run(() => Undo.RegisterCreatedObjectUndo(
1245
- RequireObject(value), label)),
1246
- recordModified: (value, completeSnapshot) => MainThread.Instance.Run(() =>
1247
- {
1248
- var target = RequireObject(value);
1249
- if (completeSnapshot)
1250
- Undo.RegisterCompleteObjectUndo(target, label);
1251
- else
1252
- Undo.RecordObject(target, label);
1253
- }),
1254
- recordDeleted: value => MainThread.Instance.Run(() => Undo.RegisterCompleteObjectUndo(
1255
- RequireObject(value), label)),
1256
- complete: () => MainThread.Instance.Run(() => Undo.CollapseUndoOperations(group)),
1257
- abort: () => MainThread.Instance.Run(() => Undo.RevertAllDownToGroup(group)));
1258
- }
1259
-
1260
- /// <summary>
1261
- /// Test/tooling helper: opens a standalone transaction, installs it as
1262
- /// the ambient scope, and completes it on dispose. Production calls
1263
- /// always receive their scope from <see cref="AuthoringSafetyMiddleware"/>.
1264
- /// </summary>
1265
- public static IDisposable BeginStandaloneScope(string label, AuthoringUndoLevel advertisedUndo = AuthoringUndoLevel.Full)
1266
- {
1267
- var transaction = BeginStandalone(label, advertisedUndo);
1268
- var scope = AuthoringTransactionScope.Push(transaction);
1269
- return new StandaloneScope(transaction, scope);
1270
- }
1271
-
1272
- private sealed class StandaloneScope : IDisposable
1273
- {
1274
- private readonly IAuthoringTransaction _transaction;
1275
- private readonly IDisposable _scope;
1276
- private int _disposed;
1277
-
1278
- public StandaloneScope(IAuthoringTransaction transaction, IDisposable scope)
1279
- {
1280
- _transaction = transaction;
1281
- _scope = scope;
1282
- }
1283
-
1284
- public void Dispose()
1285
- {
1286
- if (System.Threading.Interlocked.Exchange(ref _disposed, 1) != 0)
1287
- return;
1288
- try
1289
- {
1290
- _transaction.Complete();
1291
- }
1292
- finally
1293
- {
1294
- _scope.Dispose();
1295
- _transaction.Dispose();
1296
- }
1297
- }
1298
- }
1299
-
1300
- private static UnityEngine.Object RequireObject(object value)
1301
- => value as UnityEngine.Object
1302
- ?? throw new InvalidOperationException("The authoring target is not a Unity object.");
1303
- }
1304
-
1305
- /// <summary>
1306
- /// Unity-aware transaction: affected-object summaries carry the object's
1307
- /// type and name instead of the generic <c>ToString()</c> fallback, and
1308
- /// hierarchy paths are reported as safe relative display values.
1309
- /// </summary>
1310
- public sealed class UnityAuthoringTransaction : AuthoringTransaction
1311
- {
1312
- public UnityAuthoringTransaction(
1313
- string groupLabel,
1314
- AuthoringUndoLevel advertisedUndo,
1315
- int groupId,
1316
- Action<object>? recordCreated,
1317
- Action<object, bool>? recordModified,
1318
- Action<object>? recordDeleted,
1319
- Action? complete,
1320
- Action? abort)
1321
- : base(groupLabel, advertisedUndo, groupId, recordCreated, recordModified, recordDeleted, complete, abort)
1322
- {
1323
- }
1324
-
1325
- private readonly HashSet<string> _reported = new HashSet<string>(StringComparer.Ordinal);
1326
-
1327
- protected override void AddAffected(object value)
1328
- {
1329
- if (value is UnityEngine.Object unityObject && unityObject != null)
1330
- {
1331
- // One object may be recorded more than once inside a call
1332
- // (for example RegisterCreatedObjectUndo followed by a
1333
- // complete snapshot); report it once.
1334
- #if UNITY_6000_5_OR_NEWER
1335
- var objectId = unityObject.GetEntityId().ToString();
1336
- #else
1337
- var objectId = unityObject.GetInstanceID().ToString(System.Globalization.CultureInfo.InvariantCulture);
1338
- #endif
1339
- var path = value is GameObject go
1340
- ? go.GetPath()
1341
- : value is UnityEngine.Component component && component != null
1342
- ? component.gameObject.GetPath()
1343
- : null;
1344
- var summary = new AuthoringAffectedObjectSummary
1345
- {
1346
- Kind = value.GetType().Name,
1347
- Name = unityObject.name,
1348
- RelativePath = string.IsNullOrEmpty(path) ? null : path!.Replace('\\', '/').TrimStart('/'),
1349
- };
1350
- if (!_reported.Add(objectId))
1351
- {
1352
- AddAffectedEvent(summary);
1353
- return;
1354
- }
1355
-
1356
- AddAffectedSummary(summary);
1357
- return;
1358
- }
1359
-
1360
- base.AddAffected(value);
1361
- }
1362
- }
1363
-
1364
- /// <summary>
1365
- /// Small helper used by pilot tools. Every mutation records through the
1366
- /// ambient transaction before changing Unity state.
1367
- /// </summary>
1368
- public static class UnityAuthoringUndo
1369
- {
1370
- /// <summary>
1371
- /// Fails closed before the first mutation of a pilot tool body when
1372
- /// the call did not arrive through the policy pipeline. An explicit
1373
- /// ambient transaction (opened by the middleware, or deliberately by
1374
- /// in-process tooling/tests) is accepted; a bare reflected runner
1375
- /// call is not, so no object is created and then orphaned by a later
1376
- /// missing-transaction failure.
1377
- /// </summary>
1378
- public static void RequireAuthoringScope()
1379
- {
1380
- if (AuthoringTransactionScope.Current != null)
1381
- return;
1382
- AuthoringPolicyApproval.Require();
1383
- }
1384
-
1385
- public static void RecordCreated(UnityEngine.Object value)
1386
- {
1387
- var transaction = AuthoringTransactionScope.Current;
1388
- if (transaction == null)
1389
- throw new InvalidOperationException("An authoring transaction is required.");
1390
- transaction.RecordCreated(value);
1391
- }
1392
-
1393
- public static void RecordModified(UnityEngine.Object value, bool completeSnapshot = false)
1394
- {
1395
- var transaction = AuthoringTransactionScope.Current;
1396
- if (transaction == null)
1397
- throw new InvalidOperationException("An authoring transaction is required.");
1398
- transaction.RecordModified(value, completeSnapshot);
1399
- }
1400
-
1401
- public static void Destroy(UnityEngine.Object value)
1402
- {
1403
- var transaction = AuthoringTransactionScope.Current;
1404
- if (transaction == null)
1405
- throw new InvalidOperationException("An authoring transaction is required.");
1406
- transaction.RecordDeleted(value);
1407
- Undo.DestroyObjectImmediate(value);
1408
- transaction.MarkMutated();
1409
- }
1410
-
1411
- /// <summary>
1412
- /// Undo-aware reparenting. <c>Undo.RecordObject</c> on a Transform
1413
- /// does not capture hierarchy changes; Unity's
1414
- /// <c>Undo.SetTransformParent</c> records the parent change inside
1415
- /// the current group.
1416
- /// </summary>
1417
- public static void SetParent(Transform target, Transform? parent, bool worldPositionStays)
1418
- {
1419
- var transaction = AuthoringTransactionScope.Current;
1420
- if (transaction == null)
1421
- throw new InvalidOperationException("An authoring transaction is required.");
1422
- if (target == null)
1423
- throw new ArgumentNullException(nameof(target));
1424
- transaction.RecordHostRegistered(target);
1425
- Undo.SetTransformParent(target, parent, worldPositionStays, "MCP: set parent");
1426
- transaction.MarkMutated();
1427
- }
1428
-
1429
- /// <summary>
1430
- /// Undo-aware component creation. <c>Undo.AddComponent</c> creates
1431
- /// and registers the component in one step; a second
1432
- /// <c>RegisterCreatedObjectUndo</c> would duplicate the record.
1433
- /// </summary>
1434
- public static UnityEngine.Component? AddComponent(GameObject target, Type componentType)
1435
- {
1436
- var transaction = AuthoringTransactionScope.Current;
1437
- if (transaction == null)
1438
- throw new InvalidOperationException("An authoring transaction is required.");
1439
- if (target == null)
1440
- throw new ArgumentNullException(nameof(target));
1441
- if (componentType == null)
1442
- throw new ArgumentNullException(nameof(componentType));
1443
-
1444
- var component = Undo.AddComponent(target, componentType);
1445
- if (component == null)
1446
- return null;
1447
- transaction.RecordHostRegistered(component);
1448
- transaction.MarkMutated();
1449
- return component;
1450
- }
1451
-
1452
- /// <summary>
1453
- /// Tracks an object that a Unity API already registered with Undo
1454
- /// inside the current group (for example pasteboard duplication).
1455
- /// </summary>
1456
- public static void RecordHostRegistered(UnityEngine.Object value)
1457
- {
1458
- var transaction = AuthoringTransactionScope.Current;
1459
- if (transaction == null)
1460
- throw new InvalidOperationException("An authoring transaction is required.");
1461
- transaction.RecordHostRegistered(value);
1462
- }
1463
-
1464
- public static void MarkMutated()
1465
- => AuthoringTransactionScope.Current?.MarkMutated();
1466
- }
1467
- }
1
+ /*
2
+ ┌──────────────────────────────────────────────────────────────────┐
3
+ │ Author: AtelierAI │
4
+ │ Copyright (c) 2025 AtelierAI │
5
+ │ Licensed under the MIT License. │
6
+ └──────────────────────────────────────────────────────────────────┘
7
+ */
8
+ #nullable enable
9
+
10
+ using System;
11
+ using System.Collections.Generic;
12
+ using System.Linq;
13
+ using System.Text.Json;
14
+ using AIGD;
15
+ using com.AtelierAI.Uco.Framework;
16
+ using com.AtelierAI.Uco.Framework.Common.Model;
17
+ using com.IvanMurzak.ReflectorNet.Utils;
18
+ using com.AtelierAI.Unity.Copilot.Editor.API;
19
+ using com.AtelierAI.Unity.Copilot.Runtime.Extensions;
20
+ using com.AtelierAI.Unity.Copilot.Runtime.Utils;
21
+ using UnityEditor;
22
+ using UnityEditor.SceneManagement;
23
+ using UnityEngine;
24
+
25
+ namespace com.AtelierAI.Unity.Copilot.Editor.Utils
26
+ {
27
+ /// <summary>
28
+ /// Read-only validator shared by the Scene/GameObject/Component pilot.
29
+ /// It resolves the real Unity targets (objects, components, scenes,
30
+ /// scene assets, and canonical paths) on the main thread without
31
+ /// creating, modifying, destroying, dirtying, or saving anything. The
32
+ /// reflected runner is deliberately never called from this adapter.
33
+ /// </summary>
34
+ public sealed class UnityPilotAuthoringValidator : IAuthoringValidator
35
+ {
36
+ public AuthoringValidationResult Validate(AuthoringInvocation invocation)
37
+ {
38
+ if (invocation == null)
39
+ throw new ArgumentNullException(nameof(invocation));
40
+
41
+ var inspection = UnityPilotAuthoringInspection.Inspect(invocation);
42
+ return new AuthoringValidationResult
43
+ {
44
+ Valid = inspection.Errors.Count == 0,
45
+ FailureCode = inspection.Errors.Count == 0 ? null : "validation_failed",
46
+ Targets = inspection.Targets.Take(32).ToArray(),
47
+ Details = inspection.Errors.Count == 0
48
+ ? null
49
+ : new System.Text.Json.Nodes.JsonObject
50
+ {
51
+ ["reason"] = inspection.Errors[0],
52
+ ["errorCount"] = inspection.Errors.Count,
53
+ },
54
+ };
55
+ }
56
+ }
57
+
58
+ /// <summary>
59
+ /// Result of one read-only pilot inspection. Targets carry a state
60
+ /// fingerprint derived from the live Unity object (identity, hierarchy
61
+ /// path, owning scene/stage, component set) so a later change to the
62
+ /// inspected target makes an issued confirmation plan stale.
63
+ /// </summary>
64
+ internal sealed class UnityPilotInspection
65
+ {
66
+ public List<AuthoringTargetSummary> Targets { get; } = new List<AuthoringTargetSummary>();
67
+ public List<string> Errors { get; } = new List<string>();
68
+ public List<string> Effects { get; } = new List<string>();
69
+ public System.Text.Json.Nodes.JsonObject Before { get; } = new System.Text.Json.Nodes.JsonObject();
70
+ public System.Text.Json.Nodes.JsonObject After { get; } = new System.Text.Json.Nodes.JsonObject();
71
+ }
72
+
73
+ /// <summary>
74
+ /// Main-thread, read-only inspection of the pilot tools' targets. Every
75
+ /// lookup uses the same resolution helpers the runners use
76
+ /// (<c>GameObjectUtils.FindBy</c>, <c>FindAssetObject</c>,
77
+ /// <c>ComponentRef.Matches</c>), but no Unity API with a side effect is
78
+ /// reachable from this type.
79
+ /// </summary>
80
+ internal static class UnityPilotAuthoringInspection
81
+ {
82
+ private const int MaxListedTargets = 32;
83
+
84
+ public static UnityPilotInspection Inspect(AuthoringInvocation invocation)
85
+ => MainThread.Instance.Run(() => InspectOnMainThread(invocation));
86
+
87
+ private static UnityPilotInspection InspectOnMainThread(AuthoringInvocation invocation)
88
+ {
89
+ var result = new UnityPilotInspection();
90
+ foreach (var binding in invocation.PathBindings.Values)
91
+ {
92
+ if (binding.Resolution.IsReadOnly
93
+ && binding.Resolution.Intent != ProjectPathAccessIntent.Read)
94
+ result.Errors.Add("path_read_only");
95
+ }
96
+
97
+ switch (invocation.Name)
98
+ {
99
+ case Tool_GameObject.GameObjectCreateToolId:
100
+ InspectGameObjectCreate(invocation, result);
101
+ break;
102
+ case Tool_GameObject.GameObjectDestroyToolId:
103
+ InspectSingleGameObject(invocation, result, "gameObjectRef", "destroy");
104
+ break;
105
+ case Tool_GameObject.GameObjectModifyToolId:
106
+ InspectGameObjectList(invocation, result, "modify");
107
+ break;
108
+ case Tool_GameObject.GameObjectDuplicateToolId:
109
+ InspectGameObjectList(invocation, result, "duplicate");
110
+ break;
111
+ case Tool_GameObject.GameObjectSetParentToolId:
112
+ InspectGameObjectList(invocation, result, "parent");
113
+ InspectOptionalOrRequiredGameObject(invocation, result, "parentGameObjectRef", required: true, kind: "Parent");
114
+ break;
115
+ case Tool_GameObject.GameObjectComponentAddToolId:
116
+ InspectComponentAdd(invocation, result);
117
+ break;
118
+ case Tool_GameObject.GameObjectComponentModifyToolId:
119
+ InspectComponentModify(invocation, result);
120
+ break;
121
+ case Tool_GameObject.GameObjectComponentDestroyToolId:
122
+ InspectComponentDestroy(invocation, result);
123
+ break;
124
+ case Tool_Scene.SceneOpenToolId:
125
+ InspectSceneAsset(invocation, result, requireOpened: false, effect: "open scene");
126
+ break;
127
+ case Tool_Scene.SceneSetActiveToolId:
128
+ InspectSceneAsset(invocation, result, requireOpened: true, effect: "set active scene");
129
+ break;
130
+ case Tool_Scene.SceneCreateToolId:
131
+ InspectSceneCreate(invocation, result);
132
+ break;
133
+ case Tool_Scene.SceneSaveToolId:
134
+ InspectSceneSave(invocation, result);
135
+ break;
136
+ case Tool_Scene.SceneUnloadToolId:
137
+ InspectSceneUnload(invocation, result);
138
+ break;
139
+ default:
140
+ InspectArgumentTargets(invocation, result);
141
+ break;
142
+ }
143
+
144
+ result.Before["targetCount"] = result.Targets.Count;
145
+ result.Before["activeScene"] = SafeSceneDisplay(EditorSceneManager.GetActiveScene());
146
+ result.After["targetCount"] = result.Targets.Count;
147
+ result.After["predictedEffects"] = result.Effects.Take(32).Aggregate(
148
+ new System.Text.Json.Nodes.JsonArray(),
149
+ (array, effect) =>
150
+ {
151
+ array.Add(effect);
152
+ return array;
153
+ });
154
+ return result;
155
+ }
156
+
157
+ private static void InspectGameObjectCreate(AuthoringInvocation invocation, UnityPilotInspection result)
158
+ {
159
+ TryGetString(invocation.Arguments, "name", out var name);
160
+ if (string.IsNullOrEmpty(name))
161
+ result.Errors.Add("name_missing");
162
+
163
+ var parentFingerprint = string.Empty;
164
+ if (TryGetArgument(invocation.Arguments, "parentGameObjectRef", out var parentElement)
165
+ && parentElement.ValueKind == JsonValueKind.Object)
166
+ {
167
+ var parentRef = Deserialize<GameObjectRef>(parentElement);
168
+ if (parentRef != null && parentRef.IsValid(out _))
169
+ {
170
+ var parent = GameObjectUtils.FindBy(parentRef, out var error);
171
+ if (parent == null)
172
+ {
173
+ result.Errors.Add("parent_not_found");
174
+ }
175
+ else
176
+ {
177
+ var target = GameObjectTarget(parent, "Parent");
178
+ parentFingerprint = target.Fingerprint ?? string.Empty;
179
+ result.Targets.Add(target);
180
+ }
181
+ }
182
+ }
183
+
184
+ var container = ContainerFingerprint();
185
+ result.Targets.Add(new AuthoringTargetSummary
186
+ {
187
+ Kind = "GameObject",
188
+ Name = Bound(name),
189
+ Fingerprint = Hash("create|" + name + "|" + container + "|" + parentFingerprint),
190
+ });
191
+ result.Effects.Add("create GameObject '" + Bound(name) + "'");
192
+ }
193
+
194
+ private static void InspectSingleGameObject(
195
+ AuthoringInvocation invocation,
196
+ UnityPilotInspection result,
197
+ string argumentName,
198
+ string effect)
199
+ {
200
+ var go = ResolveRequiredGameObject(invocation, result, argumentName, "GameObject");
201
+ if (go == null)
202
+ return;
203
+ result.Effects.Add(effect + " GameObject '" + Bound(go.name) + "' (" + go.transform.childCount + " children)");
204
+ }
205
+
206
+ private static void InspectOptionalOrRequiredGameObject(
207
+ AuthoringInvocation invocation,
208
+ UnityPilotInspection result,
209
+ string argumentName,
210
+ bool required,
211
+ string kind)
212
+ {
213
+ if (!TryGetArgument(invocation.Arguments, argumentName, out var element)
214
+ || element.ValueKind != JsonValueKind.Object)
215
+ {
216
+ if (required)
217
+ result.Errors.Add("target_missing");
218
+ return;
219
+ }
220
+
221
+ var reference = Deserialize<GameObjectRef>(element);
222
+ if (reference == null || !reference.IsValid(out _))
223
+ {
224
+ if (required)
225
+ result.Errors.Add("target_invalid");
226
+ return;
227
+ }
228
+
229
+ var go = GameObjectUtils.FindBy(reference, out _);
230
+ if (go == null)
231
+ {
232
+ result.Errors.Add("target_not_found");
233
+ return;
234
+ }
235
+ result.Targets.Add(GameObjectTarget(go, kind));
236
+ }
237
+
238
+ private static void InspectGameObjectList(
239
+ AuthoringInvocation invocation,
240
+ UnityPilotInspection result,
241
+ string effect)
242
+ {
243
+ if (!TryGetArgument(invocation.Arguments, "gameObjectRefs", out var element)
244
+ || element.ValueKind != JsonValueKind.Array)
245
+ {
246
+ result.Errors.Add("target_missing");
247
+ return;
248
+ }
249
+
250
+ var list = Deserialize<GameObjectRefList>(element);
251
+ if (list == null || list.Count == 0)
252
+ {
253
+ result.Errors.Add("target_missing");
254
+ return;
255
+ }
256
+
257
+ var resolved = 0;
258
+ foreach (var reference in list)
259
+ {
260
+ if (reference == null || !reference.IsValid(out _))
261
+ {
262
+ result.Errors.Add("target_invalid");
263
+ continue;
264
+ }
265
+ var go = GameObjectUtils.FindBy(reference, out _);
266
+ if (go == null)
267
+ {
268
+ result.Errors.Add("target_not_found");
269
+ continue;
270
+ }
271
+ resolved++;
272
+ if (result.Targets.Count < MaxListedTargets)
273
+ result.Targets.Add(GameObjectTarget(go, "GameObject"));
274
+ }
275
+ result.Effects.Add(effect + " " + resolved + " GameObject(s)");
276
+ }
277
+
278
+ private static void InspectComponentAdd(AuthoringInvocation invocation, UnityPilotInspection result)
279
+ {
280
+ var go = ResolveRequiredGameObject(invocation, result, "gameObjectRef", "GameObject");
281
+ if (!TryGetArgument(invocation.Arguments, "componentNames", out var namesElement)
282
+ || namesElement.ValueKind != JsonValueKind.Array)
283
+ {
284
+ result.Errors.Add("component_names_missing");
285
+ return;
286
+ }
287
+
288
+ var count = 0;
289
+ foreach (var item in namesElement.EnumerateArray())
290
+ {
291
+ if (item.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(item.GetString()))
292
+ {
293
+ result.Errors.Add("component_name_invalid");
294
+ continue;
295
+ }
296
+
297
+ var componentName = item.GetString()!;
298
+ var type = TypeUtils.GetType(componentName)
299
+ ?? Tool_GameObject.AllComponentTypes.FirstOrDefault(t => t.Name == componentName);
300
+ if (type == null)
301
+ {
302
+ result.Errors.Add("component_type_not_found");
303
+ continue;
304
+ }
305
+ if (!typeof(UnityEngine.Component).IsAssignableFrom(type))
306
+ {
307
+ result.Errors.Add("component_type_not_component");
308
+ continue;
309
+ }
310
+ count++;
311
+ result.Targets.Add(new AuthoringTargetSummary
312
+ {
313
+ Kind = "ComponentType",
314
+ Name = Bound(type.FullName),
315
+ Fingerprint = Hash("component-type|" + type.AssemblyQualifiedName),
316
+ });
317
+ }
318
+
319
+ if (count == 0 && result.Errors.Count == 0)
320
+ result.Errors.Add("component_names_missing");
321
+ if (go != null)
322
+ result.Effects.Add("add " + count + " component(s) to GameObject '" + Bound(go.name) + "'");
323
+ }
324
+
325
+ private static void InspectComponentModify(AuthoringInvocation invocation, UnityPilotInspection result)
326
+ {
327
+ var go = ResolveRequiredGameObject(invocation, result, "gameObjectRef", "GameObject");
328
+ if (go == null)
329
+ return;
330
+
331
+ if (!TryGetArgument(invocation.Arguments, "componentRef", out var element)
332
+ || element.ValueKind != JsonValueKind.Object)
333
+ {
334
+ result.Errors.Add("component_missing");
335
+ return;
336
+ }
337
+
338
+ var reference = Deserialize<ComponentRef>(element);
339
+ if (reference == null || !reference.IsValid(out _))
340
+ {
341
+ result.Errors.Add("component_invalid");
342
+ return;
343
+ }
344
+
345
+ var component = FindComponent(go, reference);
346
+ if (component == null)
347
+ {
348
+ result.Errors.Add("component_not_found");
349
+ return;
350
+ }
351
+
352
+ result.Targets.Add(ComponentTarget(component));
353
+ result.Effects.Add("modify component '" + Bound(component.GetType().Name) + "' on GameObject '" + Bound(go.name) + "'");
354
+ }
355
+
356
+ private static void InspectComponentDestroy(AuthoringInvocation invocation, UnityPilotInspection result)
357
+ {
358
+ var go = ResolveRequiredGameObject(invocation, result, "gameObjectRef", "GameObject");
359
+ if (go == null)
360
+ return;
361
+
362
+ if (!TryGetArgument(invocation.Arguments, "destroyComponentRefs", out var element)
363
+ || element.ValueKind != JsonValueKind.Array)
364
+ {
365
+ result.Errors.Add("component_missing");
366
+ return;
367
+ }
368
+
369
+ var references = Deserialize<ComponentRefList>(element);
370
+ if (references == null || references.Count == 0)
371
+ {
372
+ result.Errors.Add("component_missing");
373
+ return;
374
+ }
375
+
376
+ var count = 0;
377
+ foreach (var reference in references)
378
+ {
379
+ if (reference == null || !reference.IsValid(out _))
380
+ {
381
+ result.Errors.Add("component_invalid");
382
+ continue;
383
+ }
384
+ var component = FindComponent(go, reference);
385
+ if (component == null)
386
+ {
387
+ result.Errors.Add("component_not_found");
388
+ continue;
389
+ }
390
+ count++;
391
+ if (result.Targets.Count < MaxListedTargets)
392
+ result.Targets.Add(ComponentTarget(component));
393
+ }
394
+ result.Effects.Add("destroy " + count + " component(s) on GameObject '" + Bound(go.name) + "'");
395
+ }
396
+
397
+ private static void InspectSceneAsset(
398
+ AuthoringInvocation invocation,
399
+ UnityPilotInspection result,
400
+ bool requireOpened,
401
+ string effect)
402
+ {
403
+ if (!TryGetArgument(invocation.Arguments, "sceneRef", out var element)
404
+ || element.ValueKind != JsonValueKind.Object)
405
+ {
406
+ result.Errors.Add("target_missing");
407
+ return;
408
+ }
409
+
410
+ var reference = Deserialize<AssetObjectRef>(element);
411
+ var sceneAsset = reference == null ? null : reference.FindAssetObject<SceneAsset>();
412
+ if (sceneAsset == null)
413
+ {
414
+ result.Errors.Add("scene_asset_not_found");
415
+ return;
416
+ }
417
+
418
+ var assetPath = AssetDatabase.GetAssetPath(sceneAsset);
419
+ var guid = AssetDatabase.AssetPathToGUID(assetPath);
420
+ if (requireOpened)
421
+ {
422
+ var opened = UnityEngine.SceneManagement.SceneManager.GetSceneByPath(assetPath);
423
+ if (!opened.IsValid())
424
+ opened = UnityEngine.SceneManagement.SceneManager.GetSceneByName(sceneAsset.name);
425
+ if (!opened.IsValid() || !opened.isLoaded)
426
+ result.Errors.Add("scene_not_opened");
427
+ }
428
+
429
+ result.Targets.Add(new AuthoringTargetSummary
430
+ {
431
+ Kind = "SceneAsset",
432
+ Name = Bound(sceneAsset.name),
433
+ RelativePath = SafeProjectRelative(assetPath),
434
+ Fingerprint = Hash("scene-asset|" + assetPath + "|" + guid),
435
+ });
436
+ result.Effects.Add(effect + " '" + Bound(sceneAsset.name) + "'");
437
+ }
438
+
439
+ private static void InspectSceneCreate(AuthoringInvocation invocation, UnityPilotInspection result)
440
+ {
441
+ if (!invocation.PathBindings.TryGetValue("path", out var binding))
442
+ {
443
+ result.Errors.Add("path_missing");
444
+ return;
445
+ }
446
+
447
+ var relative = binding.RelativePath;
448
+ if (!relative.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
449
+ result.Errors.Add("scene_extension");
450
+ // Creating over an existing scene file is an overwrite, which
451
+ // the pilot never performs implicitly.
452
+ if (binding.Resolution.Exists)
453
+ result.Errors.Add("scene_exists");
454
+
455
+ result.Targets.Add(new AuthoringTargetSummary
456
+ {
457
+ Kind = "SceneFile",
458
+ Name = Bound(System.IO.Path.GetFileName(relative)),
459
+ RelativePath = relative,
460
+ Fingerprint = Hash("scene-file|" + binding.CanonicalPath + "|exists=" + binding.Resolution.Exists),
461
+ });
462
+ result.Effects.Add("create scene file '" + relative + "'");
463
+ }
464
+
465
+ private static void InspectSceneSave(AuthoringInvocation invocation, UnityPilotInspection result)
466
+ {
467
+ TryGetString(invocation.Arguments, "openedSceneName", out var openedSceneName);
468
+ var scene = string.IsNullOrEmpty(openedSceneName)
469
+ ? EditorSceneManager.GetActiveScene()
470
+ : SceneUtils.GetAllOpenedScenes().FirstOrDefault(candidate => candidate.name == openedSceneName);
471
+ if (!scene.IsValid())
472
+ {
473
+ result.Errors.Add("scene_not_found");
474
+ return;
475
+ }
476
+
477
+ string? destination = null;
478
+ if (invocation.PathBindings.TryGetValue("path", out var binding))
479
+ {
480
+ destination = binding.RelativePath;
481
+ if (!destination.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
482
+ result.Errors.Add("scene_extension");
483
+ }
484
+ else if (!string.IsNullOrEmpty(scene.path))
485
+ {
486
+ destination = SafeProjectRelative(scene.path);
487
+ }
488
+
489
+ if (string.IsNullOrEmpty(destination))
490
+ result.Errors.Add("scene_path_missing");
491
+
492
+ result.Targets.Add(new AuthoringTargetSummary
493
+ {
494
+ Kind = "Scene",
495
+ Name = Bound(scene.name),
496
+ RelativePath = destination,
497
+ Fingerprint = Hash("scene|" + scene.name + "|" + scene.path + "|" + scene.rootCount + "|" + destination),
498
+ });
499
+ result.Effects.Add("save scene '" + Bound(scene.name) + "' to '" + (destination ?? "?") + "'");
500
+ }
501
+
502
+ private static void InspectSceneUnload(AuthoringInvocation invocation, UnityPilotInspection result)
503
+ {
504
+ TryGetString(invocation.Arguments, "name", out var name);
505
+ if (string.IsNullOrEmpty(name))
506
+ {
507
+ result.Errors.Add("target_missing");
508
+ return;
509
+ }
510
+
511
+ var scene = SceneUtils.GetAllOpenedScenes().FirstOrDefault(candidate => candidate.name == name);
512
+ if (!scene.IsValid())
513
+ {
514
+ result.Errors.Add("scene_not_found");
515
+ return;
516
+ }
517
+
518
+ result.Targets.Add(new AuthoringTargetSummary
519
+ {
520
+ Kind = "Scene",
521
+ Name = Bound(scene.name),
522
+ RelativePath = SafeProjectRelative(scene.path),
523
+ Fingerprint = Hash("scene|" + scene.name + "|" + scene.path + "|" + scene.rootCount),
524
+ });
525
+ result.Effects.Add("unload scene '" + Bound(scene.name) + "'");
526
+ }
527
+
528
+ /// <summary>
529
+ /// Fallback for pilot-attributed tools without a dedicated inspector:
530
+ /// the binding still covers every reference-like argument.
531
+ /// </summary>
532
+ private static void InspectArgumentTargets(AuthoringInvocation invocation, UnityPilotInspection result)
533
+ {
534
+ foreach (var argument in invocation.Arguments)
535
+ {
536
+ if (!LooksLikeTarget(argument.Key) && !string.Equals(argument.Key, "path", StringComparison.OrdinalIgnoreCase))
537
+ continue;
538
+ result.Targets.Add(new AuthoringTargetSummary
539
+ {
540
+ Kind = argument.Key,
541
+ Name = DisplayValue(argument.Value),
542
+ Fingerprint = AuthoringConfirmationBinding.ComputeArgumentsHash(
543
+ new Dictionary<string, JsonElement> { [argument.Key] = argument.Value.Clone() }),
544
+ });
545
+ }
546
+ var kind = invocation.Descriptor?.MutationKind ?? AuthoringMutationKind.Unknown;
547
+ result.Effects.Add(kind == AuthoringMutationKind.Unknown ? "authoring change" : kind.ToString().ToLowerInvariant());
548
+ }
549
+
550
+ private static GameObject? ResolveRequiredGameObject(
551
+ AuthoringInvocation invocation,
552
+ UnityPilotInspection result,
553
+ string argumentName,
554
+ string kind)
555
+ {
556
+ if (!TryGetArgument(invocation.Arguments, argumentName, out var element)
557
+ || element.ValueKind != JsonValueKind.Object)
558
+ {
559
+ result.Errors.Add("target_missing");
560
+ return null;
561
+ }
562
+
563
+ var reference = Deserialize<GameObjectRef>(element);
564
+ if (reference == null || !reference.IsValid(out _))
565
+ {
566
+ result.Errors.Add("target_invalid");
567
+ return null;
568
+ }
569
+
570
+ var go = GameObjectUtils.FindBy(reference, out _);
571
+ if (go == null)
572
+ {
573
+ result.Errors.Add("target_not_found");
574
+ return null;
575
+ }
576
+
577
+ result.Targets.Add(GameObjectTarget(go, kind));
578
+ return go;
579
+ }
580
+
581
+ private static UnityEngine.Component? FindComponent(GameObject go, ComponentRef reference)
582
+ {
583
+ var components = go.GetComponents<UnityEngine.Component>();
584
+ for (var index = 0; index < components.Length; index++)
585
+ {
586
+ var component = components[index];
587
+ if (component != null && reference.Matches(component, index))
588
+ return component;
589
+ }
590
+ return null;
591
+ }
592
+
593
+ private static AuthoringTargetSummary GameObjectTarget(GameObject go, string kind)
594
+ {
595
+ var components = go.GetComponents<UnityEngine.Component>();
596
+ var componentTypes = string.Join(",", components.Select(component => component == null ? "missing" : component.GetType().FullName));
597
+ var path = go.GetPath();
598
+ var scene = go.scene.IsValid() ? go.scene.path + "|" + go.scene.name : "no-scene";
599
+ return new AuthoringTargetSummary
600
+ {
601
+ Kind = kind,
602
+ Name = Bound(go.name),
603
+ RelativePath = SafeHierarchyPath(path),
604
+ Fingerprint = Hash(
605
+ "gameobject|" + StableId(go)
606
+ + "|" + path
607
+ + "|" + scene
608
+ + "|" + go.transform.childCount
609
+ + "|" + componentTypes
610
+ + "|" + go.activeSelf),
611
+ };
612
+ }
613
+
614
+ private static AuthoringTargetSummary ComponentTarget(UnityEngine.Component component)
615
+ {
616
+ var go = component.gameObject;
617
+ var index = Array.IndexOf(go.GetComponents<UnityEngine.Component>(), component);
618
+ return new AuthoringTargetSummary
619
+ {
620
+ Kind = "Component",
621
+ Name = Bound(component.GetType().FullName),
622
+ RelativePath = SafeHierarchyPath(go.GetPath()),
623
+ Fingerprint = Hash(
624
+ "component|" + StableId(component)
625
+ + "|" + component.GetType().AssemblyQualifiedName
626
+ + "|" + index
627
+ + "|" + StableId(go)),
628
+ };
629
+ }
630
+
631
+ /// <summary>Identity of the active scene or open prefab stage.</summary>
632
+ private static string ContainerFingerprint()
633
+ {
634
+ var stage = UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage();
635
+ if (stage != null)
636
+ return "prefab-stage|" + stage.assetPath;
637
+ var scene = EditorSceneManager.GetActiveScene();
638
+ return "scene|" + scene.path + "|" + scene.name;
639
+ }
640
+
641
+ private static string SafeSceneDisplay(UnityEngine.SceneManagement.Scene scene)
642
+ => scene.IsValid() ? (SafeProjectRelative(scene.path) ?? Bound(scene.name) ?? "untitled") : "none";
643
+
644
+ private static string StableId(UnityEngine.Object value)
645
+ {
646
+ #if UNITY_6000_5_OR_NEWER
647
+ return value.GetEntityId().ToString();
648
+ #else
649
+ return value.GetInstanceID().ToString(System.Globalization.CultureInfo.InvariantCulture);
650
+ #endif
651
+ }
652
+
653
+ private static string Hash(string material)
654
+ {
655
+ using var sha = System.Security.Cryptography.SHA256.Create();
656
+ var bytes = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(material));
657
+ var builder = new System.Text.StringBuilder(bytes.Length * 2);
658
+ foreach (var value in bytes)
659
+ builder.Append(value.ToString("x2", System.Globalization.CultureInfo.InvariantCulture));
660
+ return "sha256-" + builder;
661
+ }
662
+
663
+ private static T? Deserialize<T>(JsonElement element) where T : class
664
+ {
665
+ try
666
+ {
667
+ var reflector = UnityCopilotPluginEditor.HasInstance
668
+ ? UnityCopilotPluginEditor.Instance.Reflector
669
+ : null;
670
+ if (reflector != null)
671
+ return reflector.JsonSerializer.Deserialize<T>(reflector, element);
672
+ return System.Text.Json.JsonSerializer.Deserialize<T>(element.GetRawText());
673
+ }
674
+ catch
675
+ {
676
+ return null;
677
+ }
678
+ }
679
+
680
+ private static bool TryGetArgument(
681
+ IReadOnlyDictionary<string, JsonElement> arguments,
682
+ string name,
683
+ out JsonElement value)
684
+ {
685
+ if (arguments.TryGetValue(name, out value))
686
+ return true;
687
+ foreach (var pair in arguments)
688
+ {
689
+ if (string.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase))
690
+ {
691
+ value = pair.Value;
692
+ return true;
693
+ }
694
+ }
695
+ value = default;
696
+ return false;
697
+ }
698
+
699
+ private static bool TryGetString(
700
+ IReadOnlyDictionary<string, JsonElement> arguments,
701
+ string name,
702
+ out string? value)
703
+ {
704
+ value = null;
705
+ if (TryGetArgument(arguments, name, out var element) && element.ValueKind == JsonValueKind.String)
706
+ {
707
+ value = element.GetString();
708
+ return true;
709
+ }
710
+ return false;
711
+ }
712
+
713
+ private static bool LooksLikeTarget(string name)
714
+ => name.EndsWith("Ref", StringComparison.Ordinal)
715
+ || name.EndsWith("Refs", StringComparison.Ordinal)
716
+ || name.IndexOf("component", StringComparison.OrdinalIgnoreCase) >= 0;
717
+
718
+ private static string? DisplayValue(JsonElement value)
719
+ {
720
+ if (value.ValueKind == JsonValueKind.String)
721
+ return Bound(value.GetString());
722
+ if (value.ValueKind == JsonValueKind.Object)
723
+ {
724
+ foreach (var name in new[] { "path", "name", "assetPath", "assetGuid", "instanceID", "entityId" })
725
+ {
726
+ foreach (var property in value.EnumerateObject())
727
+ {
728
+ if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
729
+ return DisplayValue(property.Value);
730
+ }
731
+ }
732
+ }
733
+ return null;
734
+ }
735
+
736
+ private static string? SafeHierarchyPath(string? path)
737
+ {
738
+ if (string.IsNullOrEmpty(path)) return null;
739
+ var trimmed = path!.Replace('\\', '/').TrimStart('/');
740
+ return trimmed.Length == 0 ? null : Bound(trimmed);
741
+ }
742
+
743
+ public static string? SafeProjectRelative(string? path)
744
+ {
745
+ if (string.IsNullOrWhiteSpace(path)) return null;
746
+ var dataPath = Application.dataPath.Replace('\\', '/');
747
+ var normalized = path!.Replace('\\', '/');
748
+ if (normalized.StartsWith(dataPath + "/", StringComparison.OrdinalIgnoreCase))
749
+ return "Assets/" + normalized.Substring(dataPath.Length + 1);
750
+ if (normalized.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
751
+ return normalized;
752
+ return null;
753
+ }
754
+
755
+ private static string? Bound(string? value)
756
+ => value == null ? null : value.Length <= 160 ? value : value.Substring(0, 160);
757
+ }
758
+
759
+ /// <summary>
760
+ /// Batch-level validator/planner. It inspects every child registration
761
+ /// through the existing tool manager, but never invokes a child runner.
762
+ /// The parent batch therefore cannot obtain a plan for an opaque or
763
+ /// unsupported child and then execute it as if it were safe.
764
+ /// </summary>
765
+ public sealed class UnityBatchAuthoringValidator : IAuthoringValidator
766
+ {
767
+ public AuthoringValidationResult Validate(AuthoringInvocation invocation)
768
+ {
769
+ if (invocation == null)
770
+ throw new ArgumentNullException(nameof(invocation));
771
+
772
+ var errors = UnityBatchAuthoringPlanning.Validate(invocation);
773
+ return new AuthoringValidationResult
774
+ {
775
+ Valid = errors.Count == 0,
776
+ FailureCode = errors.Count == 0 ? null : errors[0],
777
+ Details = errors.Count == 0
778
+ ? null
779
+ : new System.Text.Json.Nodes.JsonObject { ["reason"] = errors[0] },
780
+ };
781
+ }
782
+ }
783
+
784
+ public sealed class UnityBatchAuthoringPlanner : IAuthoringPlanner
785
+ {
786
+ public AuthoringPlanSummary Plan(AuthoringInvocation invocation)
787
+ {
788
+ if (invocation == null)
789
+ throw new ArgumentNullException(nameof(invocation));
790
+ return UnityBatchAuthoringPlanning.Plan(invocation);
791
+ }
792
+ }
793
+
794
+ internal static class UnityBatchAuthoringPlanning
795
+ {
796
+ public static List<string> Validate(AuthoringInvocation invocation)
797
+ {
798
+ var errors = new List<string>();
799
+ if (!TryReadCommands(invocation, out var commands, out var error))
800
+ {
801
+ errors.Add(error ?? "validation_failed");
802
+ return errors;
803
+ }
804
+
805
+ if (commands.Count > Tool_Batch.HardMaxCommands)
806
+ {
807
+ errors.Add("validation_failed");
808
+ return errors;
809
+ }
810
+
811
+ for (var index = 0; index < commands.Count; index++)
812
+ {
813
+ var command = commands[index];
814
+ var runner = FindRunner(command.Tool);
815
+ if (runner == null)
816
+ {
817
+ errors.Add("safety_unsupported");
818
+ continue;
819
+ }
820
+
821
+ if (string.Equals(command.Tool, Tool_Batch.BatchExecuteToolId, StringComparison.Ordinal))
822
+ {
823
+ errors.Add("safety_unsupported");
824
+ continue;
825
+ }
826
+
827
+ _ = AuthoringSafetyPolicy.Classify(runner);
828
+ }
829
+
830
+ return errors;
831
+ }
832
+
833
+ public static AuthoringPlanSummary Plan(AuthoringInvocation invocation)
834
+ {
835
+ if (!TryReadCommands(invocation, out var commands, out var error))
836
+ throw new ToolCallControlException(
837
+ ToolCallErrorCodes.InvalidControl,
838
+ error ?? "Batch commands are invalid.");
839
+
840
+ if (commands.Count > Tool_Batch.HardMaxCommands)
841
+ throw new ToolCallControlException(
842
+ ToolCallErrorCodes.InvalidControl,
843
+ "The batch contains too many commands.",
844
+ details: new System.Text.Json.Nodes.JsonObject
845
+ {
846
+ ["reason"] = "max_commands",
847
+ });
848
+
849
+ var targets = new List<AuthoringTargetSummary>();
850
+ var effects = new List<string>();
851
+ var childRecords = new List<AuthoringChildPlanSummary>();
852
+ var aggregateUndo = AuthoringUndoLevel.Full;
853
+ var supportsSharedTransaction = true;
854
+ var hasMutatingChild = false;
855
+ var policy = AuthoringSafetyPolicyContext.Current;
856
+ if (policy == null)
857
+ {
858
+ throw new ToolCallControlException(
859
+ ToolCallErrorCodes.SafetyUnsupported,
860
+ "Batch planning requires the live authoring safety policy.",
861
+ details: new System.Text.Json.Nodes.JsonObject { ["reason"] = "policy_scope_missing" });
862
+ }
863
+
864
+ var existingChildren = invocation.PlanBeingVerified?.ChildRecords;
865
+ if (existingChildren != null && existingChildren.Count != commands.Count)
866
+ {
867
+ throw new ToolCallControlException(
868
+ ToolCallErrorCodes.ConfirmationStale,
869
+ "The batch child confirmation records no longer match the command list.");
870
+ }
871
+
872
+ for (var index = 0; index < commands.Count; index++)
873
+ {
874
+ var command = commands[index];
875
+ var runner = FindRunner(command.Tool);
876
+ if (runner == null
877
+ || string.Equals(command.Tool, Tool_Batch.BatchExecuteToolId, StringComparison.Ordinal))
878
+ {
879
+ throw new ToolCallControlException(
880
+ ToolCallErrorCodes.SafetyUnsupported,
881
+ "A batch child does not expose a supported authoring capability.",
882
+ details: new System.Text.Json.Nodes.JsonObject { ["reason"] = "child_capability_missing" });
883
+ }
884
+
885
+ var childContext = ToolCallContextNormalizer.DeriveChild(
886
+ invocation.Context,
887
+ new ToolCallControl
888
+ {
889
+ CallId = "batch-plan-" + index.ToString(System.Globalization.CultureInfo.InvariantCulture),
890
+ },
891
+ requestId: "batch-plan-request-" + index.ToString(System.Globalization.CultureInfo.InvariantCulture),
892
+ cancellationToken: invocation.Context.CancellationToken);
893
+ var childInvocation = policy.PrepareInvocation(new AuthoringInvocation(
894
+ childContext,
895
+ runner.Name,
896
+ command.Parameters,
897
+ runner));
898
+ var descriptor = childInvocation.Descriptor;
899
+ var risk = AuthoringSafetyPolicy.Classify(
900
+ childInvocation.Runner.ReadOnlyHint,
901
+ childInvocation.Runner.DestructiveHint,
902
+ descriptor);
903
+ if (descriptor != null
904
+ && ((descriptor.SupportsPlanning && descriptor.Planner == null)
905
+ || (descriptor.SupportsValidation && descriptor.Validator == null
906
+ && descriptor.Planner == null)))
907
+ {
908
+ throw new ToolCallControlException(
909
+ ToolCallErrorCodes.SafetyUnsupported,
910
+ "A batch child has inconsistent authoring capability data.",
911
+ details: new System.Text.Json.Nodes.JsonObject
912
+ {
913
+ ["reason"] = "binding_unavailable",
914
+ ["stage"] = "declared_adapter_unavailable",
915
+ });
916
+ }
917
+
918
+ AuthoringValidationResult? childValidation = null;
919
+ if (descriptor?.Validator != null)
920
+ {
921
+ childValidation = descriptor.Validator.Validate(childInvocation);
922
+ if (childValidation == null || !childValidation.Valid)
923
+ {
924
+ var failureCode = childValidation?.FailureCode;
925
+ var safeFailureCode = string.IsNullOrWhiteSpace(failureCode)
926
+ ? "validation_failed"
927
+ : failureCode ?? "validation_failed";
928
+ throw new ToolCallControlException(
929
+ safeFailureCode,
930
+ "A batch child did not pass validation.");
931
+ }
932
+ }
933
+
934
+ var childPlan = descriptor?.Planner?.Plan(childInvocation);
935
+ if (descriptor?.Planner != null && childPlan == null)
936
+ {
937
+ throw new ToolCallControlException(
938
+ ToolCallErrorCodes.SafetyUnsupported,
939
+ "A batch child planner returned no plan.",
940
+ details: new System.Text.Json.Nodes.JsonObject
941
+ {
942
+ ["reason"] = "binding_unavailable",
943
+ ["stage"] = "planner_returned_null",
944
+ });
945
+ }
946
+
947
+ var validationTargets = childValidation?.Targets == null
948
+ ? Array.Empty<AuthoringTargetSummary>()
949
+ : childValidation.Targets.Where(target => target != null).Take(8)
950
+ .Select(target => target.CloneSafe()).ToArray();
951
+ var plannedTargets = childPlan?.Targets == null
952
+ ? Array.Empty<AuthoringTargetSummary>()
953
+ : childPlan.Targets.Where(target => target != null).Take(8)
954
+ .Select(target => target.CloneSafe()).ToArray();
955
+ IReadOnlyList<AuthoringTargetSummary> childTargets = plannedTargets.Length == 0
956
+ ? validationTargets
957
+ : plannedTargets;
958
+ var plannedEffects = childPlan?.PredictedEffects == null
959
+ ? Array.Empty<string>()
960
+ : childPlan.PredictedEffects.Where(effect => !string.IsNullOrWhiteSpace(effect)).Take(8)
961
+ .Select(effect => effect.Length <= 160 ? effect : effect.Substring(0, 160)).ToArray();
962
+ IReadOnlyList<string> childEffects = plannedEffects.Length > 0
963
+ ? plannedEffects
964
+ : new[]
965
+ {
966
+ descriptor == null || descriptor.IsOpaque
967
+ ? "undeclared"
968
+ : descriptor.MutationKind == AuthoringMutationKind.Unknown
969
+ ? "authoring change"
970
+ : descriptor.MutationKind.ToString().ToLowerInvariant(),
971
+ };
972
+
973
+ AuthoringChildPlanSummary? existingAuthority = null;
974
+ if (existingChildren != null)
975
+ {
976
+ existingAuthority = existingChildren[index];
977
+ if (existingAuthority == null
978
+ || existingAuthority.Index != index
979
+ || !string.Equals(existingAuthority.ToolName, runner.Name, StringComparison.Ordinal))
980
+ {
981
+ throw new ToolCallControlException(
982
+ ToolCallErrorCodes.ConfirmationStale,
983
+ "The batch child confirmation records no longer match the command list.");
984
+ }
985
+ }
986
+
987
+ var childRecord = policy.CreateAggregateChildRecord(
988
+ index,
989
+ childInvocation,
990
+ childValidation,
991
+ childPlan,
992
+ childTargets,
993
+ childEffects,
994
+ existingAuthority);
995
+ childRecords.Add(childRecord);
996
+
997
+ if (risk != AuthoringRiskLevel.Read)
998
+ {
999
+ hasMutatingChild = true;
1000
+ aggregateUndo = (AuthoringUndoLevel)Math.Min(
1001
+ (int)aggregateUndo,
1002
+ (int)childRecord.UndoLevel);
1003
+ supportsSharedTransaction &= childRecord.SupportsSharedTransaction;
1004
+ }
1005
+
1006
+ targets.Add(new AuthoringTargetSummary
1007
+ {
1008
+ Kind = "batch-child",
1009
+ Name = runner.Name,
1010
+ Fingerprint = AuthoringConfirmationBinding.ComputeArgumentsHash(
1011
+ new Dictionary<string, JsonElement>
1012
+ {
1013
+ ["index"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.Index),
1014
+ ["tool"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.ToolName),
1015
+ ["argumentsHash"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.ArgumentsHash),
1016
+ ["risk"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.Risk),
1017
+ ["undo"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.Undo),
1018
+ ["planLevel"] = System.Text.Json.JsonSerializer.SerializeToElement(childRecord.PlanLevel),
1019
+ }),
1020
+ });
1021
+ targets.AddRange(childRecord.Targets.Select(target => target.CloneSafe()));
1022
+ effects.AddRange(childRecord.PredictedEffects);
1023
+ }
1024
+
1025
+ if (effects.Count == 0)
1026
+ effects.Add("authoring change");
1027
+
1028
+ return new AuthoringPlanSummary
1029
+ {
1030
+ UndoLevel = aggregateUndo,
1031
+ HasUndoOverride = true,
1032
+ SupportsSharedTransaction = hasMutatingChild && supportsSharedTransaction,
1033
+ Targets = targets.Take(32).Select(target => target.CloneSafe()).ToArray(),
1034
+ PredictedEffects = effects.Take(32).ToArray(),
1035
+ ChildRecords = childRecords.Take(Tool_Batch.HardMaxCommands)
1036
+ .Select(child => child.CloneBounded()).ToArray(),
1037
+ BeforeSummary = new System.Text.Json.Nodes.JsonObject
1038
+ {
1039
+ ["childCount"] = commands.Count,
1040
+ },
1041
+ AfterSummary = new System.Text.Json.Nodes.JsonObject
1042
+ {
1043
+ ["childCount"] = commands.Count,
1044
+ ["predictedEffects"] = effects.Take(32).Aggregate(
1045
+ new System.Text.Json.Nodes.JsonArray(),
1046
+ (array, effect) =>
1047
+ {
1048
+ array.Add(effect);
1049
+ return array;
1050
+ }),
1051
+ },
1052
+ };
1053
+ }
1054
+
1055
+ private sealed class ParsedCommand
1056
+ {
1057
+ public string Tool { get; }
1058
+ public Dictionary<string, JsonElement> Parameters { get; }
1059
+
1060
+ public ParsedCommand(string tool, Dictionary<string, JsonElement> parameters)
1061
+ {
1062
+ Tool = tool;
1063
+ Parameters = parameters;
1064
+ }
1065
+ }
1066
+
1067
+ private static bool TryReadCommands(
1068
+ AuthoringInvocation invocation,
1069
+ out List<ParsedCommand> commands,
1070
+ out string? error)
1071
+ {
1072
+ commands = new List<ParsedCommand>();
1073
+ error = null;
1074
+ if (!TryGetArgument(invocation.Arguments, "commands", out var value)
1075
+ || value.ValueKind != JsonValueKind.Array)
1076
+ {
1077
+ error = "validation_failed";
1078
+ return false;
1079
+ }
1080
+
1081
+ foreach (var item in value.EnumerateArray())
1082
+ {
1083
+ if (item.ValueKind != JsonValueKind.Object)
1084
+ {
1085
+ error = "validation_failed";
1086
+ return false;
1087
+ }
1088
+ if (!TryGetProperty(item, "tool", out var toolValue)
1089
+ || toolValue.ValueKind != JsonValueKind.String
1090
+ || string.IsNullOrWhiteSpace(toolValue.GetString()))
1091
+ {
1092
+ error = "validation_failed";
1093
+ return false;
1094
+ }
1095
+
1096
+ var parameters = new Dictionary<string, JsonElement>(StringComparer.Ordinal);
1097
+ if (TryGetProperty(item, "params", out var paramsValue)
1098
+ && paramsValue.ValueKind != JsonValueKind.Null)
1099
+ {
1100
+ if (paramsValue.ValueKind != JsonValueKind.Object)
1101
+ {
1102
+ error = "validation_failed";
1103
+ return false;
1104
+ }
1105
+ foreach (var property in paramsValue.EnumerateObject())
1106
+ parameters[property.Name] = property.Value.Clone();
1107
+ }
1108
+
1109
+ commands.Add(new ParsedCommand(toolValue.GetString()!, parameters));
1110
+ }
1111
+
1112
+ if (commands.Count == 0)
1113
+ {
1114
+ error = "validation_failed";
1115
+ return false;
1116
+ }
1117
+ return true;
1118
+ }
1119
+
1120
+ private static IRunTool? FindRunner(string name)
1121
+ {
1122
+ if (!UnityCopilotPluginEditor.HasInstance)
1123
+ return null;
1124
+ var manager = UnityCopilotPluginEditor.Instance.Tools;
1125
+ if (manager == null)
1126
+ return null;
1127
+ return manager.GetAllTools()
1128
+ .FirstOrDefault(item => item != null && string.Equals(item.Name, name, StringComparison.Ordinal));
1129
+ }
1130
+
1131
+ private static bool TryGetArgument(
1132
+ IReadOnlyDictionary<string, JsonElement> arguments,
1133
+ string name,
1134
+ out JsonElement value)
1135
+ {
1136
+ if (arguments.TryGetValue(name, out value)) return true;
1137
+ foreach (var pair in arguments)
1138
+ {
1139
+ if (string.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase))
1140
+ {
1141
+ value = pair.Value;
1142
+ return true;
1143
+ }
1144
+ }
1145
+ value = default;
1146
+ return false;
1147
+ }
1148
+
1149
+ private static bool TryGetProperty(JsonElement element, string name, out JsonElement value)
1150
+ {
1151
+ foreach (var property in element.EnumerateObject())
1152
+ {
1153
+ if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
1154
+ {
1155
+ value = property.Value;
1156
+ return true;
1157
+ }
1158
+ }
1159
+ value = default;
1160
+ return false;
1161
+ }
1162
+ }
1163
+
1164
+ /// <summary>
1165
+ /// Read-only planner for the pilot. It performs the same live target
1166
+ /// inspection as the validator and adds predicted effects plus bounded
1167
+ /// before/after summaries. A plan for an invalid call is refused rather
1168
+ /// than issued, so a confirmation token can only ever describe an action
1169
+ /// whose targets were actually observed.
1170
+ /// </summary>
1171
+ public sealed class UnityPilotAuthoringPlanner : IAuthoringPlanner
1172
+ {
1173
+ public AuthoringPlanSummary Plan(AuthoringInvocation invocation)
1174
+ {
1175
+ if (invocation == null)
1176
+ throw new ArgumentNullException(nameof(invocation));
1177
+
1178
+ var inspection = UnityPilotAuthoringInspection.Inspect(invocation);
1179
+ if (inspection.Errors.Count > 0)
1180
+ {
1181
+ throw new ToolCallControlException(
1182
+ "validation_failed",
1183
+ "The authoring call did not pass validation.",
1184
+ details: new System.Text.Json.Nodes.JsonObject
1185
+ {
1186
+ ["reason"] = inspection.Errors[0],
1187
+ ["errorCount"] = inspection.Errors.Count,
1188
+ });
1189
+ }
1190
+
1191
+ var kind = invocation.Descriptor?.MutationKind ?? AuthoringMutationKind.Unknown;
1192
+ var effects = inspection.Effects.Count == 0
1193
+ ? new List<string> { kind == AuthoringMutationKind.Unknown ? "authoring change" : kind.ToString().ToLowerInvariant() }
1194
+ : inspection.Effects;
1195
+ return new AuthoringPlanSummary
1196
+ {
1197
+ Targets = inspection.Targets.Take(32).ToArray(),
1198
+ PredictedEffects = effects.Take(32).ToArray(),
1199
+ BeforeSummary = inspection.Before,
1200
+ AfterSummary = inspection.After,
1201
+ };
1202
+ }
1203
+ }
1204
+
1205
+ /// <summary>Unity Undo-backed transaction factory attached to pilot runners.</summary>
1206
+ public sealed class UnityAuthoringTransactionFactory : IAuthoringTransactionFactory
1207
+ {
1208
+ public IAuthoringTransaction Begin(AuthoringInvocation invocation)
1209
+ {
1210
+ if (invocation == null)
1211
+ throw new ArgumentNullException(nameof(invocation));
1212
+
1213
+ return BeginStandalone(
1214
+ "Uco: " + invocation.Name,
1215
+ invocation.EffectiveUndoLevel
1216
+ ?? invocation.Descriptor?.UndoLevel
1217
+ ?? AuthoringUndoLevel.Partial);
1218
+ }
1219
+
1220
+ /// <summary>
1221
+ /// Opens one named Unity Undo group and returns the transaction that
1222
+ /// owns it. Complete collapses the group to one user-visible step;
1223
+ /// Abort reverts only this group (never unrelated earlier history).
1224
+ /// </summary>
1225
+ public static IAuthoringTransaction BeginStandalone(string label, AuthoringUndoLevel advertisedUndo)
1226
+ {
1227
+ if (string.IsNullOrWhiteSpace(label))
1228
+ throw new ArgumentException("Transaction label must be non-empty.", nameof(label));
1229
+
1230
+ // The middleware may run on the transport thread; every Undo API
1231
+ // call is marshalled to the main thread (inline when already there).
1232
+ var group = MainThread.Instance.Run(() =>
1233
+ {
1234
+ Undo.IncrementCurrentGroup();
1235
+ var current = Undo.GetCurrentGroup();
1236
+ Undo.SetCurrentGroupName(label);
1237
+ return current;
1238
+ });
1239
+
1240
+ return new UnityAuthoringTransaction(
1241
+ label,
1242
+ advertisedUndo,
1243
+ group,
1244
+ recordCreated: value => MainThread.Instance.Run(() => Undo.RegisterCreatedObjectUndo(
1245
+ RequireObject(value), label)),
1246
+ recordModified: (value, completeSnapshot) => MainThread.Instance.Run(() =>
1247
+ {
1248
+ var target = RequireObject(value);
1249
+ if (completeSnapshot)
1250
+ Undo.RegisterCompleteObjectUndo(target, label);
1251
+ else
1252
+ Undo.RecordObject(target, label);
1253
+ }),
1254
+ recordDeleted: value => MainThread.Instance.Run(() => Undo.RegisterCompleteObjectUndo(
1255
+ RequireObject(value), label)),
1256
+ complete: () => MainThread.Instance.Run(() => Undo.CollapseUndoOperations(group)),
1257
+ abort: () => MainThread.Instance.Run(() => Undo.RevertAllDownToGroup(group)));
1258
+ }
1259
+
1260
+ /// <summary>
1261
+ /// Test/tooling helper: opens a standalone transaction, installs it as
1262
+ /// the ambient scope, and completes it on dispose. Production calls
1263
+ /// always receive their scope from <see cref="AuthoringSafetyMiddleware"/>.
1264
+ /// </summary>
1265
+ public static IDisposable BeginStandaloneScope(string label, AuthoringUndoLevel advertisedUndo = AuthoringUndoLevel.Full)
1266
+ {
1267
+ var transaction = BeginStandalone(label, advertisedUndo);
1268
+ var scope = AuthoringTransactionScope.Push(transaction);
1269
+ return new StandaloneScope(transaction, scope);
1270
+ }
1271
+
1272
+ private sealed class StandaloneScope : IDisposable
1273
+ {
1274
+ private readonly IAuthoringTransaction _transaction;
1275
+ private readonly IDisposable _scope;
1276
+ private int _disposed;
1277
+
1278
+ public StandaloneScope(IAuthoringTransaction transaction, IDisposable scope)
1279
+ {
1280
+ _transaction = transaction;
1281
+ _scope = scope;
1282
+ }
1283
+
1284
+ public void Dispose()
1285
+ {
1286
+ if (System.Threading.Interlocked.Exchange(ref _disposed, 1) != 0)
1287
+ return;
1288
+ try
1289
+ {
1290
+ _transaction.Complete();
1291
+ }
1292
+ finally
1293
+ {
1294
+ _scope.Dispose();
1295
+ _transaction.Dispose();
1296
+ }
1297
+ }
1298
+ }
1299
+
1300
+ private static UnityEngine.Object RequireObject(object value)
1301
+ => value as UnityEngine.Object
1302
+ ?? throw new InvalidOperationException("The authoring target is not a Unity object.");
1303
+ }
1304
+
1305
+ /// <summary>
1306
+ /// Unity-aware transaction: affected-object summaries carry the object's
1307
+ /// type and name instead of the generic <c>ToString()</c> fallback, and
1308
+ /// hierarchy paths are reported as safe relative display values.
1309
+ /// </summary>
1310
+ public sealed class UnityAuthoringTransaction : AuthoringTransaction
1311
+ {
1312
+ public UnityAuthoringTransaction(
1313
+ string groupLabel,
1314
+ AuthoringUndoLevel advertisedUndo,
1315
+ int groupId,
1316
+ Action<object>? recordCreated,
1317
+ Action<object, bool>? recordModified,
1318
+ Action<object>? recordDeleted,
1319
+ Action? complete,
1320
+ Action? abort)
1321
+ : base(groupLabel, advertisedUndo, groupId, recordCreated, recordModified, recordDeleted, complete, abort)
1322
+ {
1323
+ }
1324
+
1325
+ private readonly HashSet<string> _reported = new HashSet<string>(StringComparer.Ordinal);
1326
+
1327
+ protected override void AddAffected(object value)
1328
+ {
1329
+ if (value is UnityEngine.Object unityObject && unityObject != null)
1330
+ {
1331
+ // One object may be recorded more than once inside a call
1332
+ // (for example RegisterCreatedObjectUndo followed by a
1333
+ // complete snapshot); report it once.
1334
+ #if UNITY_6000_5_OR_NEWER
1335
+ var objectId = unityObject.GetEntityId().ToString();
1336
+ #else
1337
+ var objectId = unityObject.GetInstanceID().ToString(System.Globalization.CultureInfo.InvariantCulture);
1338
+ #endif
1339
+ var path = value is GameObject go
1340
+ ? go.GetPath()
1341
+ : value is UnityEngine.Component component && component != null
1342
+ ? component.gameObject.GetPath()
1343
+ : null;
1344
+ var summary = new AuthoringAffectedObjectSummary
1345
+ {
1346
+ Kind = value.GetType().Name,
1347
+ Name = unityObject.name,
1348
+ RelativePath = string.IsNullOrEmpty(path) ? null : path!.Replace('\\', '/').TrimStart('/'),
1349
+ };
1350
+ if (!_reported.Add(objectId))
1351
+ {
1352
+ AddAffectedEvent(summary);
1353
+ return;
1354
+ }
1355
+
1356
+ AddAffectedSummary(summary);
1357
+ return;
1358
+ }
1359
+
1360
+ base.AddAffected(value);
1361
+ }
1362
+ }
1363
+
1364
+ /// <summary>
1365
+ /// Small helper used by pilot tools. Every mutation records through the
1366
+ /// ambient transaction before changing Unity state.
1367
+ /// </summary>
1368
+ public static class UnityAuthoringUndo
1369
+ {
1370
+ /// <summary>
1371
+ /// Fails closed before the first mutation of a pilot tool body when
1372
+ /// the call did not arrive through the policy pipeline. An explicit
1373
+ /// ambient transaction (opened by the middleware, or deliberately by
1374
+ /// in-process tooling/tests) is accepted; a bare reflected runner
1375
+ /// call is not, so no object is created and then orphaned by a later
1376
+ /// missing-transaction failure.
1377
+ /// </summary>
1378
+ public static void RequireAuthoringScope()
1379
+ {
1380
+ if (AuthoringTransactionScope.Current != null)
1381
+ return;
1382
+ AuthoringPolicyApproval.Require();
1383
+ }
1384
+
1385
+ public static void RecordCreated(UnityEngine.Object value)
1386
+ {
1387
+ var transaction = AuthoringTransactionScope.Current;
1388
+ if (transaction == null)
1389
+ throw new InvalidOperationException("An authoring transaction is required.");
1390
+ transaction.RecordCreated(value);
1391
+ }
1392
+
1393
+ public static void RecordModified(UnityEngine.Object value, bool completeSnapshot = false)
1394
+ {
1395
+ var transaction = AuthoringTransactionScope.Current;
1396
+ if (transaction == null)
1397
+ throw new InvalidOperationException("An authoring transaction is required.");
1398
+ transaction.RecordModified(value, completeSnapshot);
1399
+ }
1400
+
1401
+ public static void Destroy(UnityEngine.Object value)
1402
+ {
1403
+ var transaction = AuthoringTransactionScope.Current;
1404
+ if (transaction == null)
1405
+ throw new InvalidOperationException("An authoring transaction is required.");
1406
+ transaction.RecordDeleted(value);
1407
+ Undo.DestroyObjectImmediate(value);
1408
+ transaction.MarkMutated();
1409
+ }
1410
+
1411
+ /// <summary>
1412
+ /// Undo-aware reparenting. <c>Undo.RecordObject</c> on a Transform
1413
+ /// does not capture hierarchy changes; Unity's
1414
+ /// <c>Undo.SetTransformParent</c> records the parent change inside
1415
+ /// the current group.
1416
+ /// </summary>
1417
+ public static void SetParent(Transform target, Transform? parent, bool worldPositionStays)
1418
+ {
1419
+ var transaction = AuthoringTransactionScope.Current;
1420
+ if (transaction == null)
1421
+ throw new InvalidOperationException("An authoring transaction is required.");
1422
+ if (target == null)
1423
+ throw new ArgumentNullException(nameof(target));
1424
+ transaction.RecordHostRegistered(target);
1425
+ Undo.SetTransformParent(target, parent, worldPositionStays, "Uco: set parent");
1426
+ transaction.MarkMutated();
1427
+ }
1428
+
1429
+ /// <summary>
1430
+ /// Undo-aware component creation. <c>Undo.AddComponent</c> creates
1431
+ /// and registers the component in one step; a second
1432
+ /// <c>RegisterCreatedObjectUndo</c> would duplicate the record.
1433
+ /// </summary>
1434
+ public static UnityEngine.Component? AddComponent(GameObject target, Type componentType)
1435
+ {
1436
+ var transaction = AuthoringTransactionScope.Current;
1437
+ if (transaction == null)
1438
+ throw new InvalidOperationException("An authoring transaction is required.");
1439
+ if (target == null)
1440
+ throw new ArgumentNullException(nameof(target));
1441
+ if (componentType == null)
1442
+ throw new ArgumentNullException(nameof(componentType));
1443
+
1444
+ var component = Undo.AddComponent(target, componentType);
1445
+ if (component == null)
1446
+ return null;
1447
+ transaction.RecordHostRegistered(component);
1448
+ transaction.MarkMutated();
1449
+ return component;
1450
+ }
1451
+
1452
+ /// <summary>
1453
+ /// Tracks an object that a Unity API already registered with Undo
1454
+ /// inside the current group (for example pasteboard duplication).
1455
+ /// </summary>
1456
+ public static void RecordHostRegistered(UnityEngine.Object value)
1457
+ {
1458
+ var transaction = AuthoringTransactionScope.Current;
1459
+ if (transaction == null)
1460
+ throw new InvalidOperationException("An authoring transaction is required.");
1461
+ transaction.RecordHostRegistered(value);
1462
+ }
1463
+
1464
+ public static void MarkMutated()
1465
+ => AuthoringTransactionScope.Current?.MarkMutated();
1466
+ }
1467
+ }