@chainingintention/pi-web-cn 1.202610.1 → 1.202610.3

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 (333) hide show
  1. package/README.md +60 -106
  2. package/dist/.plugins-ready +1 -0
  3. package/dist/cli.js +161 -45
  4. package/dist/cli.js.map +1 -1
  5. package/dist/client/assets/index-CFhaGb5m.js +4357 -0
  6. package/dist/client/assets/vendor-editor-core-YWog6mAR.js +10 -0
  7. package/dist/client/assets/vendor-editor-languages-DOCw3vc9.js +28 -0
  8. package/dist/client/index.html +5 -5
  9. package/dist/config.js +30 -6
  10. package/dist/config.js.map +1 -1
  11. package/dist/docker/piWebDockerCommandPlan.js +13 -4
  12. package/dist/docker/piWebDockerCommandPlan.js.map +1 -1
  13. package/dist/nativeServices/installedServiceDefinitions.js +23 -10
  14. package/dist/nativeServices/installedServiceDefinitions.js.map +1 -1
  15. package/dist/nativeServices/serviceAction.js +78 -20
  16. package/dist/nativeServices/serviceAction.js.map +1 -1
  17. package/dist/nativeServices/serviceDoctor.js +339 -67
  18. package/dist/nativeServices/serviceDoctor.js.map +1 -1
  19. package/dist/nativeServices/servicePlan.js +2 -2
  20. package/dist/nativeServices/servicePlan.js.map +1 -1
  21. package/dist/pi-packages/captains-log/README.md +11 -0
  22. package/dist/pi-packages/captains-log/dist/browser/channelProtocol.js +29 -0
  23. package/dist/pi-packages/captains-log/dist/browser/index.js +146 -0
  24. package/dist/pi-packages/captains-log/dist/browser/markdown.js +67 -0
  25. package/dist/pi-packages/captains-log/dist/browser/panel.js +151 -0
  26. package/dist/pi-packages/captains-log/dist/browser/protocol.js +26 -0
  27. package/dist/pi-packages/captains-log/dist/companion.js +126 -0
  28. package/dist/pi-packages/captains-log/dist/roundtrip.js +62 -0
  29. package/dist/pi-packages/captains-log/dist/server.js +210 -0
  30. package/dist/pi-packages/captains-log/dist/store.js +75 -0
  31. package/dist/pi-packages/captains-log/docs/usage.md +29 -0
  32. package/dist/pi-packages/captains-log/package.json +35 -0
  33. package/dist/pi-packages/captains-log/tsconfig.json +21 -0
  34. package/dist/pi-packages/captains-log/vite.config.mjs +28 -0
  35. package/dist/pi-packages/relays/pi-web-plugin.js +9 -9
  36. package/dist/pi-packages/relays/prompts/relay.md +1 -1
  37. package/dist/pi-packages/relays/relaysPanelElement.js +8 -8
  38. package/dist/pi-packages/relays/skills/relay-runner/SKILL.md +21 -13
  39. package/dist/pi-web-plugins/files/browser/assets/files-icon-DZObYhxb.svg +3 -0
  40. package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +456 -0
  41. package/dist/pi-web-plugins/files/package.json +15 -0
  42. package/dist/pi-web-plugins/git/browser/git-panel.js +55 -32
  43. package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +2 -2
  44. package/dist/pi-web-plugins/git/git-backend.js +19 -26
  45. package/dist/pi-web-plugins/git/gitStatusCache.js +48 -0
  46. package/dist/pi-web-plugins/git/package.json +1 -0
  47. package/dist/pi-web-plugins/git/server-plugin.js +11 -10
  48. package/dist/pi-web-plugins/info/infoInternals.js +4 -1
  49. package/dist/pi-web-plugins/info/package.json +1 -1
  50. package/dist/pi-web-plugins/info/pi-web-plugin.js +8 -8
  51. package/dist/pi-web-plugins/mermaid/browser/mermaid-engine.js +3590 -0
  52. package/dist/pi-web-plugins/mermaid/browser/pi-web-plugin.js +6 -0
  53. package/dist/pi-web-plugins/mermaid/package.json +13 -0
  54. package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +210 -0
  55. package/dist/pi-web-plugins/terminal/package.json +16 -0
  56. package/dist/pi-web-plugins/terminal/server-plugin.js +365 -0
  57. package/dist/pi-web-plugins/terminal/terminalService.js +435 -0
  58. package/dist/pi-web-plugins/updates/package.json +1 -1
  59. package/dist/pi-web-plugins/updates/pi-web-plugin.js +69 -68
  60. package/dist/pi-web-plugins/workspace-tasks/package.json +1 -1
  61. package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +3 -3
  62. package/dist/piWebVersionReport.js +47 -24
  63. package/dist/piWebVersionReport.js.map +1 -1
  64. package/dist/plugin-api.d.ts +260 -25
  65. package/dist/plugin-api.js +1 -0
  66. package/dist/pluginRecoveryCli.js +3 -2
  67. package/dist/pluginRecoveryCli.js.map +1 -1
  68. package/dist/server/activity/workspaceActivityService.js +5 -5
  69. package/dist/server/activity/workspaceActivityService.js.map +1 -1
  70. package/dist/server/app.js +71 -44
  71. package/dist/server/app.js.map +1 -1
  72. package/dist/server/audit/managementAuditStore.js +1 -1
  73. package/dist/server/browserMessageProjection.js +0 -2
  74. package/dist/server/browserMessageProjection.js.map +1 -1
  75. package/dist/server/configRoutes.js +1 -2
  76. package/dist/server/configRoutes.js.map +1 -1
  77. package/dist/server/knownPiPackages.js +12 -0
  78. package/dist/server/knownPiPackages.js.map +1 -0
  79. package/dist/server/machines/machineClient.js +2 -2
  80. package/dist/server/machines/machineClient.js.map +1 -1
  81. package/dist/server/machines/machinePluginProxyRoutes.js +120 -21
  82. package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
  83. package/dist/server/machines/machineProxyRoutes.js +80 -22
  84. package/dist/server/machines/machineProxyRoutes.js.map +1 -1
  85. package/dist/server/machines/machineService.js +22 -0
  86. package/dist/server/machines/machineService.js.map +1 -1
  87. package/dist/server/managementEmbed.js +1 -1
  88. package/dist/server/managementEmbed.js.map +1 -1
  89. package/dist/server/managementPrivileges.js +7 -1
  90. package/dist/server/managementPrivileges.js.map +1 -1
  91. package/dist/server/managementTerminalProxyRoutes.js +124 -0
  92. package/dist/server/managementTerminalProxyRoutes.js.map +1 -0
  93. package/dist/server/normalAuth.js +5 -1
  94. package/dist/server/normalAuth.js.map +1 -1
  95. package/dist/server/notices/serverNoticeStore.js +50 -6
  96. package/dist/server/notices/serverNoticeStore.js.map +1 -1
  97. package/dist/server/piPackageService.js +5 -5
  98. package/dist/server/piPackageService.js.map +1 -1
  99. package/dist/server/piWebPluginCatalog.js +211 -34
  100. package/dist/server/piWebPluginCatalog.js.map +1 -1
  101. package/dist/server/piWebPluginLifecycle.js +29 -3
  102. package/dist/server/piWebPluginLifecycle.js.map +1 -1
  103. package/dist/server/piWebPluginService.js +176 -291
  104. package/dist/server/piWebPluginService.js.map +1 -1
  105. package/dist/server/piWebStatus.js +45 -55
  106. package/dist/server/piWebStatus.js.map +1 -1
  107. package/dist/server/pluginCallbackDrain.js +35 -0
  108. package/dist/server/pluginCallbackDrain.js.map +1 -0
  109. package/dist/server/plugins/pluginBackendChannelProxyAdmission.js +84 -0
  110. package/dist/server/plugins/pluginBackendChannelProxyAdmission.js.map +1 -0
  111. package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js +234 -0
  112. package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js.map +1 -0
  113. package/dist/server/plugins/pluginBackendChannelProxyRoutes.js +63 -0
  114. package/dist/server/plugins/pluginBackendChannelProxyRoutes.js.map +1 -0
  115. package/dist/server/plugins/pluginBackendProxyRoutes.js +7 -4
  116. package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -1
  117. package/dist/server/plugins/pluginBackendRegistry.js +773 -0
  118. package/dist/server/plugins/pluginBackendRegistry.js.map +1 -0
  119. package/dist/server/plugins/serverPluginPiSessionEventsCapability.js +34 -0
  120. package/dist/server/plugins/serverPluginPiSessionEventsCapability.js.map +1 -0
  121. package/dist/server/plugins/serverPluginPiSessionsCapability.js +185 -0
  122. package/dist/server/plugins/serverPluginPiSessionsCapability.js.map +1 -0
  123. package/dist/server/plugins/serverPluginRuntime.js +1049 -107
  124. package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
  125. package/dist/server/plugins/serverPluginWorkspacesCapability.js +114 -0
  126. package/dist/server/plugins/serverPluginWorkspacesCapability.js.map +1 -0
  127. package/dist/server/projectTrustRoutes.js +86 -0
  128. package/dist/server/projectTrustRoutes.js.map +1 -0
  129. package/dist/server/projects/projectLifecycleService.js +84 -0
  130. package/dist/server/projects/projectLifecycleService.js.map +1 -0
  131. package/dist/server/projects/projectService.js +2 -0
  132. package/dist/server/projects/projectService.js.map +1 -1
  133. package/dist/server/realtime/sessionEventHub.js +35 -41
  134. package/dist/server/realtime/sessionEventHub.js.map +1 -1
  135. package/dist/server/sessiond/agentHttpDispatcher.js +108 -0
  136. package/dist/server/sessiond/agentHttpDispatcher.js.map +1 -0
  137. package/dist/server/sessiond/pluginBackendChannelRoutes.js +397 -0
  138. package/dist/server/sessiond/pluginBackendChannelRoutes.js.map +1 -0
  139. package/dist/server/sessiond/pluginBackendRoutes.js +19 -9
  140. package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -1
  141. package/dist/server/sessiond/projectMutationRoutes.js +38 -0
  142. package/dist/server/sessiond/projectMutationRoutes.js.map +1 -0
  143. package/dist/server/sessiond/sessionDaemonShutdown.js +36 -0
  144. package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -0
  145. package/dist/server/sessiond/sessionDaemonStartup.js +30 -4
  146. package/dist/server/sessiond/sessionDaemonStartup.js.map +1 -1
  147. package/dist/server/sessiond/sessionProxyRoutes.js +47 -15
  148. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  149. package/dist/server/sessiond/sessionServiceDependencies.js +4 -0
  150. package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
  151. package/dist/server/sessiond/sessiondStateOwnership.js +174 -39
  152. package/dist/server/sessiond/sessiondStateOwnership.js.map +1 -1
  153. package/dist/server/sessiond/workspaceRemovalRoutes.js +1 -1
  154. package/dist/server/sessiond/workspaceRemovalRoutes.js.map +1 -1
  155. package/dist/server/sessiond.js +226 -56
  156. package/dist/server/sessiond.js.map +1 -1
  157. package/dist/server/sessions/askUserTool.js +0 -3
  158. package/dist/server/sessions/askUserTool.js.map +1 -1
  159. package/dist/server/sessions/attachmentService.js +0 -1
  160. package/dist/server/sessions/attachmentService.js.map +1 -1
  161. package/dist/server/sessions/authService.js +8 -1
  162. package/dist/server/sessions/authService.js.map +1 -1
  163. package/dist/server/sessions/builtinCommands.js +2 -2
  164. package/dist/server/sessions/builtinCommands.js.map +1 -1
  165. package/dist/server/sessions/clientSessionPreview.js +14 -0
  166. package/dist/server/sessions/clientSessionPreview.js.map +1 -0
  167. package/dist/server/sessions/dockerEnvironmentFacts.js +82 -9
  168. package/dist/server/sessions/dockerEnvironmentFacts.js.map +1 -1
  169. package/dist/server/sessions/managementAgentTools.js +11 -6
  170. package/dist/server/sessions/managementAgentTools.js.map +1 -1
  171. package/dist/server/sessions/managementPermissionSystem.js +16 -4
  172. package/dist/server/sessions/managementPermissionSystem.js.map +1 -1
  173. package/dist/server/sessions/messagePaging.js +2 -2
  174. package/dist/server/sessions/messagePaging.js.map +1 -1
  175. package/dist/server/sessions/modelCatalogRefresher.js +4 -3
  176. package/dist/server/sessions/modelCatalogRefresher.js.map +1 -1
  177. package/dist/server/sessions/oauthLoginFlowService.js +2 -5
  178. package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
  179. package/dist/server/sessions/pendingAskStore.js +0 -3
  180. package/dist/server/sessions/pendingAskStore.js.map +1 -1
  181. package/dist/server/sessions/piSessionEventConnections.js +65 -0
  182. package/dist/server/sessions/piSessionEventConnections.js.map +1 -0
  183. package/dist/server/sessions/piSessionManagerGateway.js +56 -3
  184. package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
  185. package/dist/server/sessions/piSessionService.js +774 -448
  186. package/dist/server/sessions/piSessionService.js.map +1 -1
  187. package/dist/server/sessions/projectAuthService.js +29 -2
  188. package/dist/server/sessions/projectAuthService.js.map +1 -1
  189. package/dist/server/sessions/projectSettingsScope.js +12 -4
  190. package/dist/server/sessions/projectSettingsScope.js.map +1 -1
  191. package/dist/server/sessions/projectTrust.js +119 -0
  192. package/dist/server/sessions/projectTrust.js.map +1 -0
  193. package/dist/server/sessions/sessionActivityMarker.js +105 -0
  194. package/dist/server/sessions/sessionActivityMarker.js.map +1 -0
  195. package/dist/server/sessions/sessionArchiveMapping.js +2 -8
  196. package/dist/server/sessions/sessionArchiveMapping.js.map +1 -1
  197. package/dist/server/sessions/sessionCommandService.js +12 -10
  198. package/dist/server/sessions/sessionCommandService.js.map +1 -1
  199. package/dist/server/sessions/sessionEnvironmentFacts.js +5 -4
  200. package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -1
  201. package/dist/server/sessions/sessionModelScope.js +42 -5
  202. package/dist/server/sessions/sessionModelScope.js.map +1 -1
  203. package/dist/server/sessions/sessionNameGenerator.js +6 -6
  204. package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
  205. package/dist/server/sessions/sessionRoutes.js +63 -15
  206. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  207. package/dist/server/sessions/sessionTreeProjection.js +29 -29
  208. package/dist/server/sessions/sessionTreeProjection.js.map +1 -1
  209. package/dist/server/sessions/sessionUiEvents.js +6 -7
  210. package/dist/server/sessions/sessionUiEvents.js.map +1 -1
  211. package/dist/server/sessions/sessionUnreadStore.js +26 -1
  212. package/dist/server/sessions/sessionUnreadStore.js.map +1 -1
  213. package/dist/server/sessions/spawnSessionTool.js +3 -3
  214. package/dist/server/sessions/spawnSessionTool.js.map +1 -1
  215. package/dist/server/sessions/spawnSubsessionTool.js +5 -5
  216. package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
  217. package/dist/server/sessions/spawnTargetResolver.js +1 -1
  218. package/dist/server/sessions/transcriptMessages.js +54 -0
  219. package/dist/server/sessions/transcriptMessages.js.map +1 -0
  220. package/dist/server/status/machineStatusService.js +3 -1
  221. package/dist/server/status/machineStatusService.js.map +1 -1
  222. package/dist/server/terminals/{terminalService.js → managementCommandRunService.js} +79 -107
  223. package/dist/server/terminals/managementCommandRunService.js.map +1 -0
  224. package/dist/server/terminals/managementTerminalRoutes.js +91 -0
  225. package/dist/server/terminals/managementTerminalRoutes.js.map +1 -0
  226. package/dist/server/terminals/requiredTerminalService.js +103 -0
  227. package/dist/server/terminals/requiredTerminalService.js.map +1 -0
  228. package/dist/server/webSocketBridge.js +339 -0
  229. package/dist/server/webSocketBridge.js.map +1 -1
  230. package/dist/server/workbench/gatewayIntegration.js +4 -1
  231. package/dist/server/workbench/gatewayIntegration.js.map +1 -1
  232. package/dist/server/workspaceExplorerRoutes.js +13 -12
  233. package/dist/server/workspaceExplorerRoutes.js.map +1 -1
  234. package/dist/server/workspaces/fileContentService.js +9 -7
  235. package/dist/server/workspaces/fileContentService.js.map +1 -1
  236. package/dist/server/workspaces/fileOperationService.js +4 -3
  237. package/dist/server/workspaces/fileOperationService.js.map +1 -1
  238. package/dist/server/workspaces/projectWorkspaceCwds.js +0 -5
  239. package/dist/server/workspaces/projectWorkspaceCwds.js.map +1 -1
  240. package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +36 -12
  241. package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -1
  242. package/dist/server/workspaces/workspaceCatalog.js +6 -4
  243. package/dist/server/workspaces/workspaceCatalog.js.map +1 -1
  244. package/dist/server/workspaces/workspaceDeletionRoutes.js +5 -57
  245. package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -1
  246. package/dist/server/workspaces/workspaceProviderRegistry.js +75 -24
  247. package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -1
  248. package/dist/server/workspaces/workspaceRemovalService.js +25 -3
  249. package/dist/server/workspaces/workspaceRemovalService.js.map +1 -1
  250. package/dist/server-plugin-api.d.ts +274 -44
  251. package/dist/server-plugin-api.js +319 -2
  252. package/dist/server-plugin-api.js.map +1 -1
  253. package/dist/serverPluginRecovery.js +4 -0
  254. package/dist/serverPluginRecovery.js.map +1 -1
  255. package/dist/sessiond/activeAgentProfile.js +27 -2
  256. package/dist/sessiond/activeAgentProfile.js.map +1 -1
  257. package/dist/sessiond/config.js +4 -3
  258. package/dist/sessiond/config.js.map +1 -1
  259. package/dist/sessiond/sessionDaemonClient.js +29 -8
  260. package/dist/sessiond/sessionDaemonClient.js.map +1 -1
  261. package/dist/shared/activeAgentProfile.js +2 -34
  262. package/dist/shared/activeAgentProfile.js.map +1 -1
  263. package/dist/shared/activity.js +0 -3
  264. package/dist/shared/activity.js.map +1 -1
  265. package/dist/shared/apiTypes.js +7 -2
  266. package/dist/shared/apiTypes.js.map +1 -1
  267. package/dist/shared/capabilities.js +5 -0
  268. package/dist/shared/capabilities.js.map +1 -1
  269. package/dist/shared/federatedRoutes.js +49 -9
  270. package/dist/shared/federatedRoutes.js.map +1 -1
  271. package/dist/shared/machinePluginIds.js +30 -7
  272. package/dist/shared/machinePluginIds.js.map +1 -1
  273. package/dist/shared/machineStatus.js +35 -1
  274. package/dist/shared/machineStatus.js.map +1 -1
  275. package/dist/shared/piWebStatusParsing.js +29 -0
  276. package/dist/shared/piWebStatusParsing.js.map +1 -1
  277. package/dist/shared/pluginApiTypes.d.ts +24 -9
  278. package/dist/shared/pluginApiTypes.js +2 -0
  279. package/dist/shared/pluginApiTypes.js.map +1 -0
  280. package/dist/shared/pluginBackendProtocol.js +163 -2
  281. package/dist/shared/pluginBackendProtocol.js.map +1 -1
  282. package/dist/shared/pluginIds.js +10 -1
  283. package/dist/shared/pluginIds.js.map +1 -1
  284. package/dist/shared/requiredTerminalPlugin.js +6 -0
  285. package/dist/shared/requiredTerminalPlugin.js.map +1 -0
  286. package/dist/shared/serverNoticeContract.js +88 -0
  287. package/dist/shared/serverNoticeContract.js.map +1 -0
  288. package/dist/shared/sessionDefaults.js +41 -0
  289. package/dist/shared/sessionDefaults.js.map +1 -0
  290. package/dist/shared/workspaceFiles.js.map +1 -1
  291. package/docs/config.md +270 -323
  292. package/docs/plugins.md +171 -956
  293. package/examples/session-bridge-plugin/README.md +20 -0
  294. package/examples/session-bridge-plugin/docs/usage.md +54 -0
  295. package/examples/session-bridge-plugin/package.json +26 -0
  296. package/examples/session-bridge-plugin/src/browser/index.ts +87 -0
  297. package/examples/session-bridge-plugin/src/browser/protocol.ts +24 -0
  298. package/examples/session-bridge-plugin/src/companion.ts +65 -0
  299. package/examples/session-bridge-plugin/src/reviewRun.ts +39 -0
  300. package/examples/session-bridge-plugin/src/server.ts +85 -0
  301. package/examples/session-bridge-plugin/src/store.ts +80 -0
  302. package/examples/session-bridge-plugin/tsconfig.json +21 -0
  303. package/examples/workspace-provider-plugin/README.md +39 -0
  304. package/examples/workspace-provider-plugin/package.json +25 -0
  305. package/examples/workspace-provider-plugin/src/browser/index.ts +69 -0
  306. package/examples/workspace-provider-plugin/src/server.ts +68 -0
  307. package/examples/workspace-provider-plugin/tsconfig.json +21 -0
  308. package/package.json +22 -13
  309. package/skills/README.md +27 -2
  310. package/skills/relay-runner/SKILL.md +21 -13
  311. package/dist/client/assets/CodeViewer-CZHnKRwQ.js +0 -4
  312. package/dist/client/assets/TerminalPanel-B_EaRPHY.js +0 -187
  313. package/dist/client/assets/index-Bv1v-Jz7.js +0 -4717
  314. package/dist/client/assets/vendor-editor-core-vUi74DnD.js +0 -12
  315. package/dist/client/assets/vendor-editor-languages-DoaXUodB.js +0 -46
  316. package/dist/client/assets/vendor-editor-legacy-CYBnW6ZU.js +0 -1
  317. package/dist/client/assets/vendor-terminal-BrP-ENHg.css +0 -1
  318. package/dist/client/assets/vendor-terminal-D8k4UKM2.js +0 -35
  319. package/dist/server/git/gitService.js +0 -316
  320. package/dist/server/git/gitService.js.map +0 -1
  321. package/dist/server/gitRoutes.js +0 -23
  322. package/dist/server/gitRoutes.js.map +0 -1
  323. package/dist/server/terminalProxyRoutes.js +0 -181
  324. package/dist/server/terminalProxyRoutes.js.map +0 -1
  325. package/dist/server/terminals/terminalRoutes.js +0 -164
  326. package/dist/server/terminals/terminalRoutes.js.map +0 -1
  327. package/dist/server/terminals/terminalService.js.map +0 -1
  328. package/dist/server/terminals/terminalSize.js +0 -17
  329. package/dist/server/terminals/terminalSize.js.map +0 -1
  330. package/dist/server/workspaces/gitWorktreeDiscovery.js +0 -43
  331. package/dist/server/workspaces/gitWorktreeDiscovery.js.map +0 -1
  332. package/dist/server/workspaces/workspaceService.js +0 -52
  333. package/dist/server/workspaces/workspaceService.js.map +0 -1
@@ -1,32 +1,56 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { isPiWebPluginId } from "../../shared/pluginIds.js";
1
4
  import { pathToFileURL } from "node:url";
5
+ import { parseServerNoticeScope, SERVER_PLUGIN_NOTICE_CONTEXT_MAX_BYTES, SERVER_PLUGIN_NOTICE_CONTEXT_MAX_DEPTH, SERVER_PLUGIN_NOTICE_MESSAGE_MAX_BYTES, SERVER_PLUGIN_NOTICE_SOURCE_PREFIX, serverNoticeStringExceedsUtf8ByteLimit, } from "../../shared/serverNoticeContract.js";
6
+ import { REQUIRED_TERMINAL_PLUGIN_ID, REQUIRED_TERMINAL_RECOVERY_GUIDANCE, } from "../../shared/requiredTerminalPlugin.js";
7
+ import { REQUIRED_TERMINAL_SERVICE_CAPABILITY } from "../terminals/requiredTerminalService.js";
2
8
  import { createServerPluginExecFile } from "./serverPluginExec.js";
3
9
  const DEFAULT_LIFECYCLE_TIMEOUT_MS = 10_000;
4
10
  /**
5
11
  * Resolves exactly one desired catalog snapshot and activates its server
6
12
  * entries. The resulting runtime is immutable except for explicit shutdown;
7
- * v1 intentionally has no hot reload or unload path.
13
+ * this lifecycle intentionally has no hot reload or unload path.
8
14
  */
9
15
  export async function createServerPluginRuntime(options) {
10
16
  if (options.safeStart === "none") {
11
17
  return await ServerPluginRuntime.activate({ plugins: [], diagnostics: [] }, options);
12
18
  }
13
19
  const snapshot = await options.catalog.snapshot(options.safeStart === "bundled-only" ? { scope: "bundled" } : undefined);
20
+ if (options.enforceRequiredTerminal !== false)
21
+ requireTerminalCatalogEntry(snapshot);
14
22
  return await ServerPluginRuntime.activate(snapshot, options);
15
23
  }
16
24
  export class ServerPluginRuntime {
17
- constructor(safeStart, diagnostics, logger, importer, execFile, lifecycleTimeoutMs) {
25
+ constructor(dataDir, safeStart, diagnostics, logger, importer, execFile, lifecycleTimeoutMs, noticeSink, hostCapabilities, hostCapabilityFactories, lateHostCapabilities, enforceRequiredTerminal) {
26
+ this.dataDir = dataDir;
18
27
  this.safeStart = safeStart;
19
28
  this.diagnostics = diagnostics;
20
29
  this.logger = logger;
21
30
  this.importer = importer;
22
31
  this.execFile = execFile;
23
32
  this.lifecycleTimeoutMs = lifecycleTimeoutMs;
33
+ this.noticeSink = noticeSink;
34
+ this.enforceRequiredTerminal = enforceRequiredTerminal;
24
35
  this.recordsById = new Map();
36
+ this.declaredCapabilitiesByKey = new Map();
37
+ this.activeCapabilitiesByKey = new Map();
25
38
  this.activePlugins = [];
39
+ this.stagedPlugins = [];
40
+ this.lifetimesAborted = false;
41
+ this.lateHostCapabilitiesResumed = false;
26
42
  this.stopped = false;
43
+ for (const provision of hostCapabilities) {
44
+ this.registerHostCapability(provision);
45
+ this.activeCapabilitiesByKey.set(provision.key, provision);
46
+ }
47
+ for (const factory of hostCapabilityFactories)
48
+ this.registerHostCapability(factory);
49
+ for (const capability of lateHostCapabilities)
50
+ this.registerHostCapability(capability);
27
51
  }
28
52
  static async activate(snapshot, options) {
29
- const runtime = new ServerPluginRuntime(options.safeStart, Object.freeze(snapshot.diagnostics.map((diagnostic) => Object.freeze({ ...diagnostic }))), options.logger, options.importer ?? importServerPluginModule, options.execFile ?? createServerPluginExecFile(), positiveInteger(options.lifecycleTimeoutMs, DEFAULT_LIFECYCLE_TIMEOUT_MS, "lifecycleTimeoutMs"));
53
+ const runtime = new ServerPluginRuntime(resolve(options.dataDir), options.safeStart, Object.freeze(snapshot.diagnostics.map((diagnostic) => Object.freeze({ ...diagnostic }))), options.logger, options.importer ?? importServerPluginModule, options.execFile ?? createServerPluginExecFile(), positiveInteger(options.lifecycleTimeoutMs, DEFAULT_LIFECYCLE_TIMEOUT_MS, "lifecycleTimeoutMs"), options.noticeSink, snapshotHostCapabilities(options.hostCapabilities), snapshotHostCapabilityFactories(options.hostCapabilityFactories), snapshotLateHostCapabilities(options.lateHostCapabilities), options.enforceRequiredTerminal !== false && options.safeStart !== "none");
30
54
  try {
31
55
  await runtime.start(snapshot.plugins);
32
56
  return runtime;
@@ -36,6 +60,12 @@ export class ServerPluginRuntime {
36
60
  throw error;
37
61
  }
38
62
  }
63
+ registerHostCapability(declaration) {
64
+ if (this.declaredCapabilitiesByKey.has(declaration.key)) {
65
+ throw new Error(`Host capability ${formatCapability(declaration.capability)} is configured more than once`);
66
+ }
67
+ this.declaredCapabilitiesByKey.set(declaration.key, declaration);
68
+ }
39
69
  safeStartLevel() {
40
70
  return this.safeStart;
41
71
  }
@@ -48,11 +78,67 @@ export class ServerPluginRuntime {
48
78
  .map((record) => Object.freeze({ ...record })));
49
79
  }
50
80
  providerContributions() {
51
- return Object.freeze(this.activePlugins.flatMap((active) => active.contribution === undefined ? [] : [active.contribution]));
81
+ return Object.freeze(this.activePlugins.flatMap((active) => active.providerContribution === undefined ? [] : [active.providerContribution]));
82
+ }
83
+ pairedBackendContributions() {
84
+ return Object.freeze(this.activePlugins.flatMap((active) => active.pairedBackendContribution === undefined ? [] : [active.pairedBackendContribution]));
52
85
  }
53
- async inspectHealth() {
86
+ /** Resolves one exact active capability for a core host consumer. */
87
+ resolve(capability) {
88
+ const parsed = snapshotTypedCapability(capability, "Host capability request");
89
+ const provision = this.activeCapabilitiesByKey.get(capabilityKey(parsed));
90
+ if (provision === undefined) {
91
+ throw new Error(`Server capability ${formatCapability(parsed)} is not active`);
92
+ }
93
+ return parseCapabilityValue(parsed, provision.value, `Server capability ${formatCapability(parsed)}`);
94
+ }
95
+ /**
96
+ * Registers every declared late host factory atomically, then resumes the
97
+ * plugins staged on those capabilities. This boundary is intentionally one-shot.
98
+ */
99
+ async resumeWithHostCapabilityFactories(factories) {
100
+ if (this.stopped || this.lifetimesAborted) {
101
+ throw new Error("Server plugin runtime cannot resume after shutdown begins");
102
+ }
103
+ if (this.lateHostCapabilitiesResumed) {
104
+ throw new Error("Server plugin late host capabilities have already resumed");
105
+ }
106
+ const snapshottedFactories = snapshotHostCapabilityFactories(factories);
107
+ const lateDeclarations = [...this.declaredCapabilitiesByKey.values()]
108
+ .filter((declaration) => declaration.source === "late-host");
109
+ const factoriesByKey = new Map(snapshottedFactories.map((factory) => [factory.key, factory]));
110
+ for (const factory of snapshottedFactories) {
111
+ if (!lateDeclarations.some(({ key }) => key === factory.key)) {
112
+ throw new Error(`Host capability ${formatCapability(factory.capability)} was not declared for late registration`);
113
+ }
114
+ }
115
+ for (const declaration of lateDeclarations) {
116
+ if (!factoriesByKey.has(declaration.key)) {
117
+ throw new Error(`Late host capability ${formatCapability(declaration.capability)} was not registered`);
118
+ }
119
+ }
120
+ this.lateHostCapabilitiesResumed = true;
121
+ for (const factory of snapshottedFactories)
122
+ this.declaredCapabilitiesByKey.set(factory.key, factory);
123
+ await this.startStagedPlugins(false);
124
+ }
125
+ /** Cancels plugin lifetimes while retaining publications for consumer cleanup. */
126
+ beginShutdown() {
127
+ if (this.lifetimesAborted)
128
+ return;
129
+ this.lifetimesAborted = true;
130
+ const reason = new DOMException("Server plugin host is shutting down", "AbortError");
131
+ for (const plugin of [...this.activePlugins, ...this.stagedPlugins]) {
132
+ if (!plugin.lifetimeController.signal.aborted)
133
+ plugin.lifetimeController.abort(reason);
134
+ }
135
+ }
136
+ async inspectHealth(pluginIds) {
137
+ const selectedPluginIds = pluginIds === undefined ? undefined : new Set(pluginIds);
54
138
  const inspections = [];
55
139
  for (const active of this.activePlugins) {
140
+ if (selectedPluginIds !== undefined && !selectedPluginIds.has(active.entry.id))
141
+ continue;
56
142
  const callback = active.activation.health?.bind(active.activation);
57
143
  if (callback === undefined) {
58
144
  inspections.push(Object.freeze({ pluginId: active.entry.id, health: Object.freeze({ status: "healthy" }) }));
@@ -75,39 +161,43 @@ export class ServerPluginRuntime {
75
161
  }
76
162
  return Object.freeze(inspections);
77
163
  }
78
- /** Stops every successfully published plugin in reverse activation order. */
164
+ /** Disposes published plugins in reverse dependency/start order. */
79
165
  async stop() {
80
166
  if (this.stopped)
81
167
  return;
168
+ this.beginShutdown();
82
169
  this.stopped = true;
170
+ // During failed construction, activated entries may not have reached start.
171
+ // Dispose those first while any started providers remain published.
172
+ const stagedPlugins = [...this.stagedPlugins].reverse();
173
+ this.stagedPlugins = [];
174
+ for (const staged of stagedPlugins) {
175
+ await this.disposeForShutdown(staged);
176
+ }
83
177
  const activePlugins = [...this.activePlugins].reverse();
84
178
  this.activePlugins = [];
179
+ this.activeCapabilitiesByKey.clear();
85
180
  for (const active of activePlugins) {
86
- const stop = active.activation.stop?.bind(active.activation);
87
- if (stop === undefined)
88
- continue;
89
- try {
90
- await runBounded(active.entry.id, "stop", this.lifecycleTimeoutMs, (signal) => stop(signal));
91
- }
92
- catch (error) {
93
- this.recordsById.set(active.entry.id, recordFor(active.entry, {
94
- state: "failed",
95
- name: active.plugin.name,
96
- phase: "stop",
97
- message: errorMessage(error),
98
- }));
99
- this.logger.error({ err: error, pluginId: active.entry.id, phase: "stop" }, "server plugin stop failed");
100
- }
181
+ await this.disposeForShutdown(active);
101
182
  }
102
183
  }
103
184
  async start(entries) {
104
185
  const serverEntries = entries
105
186
  .filter((entry) => entry.serverModule !== undefined)
106
- .sort((left, right) => left.id.localeCompare(right.id));
187
+ .sort(requiredTerminalFirst);
107
188
  for (const entry of serverEntries)
108
- await this.activateEntry(entry);
189
+ await this.stageEntry(entry);
190
+ await this.startStagedPlugins(true);
191
+ if (this.enforceRequiredTerminal) {
192
+ try {
193
+ this.resolve(REQUIRED_TERMINAL_SERVICE_CAPABILITY);
194
+ }
195
+ catch (error) {
196
+ throw requiredTerminalError("Required Terminal service capability is not active", error);
197
+ }
198
+ }
109
199
  }
110
- async activateEntry(entry) {
200
+ async stageEntry(entry) {
111
201
  const disabledMessage = disabledReason(entry, this.safeStart);
112
202
  if (disabledMessage !== undefined) {
113
203
  this.recordsById.set(entry.id, recordFor(entry, { state: "disabled", message: disabledMessage }));
@@ -116,7 +206,9 @@ export class ServerPluginRuntime {
116
206
  }
117
207
  let phase = "validate";
118
208
  let plugin;
119
- let activation;
209
+ let rollbackDispose;
210
+ let noticeReporter;
211
+ const lifetimeController = new AbortController();
120
212
  try {
121
213
  const settings = cloneJsonObject(entry.settings, `settings for server plugin ${entry.id}`);
122
214
  phase = "import";
@@ -126,51 +218,56 @@ export class ServerPluginRuntime {
126
218
  const loadedPlugin = parsePluginExport(imported);
127
219
  plugin = loadedPlugin;
128
220
  phase = "activate";
221
+ if (!isPiWebPluginId(entry.id))
222
+ throw new Error(`Invalid PI WEB plugin id: ${entry.id}`);
223
+ const dataDirectory = resolve(this.dataDir, "plugin-data", entry.id);
224
+ await mkdir(dataDirectory, { recursive: true });
129
225
  const scopedLogger = createScopedLogger(entry.id, this.logger);
226
+ noticeReporter = createScopedNoticeReporter(entry.id, this.noticeSink);
130
227
  const activationValue = await runBounded(entry.id, phase, this.lifecycleTimeoutMs, (signal) => loadedPlugin.activate(Object.freeze({
131
- apiVersion: 1,
228
+ apiVersion: 3,
132
229
  pluginId: entry.id,
133
230
  packageRoot: entry.packageRoot,
231
+ dataDirectory,
134
232
  logger: scopedLogger,
135
233
  settings,
234
+ ...(noticeReporter === undefined ? {} : { notices: noticeReporter.reporter }),
136
235
  execFile: this.execFile,
137
236
  signal,
237
+ lifetimeSignal: lifetimeController.signal,
138
238
  })));
239
+ rollbackDispose = activationDisposeForRollback(activationValue);
139
240
  phase = "validate";
140
- const loadedActivation = parseActivation(activationValue);
141
- activation = loadedActivation;
142
- phase = "start";
143
- const start = loadedActivation.start?.bind(loadedActivation);
144
- if (start !== undefined) {
145
- await runBounded(entry.id, phase, this.lifecycleTimeoutMs, (signal) => start(signal));
241
+ const loadedActivation = parseActivation(activationValue, entry.id);
242
+ if (this.enforceRequiredTerminal && entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
243
+ requireTerminalActivation(loadedActivation);
146
244
  }
147
- const contribution = loadedActivation.workspaceProvider === undefined
148
- ? undefined
149
- : Object.freeze({
150
- pluginId: entry.id,
151
- pluginName: loadedPlugin.name,
152
- packageRoot: entry.packageRoot,
153
- source: entry.source,
154
- scope: entry.scope,
155
- moduleRevision: requireServerModule(entry).revision,
156
- provider: loadedActivation.workspaceProvider,
157
- });
158
- this.activePlugins.push(Object.freeze({
245
+ const provisions = internalProvisions(loadedActivation, "plugin");
246
+ for (const provision of provisions) {
247
+ if (this.declaredCapabilitiesByKey.has(provision.key)) {
248
+ throw new IncompatibleServerPluginError(`Server capability ${formatCapability(provision.capability)} is provided more than once`);
249
+ }
250
+ }
251
+ const staged = Object.freeze({
159
252
  entry,
160
253
  plugin: loadedPlugin,
161
254
  activation: loadedActivation,
162
- ...(contribution === undefined ? {} : { contribution }),
163
- }));
164
- this.recordsById.set(entry.id, recordFor(entry, { state: "active", name: loadedPlugin.name }));
165
- this.logger.info({ pluginId: entry.id, pluginName: loadedPlugin.name }, "server plugin activated");
255
+ lifetimeController,
256
+ ...(noticeReporter === undefined ? {} : { noticeReporter }),
257
+ provisions,
258
+ hostCapabilityInstances: [],
259
+ });
260
+ this.stagedPlugins.push(staged);
261
+ for (const provision of provisions)
262
+ this.declaredCapabilitiesByKey.set(provision.key, provision);
166
263
  }
167
264
  catch (error) {
168
- const rollbackError = phase === "start" && activation?.stop !== undefined
169
- ? await this.rollbackStart(entry.id, activation)
170
- : undefined;
171
- const message = rollbackError === undefined
172
- ? errorMessage(error)
173
- : `${errorMessage(error)}; startup rollback failed: ${errorMessage(rollbackError)}`;
265
+ abortPluginLifetime(lifetimeController, entry.id, "activation failed");
266
+ noticeReporter?.revoke();
267
+ const rollbackError = rollbackDispose === undefined
268
+ ? undefined
269
+ : await this.runDispose(entry.id, rollbackDispose);
270
+ const message = withRollbackError(errorMessage(error), rollbackError);
174
271
  const state = error instanceof IncompatibleServerPluginError ? "incompatible" : "failed";
175
272
  this.recordsById.set(entry.id, recordFor(entry, {
176
273
  state,
@@ -179,20 +276,311 @@ export class ServerPluginRuntime {
179
276
  message,
180
277
  }));
181
278
  const details = { err: error, pluginId: entry.id, phase, ...(rollbackError === undefined ? {} : { rollbackError }) };
182
- if (state === "incompatible") {
279
+ if (state === "incompatible")
183
280
  this.logger.warn(details, "server plugin is incompatible");
281
+ else
282
+ this.logger.error(details, "server plugin activation failed");
283
+ if (this.enforceRequiredTerminal && entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
284
+ throw requiredTerminalError(`Required Terminal server entry failed during ${phase}: ${message}`, error);
285
+ }
286
+ }
287
+ }
288
+ async startStagedPlugins(allowLateHostWait) {
289
+ const pending = new Map(this.stagedPlugins.map((plugin) => [plugin.entry.id, plugin]));
290
+ while (pending.size > 0) {
291
+ let progressed = false;
292
+ const candidates = [...pending.values()].sort((left, right) => requiredTerminalFirst(left.entry, right.entry));
293
+ for (const staged of candidates) {
294
+ const dependencyFailure = this.dependencyFailure(staged, pending);
295
+ if (dependencyFailure !== undefined) {
296
+ pending.delete(staged.entry.id);
297
+ const message = await this.failBeforeStart(staged, dependencyFailure);
298
+ progressed = true;
299
+ if (this.enforceRequiredTerminal && staged.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
300
+ throw requiredTerminalError(`Required Terminal server entry failed during start: ${message}`, dependencyFailure);
301
+ }
302
+ continue;
303
+ }
304
+ if (this.waitingForDependencies(staged))
305
+ continue;
306
+ pending.delete(staged.entry.id);
307
+ const startError = await this.startStagedPlugin(staged);
308
+ progressed = true;
309
+ if (startError !== undefined && this.enforceRequiredTerminal && staged.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
310
+ const record = this.recordsById.get(staged.entry.id);
311
+ throw requiredTerminalError(`Required Terminal server entry failed during start: ${record?.message ?? errorMessage(startError)}`, startError);
312
+ }
313
+ }
314
+ if (progressed)
315
+ continue;
316
+ const cycleIds = findCapabilityCycle(pending, this.declaredCapabilitiesByKey);
317
+ if (cycleIds.length > 0) {
318
+ for (const pluginId of cycleIds) {
319
+ const staged = pending.get(pluginId);
320
+ if (staged === undefined)
321
+ continue;
322
+ pending.delete(pluginId);
323
+ const cyclicRequirement = (staged.plugin.requires ?? []).find((requirement) => cycleIds.includes(requirement.pluginId));
324
+ const error = new Error(cyclicRequirement === undefined
325
+ ? `Server plugin ${pluginId} has an unresolved capability dependency cycle`
326
+ : `Server plugin ${pluginId} requires ${formatCapability(cyclicRequirement)} in a capability dependency cycle`);
327
+ const message = await this.failBeforeStart(staged, error);
328
+ if (this.enforceRequiredTerminal && pluginId === REQUIRED_TERMINAL_PLUGIN_ID) {
329
+ throw requiredTerminalError(`Required Terminal server entry failed during start: ${message}`, error);
330
+ }
331
+ }
332
+ continue;
333
+ }
334
+ if (allowLateHostWait) {
335
+ const providerCycles = candidates.flatMap((staged) => {
336
+ if (staged.activation.workspaceProvider === undefined)
337
+ return [];
338
+ const lateCapability = findLateHostDependency(staged, pending, this.declaredCapabilitiesByKey);
339
+ return lateCapability === undefined ? [] : [{ staged, lateCapability }];
340
+ });
341
+ if (providerCycles.length > 0) {
342
+ for (const { staged, lateCapability } of providerCycles) {
343
+ pending.delete(staged.entry.id);
344
+ const error = new Error(`Server plugin ${staged.entry.id} contributes a workspace provider that depends on late host capability ${formatCapability(lateCapability)} in a capability dependency cycle`);
345
+ const message = await this.failBeforeStart(staged, error);
346
+ if (this.enforceRequiredTerminal && staged.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
347
+ throw requiredTerminalError(`Required Terminal server entry failed during start: ${message}`, error);
348
+ }
349
+ }
350
+ continue;
351
+ }
352
+ if (candidates.every((staged) => (findLateHostDependency(staged, pending, this.declaredCapabilitiesByKey) !== undefined)))
353
+ return;
354
+ }
355
+ const unresolved = candidates[0];
356
+ if (unresolved === undefined)
357
+ return;
358
+ pending.delete(unresolved.entry.id);
359
+ const error = new Error(`Server plugin ${unresolved.entry.id} has an unresolved capability dependency cycle`);
360
+ const message = await this.failBeforeStart(unresolved, error);
361
+ if (this.enforceRequiredTerminal && unresolved.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
362
+ throw requiredTerminalError(`Required Terminal server entry failed during start: ${message}`, error);
363
+ }
364
+ }
365
+ }
366
+ dependencyFailure(staged, pending) {
367
+ for (const requirement of staged.plugin.requires ?? []) {
368
+ const key = capabilityKey(requirement);
369
+ const provision = this.declaredCapabilitiesByKey.get(key);
370
+ if (provision === undefined) {
371
+ return new Error(`Server plugin ${staged.entry.id} requires unavailable capability ${formatCapability(requirement)}`);
372
+ }
373
+ if (this.activeCapabilitiesByKey.has(key)
374
+ || provision.source === "host"
375
+ || provision.source === "host-factory"
376
+ || provision.source === "late-host")
377
+ continue;
378
+ if (!pending.has(requirement.pluginId)) {
379
+ return new Error(`Server plugin ${staged.entry.id} requires ${formatCapability(requirement)}, but provider plugin ${requirement.pluginId} did not start`);
380
+ }
381
+ }
382
+ return undefined;
383
+ }
384
+ waitingForDependencies(staged) {
385
+ return (staged.plugin.requires ?? []).some((requirement) => {
386
+ const key = capabilityKey(requirement);
387
+ return this.declaredCapabilitiesByKey.get(key)?.source !== "host-factory"
388
+ && !this.activeCapabilitiesByKey.has(key);
389
+ });
390
+ }
391
+ async startStagedPlugin(staged) {
392
+ try {
393
+ const capabilities = this.capabilityResolver(staged);
394
+ const start = staged.activation.start?.bind(staged.activation);
395
+ if (start !== undefined) {
396
+ await runBounded(staged.entry.id, "start", this.lifecycleTimeoutMs, (signal) => start(Object.freeze({
397
+ capabilities,
398
+ signal,
399
+ })));
400
+ }
401
+ if (this.enforceRequiredTerminal && staged.entry.id === REQUIRED_TERMINAL_PLUGIN_ID) {
402
+ await this.requireHealthyTerminal(staged);
403
+ }
404
+ this.publish(staged);
405
+ return undefined;
406
+ }
407
+ catch (error) {
408
+ await this.failBeforeStart(staged, error);
409
+ return error;
410
+ }
411
+ }
412
+ capabilityResolver(staged) {
413
+ const resolved = new Map();
414
+ for (const requirement of staged.plugin.requires ?? []) {
415
+ const key = capabilityKey(requirement);
416
+ const activeProvision = this.activeCapabilitiesByKey.get(key);
417
+ const declaration = this.declaredCapabilitiesByKey.get(key);
418
+ let provisionValue;
419
+ if (activeProvision !== undefined) {
420
+ provisionValue = activeProvision.value;
421
+ }
422
+ else if (declaration?.source === "host-factory") {
423
+ provisionValue = this.createHostCapabilityValue(staged, declaration);
184
424
  }
185
425
  else {
186
- this.logger.error(details, "server plugin activation failed");
426
+ throw new Error(`Server plugin ${staged.entry.id} requires inactive capability ${formatCapability(requirement)}`);
187
427
  }
428
+ // Validate the declared requirement before start, but retain the provider value:
429
+ // resolve() may supply a different parser for this same capability key.
430
+ parseCapabilityValue(requirement, provisionValue, `Required capability ${formatCapability(requirement)} for server plugin ${staged.entry.id}`);
431
+ resolved.set(key, { provisionValue });
188
432
  }
433
+ return createCapabilityResolver(staged.entry.id, resolved);
434
+ }
435
+ createHostCapabilityValue(staged, factory) {
436
+ const context = Object.freeze({
437
+ pluginId: staged.entry.id,
438
+ packageRoot: staged.entry.packageRoot,
439
+ lifetimeSignal: staged.lifetimeController.signal,
440
+ });
441
+ const instance = factory.create(context);
442
+ staged.hostCapabilityInstances.push(instance);
443
+ return parseCapabilityValue(factory.capability, instance.value, `Host capability ${formatCapability(factory.capability)} for server plugin ${staged.entry.id}`);
444
+ }
445
+ async requireHealthyTerminal(staged) {
446
+ const health = staged.activation.health?.bind(staged.activation);
447
+ if (health === undefined)
448
+ return;
449
+ const result = await runBounded(staged.entry.id, "health", this.lifecycleTimeoutMs, (signal) => health(signal));
450
+ const parsed = parseHealth(result);
451
+ if (parsed.status === "unhealthy") {
452
+ throw new Error(`Required Terminal server entry is unhealthy: ${parsed.message ?? "health check failed"}`);
453
+ }
454
+ }
455
+ publish(staged) {
456
+ const providerContribution = staged.activation.workspaceProvider === undefined
457
+ ? undefined
458
+ : Object.freeze({
459
+ pluginId: staged.entry.id,
460
+ pluginName: staged.plugin.name,
461
+ packageRoot: staged.entry.packageRoot,
462
+ source: staged.entry.source,
463
+ scope: staged.entry.scope,
464
+ moduleRevision: requireServerModule(staged.entry).revision,
465
+ provider: staged.activation.workspaceProvider,
466
+ });
467
+ const pairedBackendContribution = staged.activation.peer === undefined
468
+ ? undefined
469
+ : Object.freeze({
470
+ pluginId: staged.entry.id,
471
+ pluginName: staged.plugin.name,
472
+ packageRoot: staged.entry.packageRoot,
473
+ source: staged.entry.source,
474
+ scope: staged.entry.scope,
475
+ moduleRevision: requireServerModule(staged.entry).revision,
476
+ backend: staged.activation.peer,
477
+ requires: this.resolvedCapabilityRequirements(staged),
478
+ });
479
+ const active = Object.freeze({
480
+ ...staged,
481
+ ...(providerContribution === undefined ? {} : { providerContribution }),
482
+ ...(pairedBackendContribution === undefined ? {} : { pairedBackendContribution }),
483
+ });
484
+ this.removeStaged(staged.entry.id);
485
+ this.activePlugins.push(active);
486
+ for (const provision of staged.provisions)
487
+ this.activeCapabilitiesByKey.set(provision.key, provision);
488
+ this.recordsById.set(staged.entry.id, recordFor(staged.entry, {
489
+ state: "active",
490
+ name: staged.plugin.name,
491
+ ...(pairedBackendContribution?.backend.request === undefined ? {} : { pairedRequestVersion: 1 }),
492
+ ...(pairedBackendContribution?.backend.openChannel === undefined ? {} : { pairedChannelVersion: 1 }),
493
+ }));
494
+ this.logger.info({ pluginId: staged.entry.id, pluginName: staged.plugin.name }, "server plugin activated");
495
+ }
496
+ resolvedCapabilityRequirements(staged) {
497
+ const requirements = new Map();
498
+ const visited = new Set();
499
+ const visit = (plugin) => {
500
+ if (visited.has(plugin.entry.id))
501
+ return;
502
+ visited.add(plugin.entry.id);
503
+ for (const requirement of plugin.plugin.requires ?? []) {
504
+ const key = capabilityKey(requirement);
505
+ requirements.set(key, requirement);
506
+ if (this.declaredCapabilitiesByKey.get(key)?.source !== "plugin")
507
+ continue;
508
+ const provider = this.activePlugins.find(({ entry }) => entry.id === requirement.pluginId);
509
+ if (provider !== undefined)
510
+ visit(provider);
511
+ }
512
+ };
513
+ visit(staged);
514
+ return Object.freeze([...requirements.values()]);
189
515
  }
190
- async rollbackStart(pluginId, activation) {
191
- const stop = activation.stop?.bind(activation);
192
- if (stop === undefined)
516
+ async failBeforeStart(staged, error) {
517
+ abortPluginLifetime(staged.lifetimeController, staged.entry.id, "startup failed");
518
+ staged.noticeReporter?.revoke();
519
+ this.removeStaged(staged.entry.id);
520
+ const dispose = staged.activation.dispose?.bind(staged.activation);
521
+ const rollbackError = dispose === undefined ? undefined : await this.runDispose(staged.entry.id, dispose);
522
+ const hostCleanupError = await this.disposeHostCapabilityInstances(staged);
523
+ const message = withHostCleanupError(withRollbackError(errorMessage(error), rollbackError), hostCleanupError);
524
+ this.recordsById.set(staged.entry.id, recordFor(staged.entry, {
525
+ state: "failed",
526
+ name: staged.plugin.name,
527
+ phase: "start",
528
+ message,
529
+ }));
530
+ this.logger.error({
531
+ err: error,
532
+ pluginId: staged.entry.id,
533
+ phase: "start",
534
+ ...(rollbackError === undefined ? {} : { rollbackError }),
535
+ ...(hostCleanupError === undefined ? {} : { hostCleanupError }),
536
+ }, "server plugin start failed");
537
+ return message;
538
+ }
539
+ removeStaged(pluginId) {
540
+ this.stagedPlugins = this.stagedPlugins.filter(({ entry }) => entry.id !== pluginId);
541
+ }
542
+ async disposeForShutdown(plugin) {
543
+ plugin.noticeReporter?.revoke();
544
+ const dispose = plugin.activation.dispose?.bind(plugin.activation);
545
+ const pluginDisposeError = dispose === undefined ? undefined : await this.runDispose(plugin.entry.id, dispose);
546
+ const hostCleanupError = await this.disposeHostCapabilityInstances(plugin);
547
+ const error = combinedDisposalError(pluginDisposeError, hostCleanupError);
548
+ if (error === undefined)
549
+ return;
550
+ this.recordsById.set(plugin.entry.id, recordFor(plugin.entry, {
551
+ state: "failed",
552
+ name: plugin.plugin.name,
553
+ phase: "dispose",
554
+ message: errorMessage(error),
555
+ ...(plugin.activation.peer?.request === undefined ? {} : { pairedRequestVersion: 1 }),
556
+ ...(plugin.activation.peer?.openChannel === undefined ? {} : { pairedChannelVersion: 1 }),
557
+ }));
558
+ this.logger.error({
559
+ err: error,
560
+ pluginId: plugin.entry.id,
561
+ phase: "dispose",
562
+ ...(hostCleanupError === undefined ? {} : { hostCleanupError }),
563
+ }, "server plugin disposal failed");
564
+ }
565
+ async disposeHostCapabilityInstances(plugin) {
566
+ const instances = plugin.hostCapabilityInstances.splice(0).reverse();
567
+ const errors = [];
568
+ for (const instance of instances) {
569
+ if (instance.dispose === undefined)
570
+ continue;
571
+ const error = await this.runDispose(plugin.entry.id, instance.dispose);
572
+ if (error !== undefined)
573
+ errors.push(error);
574
+ }
575
+ if (errors.length === 0)
193
576
  return undefined;
577
+ if (errors.length === 1)
578
+ return errors[0];
579
+ return new AggregateError(errors, "Multiple host capability cleanup operations failed");
580
+ }
581
+ async runDispose(pluginId, dispose) {
194
582
  try {
195
- await runBounded(pluginId, "stop", this.lifecycleTimeoutMs, (signal) => stop(signal));
583
+ await runBounded(pluginId, "dispose", this.lifecycleTimeoutMs, (signal) => dispose(signal));
196
584
  return undefined;
197
585
  }
198
586
  catch (error) {
@@ -200,6 +588,46 @@ export class ServerPluginRuntime {
200
588
  }
201
589
  }
202
590
  }
591
+ function requireTerminalCatalogEntry(snapshot) {
592
+ const entry = snapshot.plugins.find(({ id }) => id === REQUIRED_TERMINAL_PLUGIN_ID);
593
+ if (entry === undefined)
594
+ throw requiredTerminalError("Required bundled Terminal package is missing");
595
+ if (entry.scope !== "bundled" || entry.source !== "bundled") {
596
+ throw requiredTerminalError("Required Terminal package must come from the bundled plugin scope");
597
+ }
598
+ if (entry.browserModule === undefined || entry.serverModule === undefined || !entry.machineSpecific) {
599
+ throw requiredTerminalError("Required Terminal package must provide machine-specific browser and server entries");
600
+ }
601
+ if (!entry.enabled)
602
+ throw requiredTerminalError("Required Terminal package cannot be disabled in normal startup");
603
+ return entry;
604
+ }
605
+ function requireTerminalActivation(activation) {
606
+ if (activation.peer?.request === undefined || activation.peer.openChannel === undefined) {
607
+ throw new IncompatibleServerPluginError("Required Terminal server entry must expose peer request and channel capabilities");
608
+ }
609
+ const requiredKey = capabilityKey(REQUIRED_TERMINAL_SERVICE_CAPABILITY);
610
+ const provision = activation.provides?.find(({ capability }) => capabilityKey(capability) === requiredKey);
611
+ if (provision === undefined) {
612
+ throw new IncompatibleServerPluginError(`Required Terminal server entry must provide ${formatCapability(REQUIRED_TERMINAL_SERVICE_CAPABILITY)}`);
613
+ }
614
+ try {
615
+ REQUIRED_TERMINAL_SERVICE_CAPABILITY.parse(provision.value);
616
+ }
617
+ catch (error) {
618
+ throw new IncompatibleServerPluginError(errorMessage(error), { cause: error });
619
+ }
620
+ }
621
+ function requiredTerminalFirst(left, right) {
622
+ if (left.id === REQUIRED_TERMINAL_PLUGIN_ID)
623
+ return right.id === REQUIRED_TERMINAL_PLUGIN_ID ? 0 : -1;
624
+ if (right.id === REQUIRED_TERMINAL_PLUGIN_ID)
625
+ return 1;
626
+ return left.id.localeCompare(right.id);
627
+ }
628
+ function requiredTerminalError(message, cause) {
629
+ return new RequiredTerminalPluginError(`${message}. ${REQUIRED_TERMINAL_RECOVERY_GUIDANCE}`, cause === undefined ? {} : { cause });
630
+ }
203
631
  function disabledReason(entry, safeStart) {
204
632
  if (!entry.enabled)
205
633
  return "disabled in PI WEB config";
@@ -218,6 +646,8 @@ function recordFor(entry, status) {
218
646
  ...(entry.browserModule === undefined ? {} : { browserRevision: entry.browserModule.revision }),
219
647
  settingsRevision: entry.settingsRevision,
220
648
  machineSpecific: entry.machineSpecific,
649
+ ...(status.pairedRequestVersion === undefined ? {} : { pairedRequestVersion: status.pairedRequestVersion }),
650
+ ...(status.pairedChannelVersion === undefined ? {} : { pairedChannelVersion: status.pairedChannelVersion }),
221
651
  state: status.state,
222
652
  ...(status.name === undefined ? {} : { name: status.name }),
223
653
  ...(status.phase === undefined ? {} : { phase: status.phase }),
@@ -246,44 +676,62 @@ function parsePluginExport(imported) {
246
676
  const plugin = imported["default"];
247
677
  if (!isRecord(plugin))
248
678
  throw new IncompatibleServerPluginError("Server plugin module must export a default plugin object");
249
- const candidate = {
250
- apiVersion: plugin["apiVersion"],
251
- name: plugin["name"],
252
- activate: plugin["activate"],
253
- };
254
- if (candidate.apiVersion !== 1) {
255
- throw new IncompatibleServerPluginError(`Unsupported server plugin API version: ${formatUnknown(candidate.apiVersion)}`);
679
+ const apiVersion = plugin["apiVersion"];
680
+ const name = plugin["name"];
681
+ const activateValue = plugin["activate"];
682
+ if (apiVersion !== 3) {
683
+ throw new IncompatibleServerPluginError(`Unsupported server plugin API version: ${formatUnknown(apiVersion)}`);
256
684
  }
257
- if (typeof candidate.name !== "string" || candidate.name === "") {
685
+ if (typeof name !== "string" || name === "") {
258
686
  throw new IncompatibleServerPluginError("Server plugin name must be a non-empty string");
259
687
  }
260
- if (typeof candidate.activate !== "function" || !isPiWebServerPlugin(candidate)) {
688
+ if (typeof activateValue !== "function") {
261
689
  throw new IncompatibleServerPluginError("Server plugin activate must be a function");
262
690
  }
263
- const activate = candidate.activate.bind(plugin);
264
- return Object.freeze({ apiVersion: 1, name: candidate.name, activate: (context) => activate(context) });
691
+ const requires = snapshotCapabilityRequirements(plugin["requires"], `Server plugin ${name} requirements`);
692
+ return Object.freeze({
693
+ name,
694
+ ...(requires.length === 0 ? {} : { requires }),
695
+ activate(context) {
696
+ const result = Reflect.apply(activateValue, plugin, [context]);
697
+ return result;
698
+ },
699
+ });
265
700
  }
266
- function isPiWebServerPlugin(value) {
267
- return isRecord(value)
268
- && value["apiVersion"] === 1
269
- && typeof value["name"] === "string"
270
- && value["name"] !== ""
271
- && typeof value["activate"] === "function";
701
+ function activationDisposeForRollback(value) {
702
+ if (!isRecord(value))
703
+ return undefined;
704
+ const dispose = value["dispose"];
705
+ if (typeof dispose !== "function")
706
+ return undefined;
707
+ return async (signal) => {
708
+ await Reflect.apply(dispose, value, [signal]);
709
+ };
272
710
  }
273
- function parseActivation(value) {
711
+ function parseActivation(value, pluginId) {
274
712
  if (!isRecord(value))
275
713
  throw new IncompatibleServerPluginError("Server plugin activation must be an object");
276
714
  if (value["workspaceProviders"] !== undefined) {
277
715
  throw new IncompatibleServerPluginError("Server plugins may contribute only one workspaceProvider");
278
716
  }
717
+ if (value["requiredTerminalService"] !== undefined) {
718
+ throw new IncompatibleServerPluginError("requiredTerminalService was removed in server plugin API v3; use a typed capability provision");
719
+ }
720
+ if (value["stop"] !== undefined) {
721
+ throw new IncompatibleServerPluginError("Server plugin stop was removed in API v3; use dispose");
722
+ }
279
723
  const workspaceProviderValue = value["workspaceProvider"];
724
+ const peerValue = value["peer"];
725
+ const provides = snapshotCapabilityProvisions(value["provides"], pluginId, `Server plugin ${pluginId} provisions`);
280
726
  const candidate = {
281
727
  workspaceProvider: workspaceProviderValue === undefined ? undefined : snapshotWorkspaceProvider(workspaceProviderValue),
728
+ peer: peerValue === undefined ? undefined : snapshotPluginPeer(peerValue),
729
+ provides,
282
730
  start: value["start"],
283
- stop: value["stop"],
731
+ dispose: value["dispose"],
284
732
  health: value["health"],
285
733
  };
286
- for (const callback of ["start", "stop", "health"]) {
734
+ for (const callback of ["start", "dispose", "health"]) {
287
735
  const callbackValue = candidate[callback];
288
736
  if (callbackValue !== undefined && typeof callbackValue !== "function") {
289
737
  throw new IncompatibleServerPluginError(`Server plugin ${callback} must be a function`);
@@ -292,12 +740,14 @@ function parseActivation(value) {
292
740
  if (!isServerPluginActivation(candidate))
293
741
  throw new IncompatibleServerPluginError("Server plugin activation is invalid");
294
742
  const start = candidate.start?.bind(value);
295
- const stop = candidate.stop?.bind(value);
743
+ const dispose = candidate.dispose?.bind(value);
296
744
  const health = candidate.health?.bind(value);
297
745
  return Object.freeze({
298
746
  ...(candidate.workspaceProvider === undefined ? {} : { workspaceProvider: candidate.workspaceProvider }),
299
- ...(start === undefined ? {} : { start: (signal) => start(signal) }),
300
- ...(stop === undefined ? {} : { stop: (signal) => stop(signal) }),
747
+ ...(candidate.peer === undefined ? {} : { peer: candidate.peer }),
748
+ ...(provides.length === 0 ? {} : { provides }),
749
+ ...(start === undefined ? {} : { start: (context) => start(context) }),
750
+ ...(dispose === undefined ? {} : { dispose: (signal) => dispose(signal) }),
301
751
  ...(health === undefined ? {} : { health: (signal) => health(signal) }),
302
752
  });
303
753
  }
@@ -305,14 +755,342 @@ function isServerPluginActivation(value) {
305
755
  if (!isRecord(value))
306
756
  return false;
307
757
  const workspaceProvider = value["workspaceProvider"];
758
+ const peer = value["peer"];
759
+ const provides = value["provides"];
308
760
  const start = value["start"];
309
- const stop = value["stop"];
761
+ const dispose = value["dispose"];
310
762
  const health = value["health"];
311
763
  return (workspaceProvider === undefined || isWorkspaceProvider(workspaceProvider))
764
+ && (peer === undefined || isPluginPeer(peer))
765
+ && (provides === undefined || Array.isArray(provides))
312
766
  && (start === undefined || typeof start === "function")
313
- && (stop === undefined || typeof stop === "function")
767
+ && (dispose === undefined || typeof dispose === "function")
314
768
  && (health === undefined || typeof health === "function");
315
769
  }
770
+ function snapshotCapabilityRequirements(value, label) {
771
+ if (value === undefined)
772
+ return Object.freeze([]);
773
+ if (!Array.isArray(value))
774
+ throw new IncompatibleServerPluginError(`${label} must be an array`);
775
+ const seen = new Set();
776
+ const requirements = [];
777
+ for (let index = 0; index < value.length; index += 1) {
778
+ if (!Object.hasOwn(value, index))
779
+ throw new IncompatibleServerPluginError(`${label} must not be sparse`);
780
+ const capability = snapshotUnknownCapability(value[index], `${label}[${String(index)}]`);
781
+ const key = capabilityKey(capability);
782
+ if (seen.has(key))
783
+ throw new IncompatibleServerPluginError(`${label} declares ${formatCapability(capability)} more than once`);
784
+ seen.add(key);
785
+ requirements.push(capability);
786
+ }
787
+ return Object.freeze(requirements);
788
+ }
789
+ function snapshotCapabilityProvisions(value, ownerPluginId, label) {
790
+ if (value === undefined)
791
+ return Object.freeze([]);
792
+ if (!Array.isArray(value))
793
+ throw new IncompatibleServerPluginError(`${label} must be an array`);
794
+ const seen = new Set();
795
+ const provisions = [];
796
+ for (let index = 0; index < value.length; index += 1) {
797
+ if (!Object.hasOwn(value, index))
798
+ throw new IncompatibleServerPluginError(`${label} must not be sparse`);
799
+ const candidate = value[index];
800
+ if (!isRecord(candidate))
801
+ throw new IncompatibleServerPluginError(`${label}[${String(index)}] must be an object`);
802
+ const capability = snapshotUnknownCapability(candidate["capability"], `${label}[${String(index)}].capability`);
803
+ if (ownerPluginId !== undefined && capability.pluginId !== ownerPluginId) {
804
+ throw new IncompatibleServerPluginError(`Server plugin ${ownerPluginId} cannot provide capability owned by ${capability.pluginId}`);
805
+ }
806
+ const key = capabilityKey(capability);
807
+ if (seen.has(key))
808
+ throw new IncompatibleServerPluginError(`${label} publishes ${formatCapability(capability)} more than once`);
809
+ seen.add(key);
810
+ const parsedValue = parseCapabilityValue(capability, candidate["value"], `Provided capability ${formatCapability(capability)}`);
811
+ provisions.push(Object.freeze({ capability, value: parsedValue }));
812
+ }
813
+ return Object.freeze(provisions);
814
+ }
815
+ function snapshotHostCapabilities(value) {
816
+ const provisions = snapshotCapabilityProvisions(value, undefined, "Host capability provisions");
817
+ return internalProvisions({ provides: provisions }, "host");
818
+ }
819
+ function snapshotLateHostCapabilities(value) {
820
+ if (value === undefined)
821
+ return Object.freeze([]);
822
+ if (!Array.isArray(value))
823
+ throw new Error("Late host capabilities must be an array");
824
+ const seen = new Set();
825
+ const capabilities = [];
826
+ for (let index = 0; index < value.length; index += 1) {
827
+ if (!Object.hasOwn(value, index))
828
+ throw new Error("Late host capabilities must not be sparse");
829
+ const capability = snapshotUnknownCapability(value[index], `Late host capabilities[${String(index)}]`);
830
+ const key = capabilityKey(capability);
831
+ if (seen.has(key))
832
+ throw new Error(`Late host capability ${formatCapability(capability)} is configured more than once`);
833
+ seen.add(key);
834
+ capabilities.push(Object.freeze({ capability, key, source: "late-host" }));
835
+ }
836
+ return Object.freeze(capabilities);
837
+ }
838
+ function snapshotHostCapabilityFactories(value) {
839
+ if (value === undefined)
840
+ return Object.freeze([]);
841
+ if (!Array.isArray(value))
842
+ throw new Error("Host capability factories must be an array");
843
+ const seen = new Set();
844
+ const factories = [];
845
+ for (let index = 0; index < value.length; index += 1) {
846
+ if (!Object.hasOwn(value, index))
847
+ throw new Error("Host capability factories must not be sparse");
848
+ const candidate = value[index];
849
+ if (!isRecord(candidate))
850
+ throw new Error(`Host capability factories[${String(index)}] must be an object`);
851
+ const capability = snapshotUnknownCapability(candidate["capability"], `Host capability factories[${String(index)}].capability`);
852
+ const key = capabilityKey(capability);
853
+ if (seen.has(key))
854
+ throw new Error(`Host capability factory for ${formatCapability(capability)} is configured more than once`);
855
+ const create = candidate["create"];
856
+ if (typeof create !== "function")
857
+ throw new Error(`Host capability factory for ${formatCapability(capability)} must expose create`);
858
+ seen.add(key);
859
+ factories.push(Object.freeze({
860
+ capability,
861
+ key,
862
+ source: "host-factory",
863
+ create(context) {
864
+ const result = Reflect.apply(create, candidate, [context]);
865
+ return snapshotHostCapabilityInstance(result, `Host capability ${formatCapability(capability)}`);
866
+ },
867
+ }));
868
+ }
869
+ return Object.freeze(factories);
870
+ }
871
+ function snapshotHostCapabilityInstance(value, label) {
872
+ if (!isRecord(value) || !Object.hasOwn(value, "value")) {
873
+ throw new Error(`${label} factory must return an object containing value`);
874
+ }
875
+ const dispose = value["dispose"];
876
+ if (dispose !== undefined && typeof dispose !== "function") {
877
+ throw new Error(`${label} factory dispose must be a function`);
878
+ }
879
+ return Object.freeze({
880
+ value: value["value"],
881
+ ...(dispose === undefined
882
+ ? {}
883
+ : { dispose: async (signal) => { await Reflect.apply(dispose, value, [signal]); } }),
884
+ });
885
+ }
886
+ function internalProvisions(activation, source) {
887
+ return Object.freeze((activation.provides ?? []).map((provision) => Object.freeze({
888
+ capability: provision.capability,
889
+ key: capabilityKey(provision.capability),
890
+ value: provision.value,
891
+ source,
892
+ })));
893
+ }
894
+ function snapshotUnknownCapability(value, label) {
895
+ if (!isRecord(value))
896
+ throw new IncompatibleServerPluginError(`${label} must be an object`);
897
+ const pluginId = value["pluginId"];
898
+ const id = value["id"];
899
+ const version = value["version"];
900
+ const parse = value["parse"];
901
+ const validated = validateCapabilityFields(pluginId, id, version, parse, label);
902
+ return Object.freeze({
903
+ pluginId: validated.pluginId,
904
+ id: validated.id,
905
+ version: validated.version,
906
+ parse(input) {
907
+ const result = Reflect.apply(validated.parse, value, [input]);
908
+ return result;
909
+ },
910
+ });
911
+ }
912
+ function snapshotTypedCapability(value, label) {
913
+ const validated = validateCapabilityFields(value.pluginId, value.id, value.version, value.parse, label);
914
+ const parse = value.parse.bind(value);
915
+ return Object.freeze({
916
+ pluginId: validated.pluginId,
917
+ id: validated.id,
918
+ version: validated.version,
919
+ parse: (input) => parse(input),
920
+ });
921
+ }
922
+ function validateCapabilityFields(pluginId, id, version, parse, label) {
923
+ if (typeof pluginId !== "string" || pluginId.trim() === "") {
924
+ throw new IncompatibleServerPluginError(`${label} pluginId must be a non-empty string`);
925
+ }
926
+ if (typeof id !== "string" || id.trim() === "") {
927
+ throw new IncompatibleServerPluginError(`${label} id must be a non-empty string`);
928
+ }
929
+ if (typeof version !== "number" || !Number.isInteger(version) || version <= 0) {
930
+ throw new IncompatibleServerPluginError(`${label} version must be a positive integer`);
931
+ }
932
+ if (!isCapabilityParser(parse))
933
+ throw new IncompatibleServerPluginError(`${label} parse must be a function`);
934
+ return { pluginId, id, version, parse };
935
+ }
936
+ function isCapabilityParser(value) {
937
+ return typeof value === "function";
938
+ }
939
+ function parseCapabilityValue(capability, value, label) {
940
+ try {
941
+ return capability.parse(value);
942
+ }
943
+ catch (error) {
944
+ throw new IncompatibleServerPluginError(`${label} is invalid: ${errorMessage(error)}`, { cause: error });
945
+ }
946
+ }
947
+ function capabilityKey(capability) {
948
+ return JSON.stringify([capability.pluginId, capability.id, capability.version]);
949
+ }
950
+ function formatCapability(capability) {
951
+ return `${capability.pluginId}/${capability.id} v${String(capability.version)}`;
952
+ }
953
+ function createCapabilityResolver(pluginId, resolved) {
954
+ return Object.freeze({
955
+ resolve(value) {
956
+ const capability = snapshotTypedCapability(value, `Capability request from server plugin ${pluginId}`);
957
+ const requirement = resolved.get(capabilityKey(capability));
958
+ if (requirement === undefined) {
959
+ throw new Error(`Server plugin ${pluginId} did not declare required capability ${formatCapability(capability)}`);
960
+ }
961
+ return parseCapabilityValue(capability, requirement.provisionValue, `Required capability ${formatCapability(capability)} for server plugin ${pluginId}`);
962
+ },
963
+ });
964
+ }
965
+ function findLateHostDependency(staged, pending, declared, visiting = new Set()) {
966
+ if (visiting.has(staged.entry.id))
967
+ return undefined;
968
+ const nextVisiting = new Set(visiting);
969
+ nextVisiting.add(staged.entry.id);
970
+ for (const requirement of staged.plugin.requires ?? []) {
971
+ const declaration = declared.get(capabilityKey(requirement));
972
+ if (declaration?.source === "late-host")
973
+ return declaration.capability;
974
+ if (declaration?.source !== "plugin")
975
+ continue;
976
+ const provider = pending.get(requirement.pluginId);
977
+ if (provider === undefined)
978
+ continue;
979
+ const lateCapability = findLateHostDependency(provider, pending, declared, nextVisiting);
980
+ if (lateCapability !== undefined)
981
+ return lateCapability;
982
+ }
983
+ return undefined;
984
+ }
985
+ function findCapabilityCycle(pending, declared) {
986
+ const state = new Map();
987
+ const stack = [];
988
+ const visit = (pluginId) => {
989
+ state.set(pluginId, "visiting");
990
+ stack.push(pluginId);
991
+ const plugin = pending.get(pluginId);
992
+ const providers = (plugin?.plugin.requires ?? [])
993
+ .filter((requirement) => declared.get(capabilityKey(requirement))?.source === "plugin" && pending.has(requirement.pluginId))
994
+ .map((requirement) => requirement.pluginId)
995
+ .sort((left, right) => left.localeCompare(right));
996
+ for (const providerId of providers) {
997
+ if (state.get(providerId) === "visiting") {
998
+ const start = stack.indexOf(providerId);
999
+ return stack.slice(start);
1000
+ }
1001
+ if (state.get(providerId) !== "visited") {
1002
+ const cycle = visit(providerId);
1003
+ if (cycle !== undefined)
1004
+ return cycle;
1005
+ }
1006
+ }
1007
+ stack.pop();
1008
+ state.set(pluginId, "visited");
1009
+ return undefined;
1010
+ };
1011
+ for (const pluginId of [...pending.keys()].sort((left, right) => left.localeCompare(right))) {
1012
+ if (state.has(pluginId))
1013
+ continue;
1014
+ const cycle = visit(pluginId);
1015
+ if (cycle !== undefined)
1016
+ return cycle;
1017
+ }
1018
+ return [];
1019
+ }
1020
+ function abortPluginLifetime(controller, pluginId, outcome) {
1021
+ if (!controller.signal.aborted) {
1022
+ controller.abort(new DOMException(`Server plugin ${pluginId} ${outcome}`, "AbortError"));
1023
+ }
1024
+ }
1025
+ function withRollbackError(message, rollbackError) {
1026
+ return rollbackError === undefined
1027
+ ? message
1028
+ : `${message}; startup rollback failed: ${errorMessage(rollbackError)}`;
1029
+ }
1030
+ function withHostCleanupError(message, cleanupError) {
1031
+ return cleanupError === undefined
1032
+ ? message
1033
+ : `${message}; host capability cleanup failed: ${errorMessage(cleanupError)}`;
1034
+ }
1035
+ function combinedDisposalError(pluginError, hostCleanupError) {
1036
+ if (pluginError === undefined)
1037
+ return hostCleanupError;
1038
+ if (hostCleanupError === undefined)
1039
+ return pluginError;
1040
+ return new AggregateError([pluginError, hostCleanupError], `${errorMessage(pluginError)}; host capability cleanup failed: ${errorMessage(hostCleanupError)}`);
1041
+ }
1042
+ function snapshotPluginPeer(value) {
1043
+ if (!isPluginPeer(value)) {
1044
+ throw new IncompatibleServerPluginError("Server plugin peer must include a request or channel handler");
1045
+ }
1046
+ const request = value.request?.bind(value);
1047
+ const openChannel = value.openChannel?.bind(value);
1048
+ const snapshotRequest = request === undefined
1049
+ ? undefined
1050
+ : (context) => request(context);
1051
+ const snapshotOpenChannel = openChannel === undefined
1052
+ ? undefined
1053
+ : async (context) => (snapshotPluginPeerChannel(await openChannel(context)));
1054
+ if (snapshotRequest !== undefined && snapshotOpenChannel !== undefined) {
1055
+ return Object.freeze({ request: snapshotRequest, openChannel: snapshotOpenChannel });
1056
+ }
1057
+ if (snapshotRequest !== undefined)
1058
+ return Object.freeze({ request: snapshotRequest });
1059
+ if (snapshotOpenChannel !== undefined)
1060
+ return Object.freeze({ openChannel: snapshotOpenChannel });
1061
+ throw new IncompatibleServerPluginError("Server plugin peer must include a request or channel handler");
1062
+ }
1063
+ function isPluginPeer(value) {
1064
+ if (!isRecord(value))
1065
+ return false;
1066
+ const request = value["request"];
1067
+ const openChannel = value["openChannel"];
1068
+ return (typeof request === "function" || typeof openChannel === "function")
1069
+ && (request === undefined || typeof request === "function")
1070
+ && (openChannel === undefined || typeof openChannel === "function");
1071
+ }
1072
+ function snapshotPluginPeerChannel(value) {
1073
+ if (!isPluginPeerChannel(value)) {
1074
+ throw new Error("Server plugin openChannel must return a channel with receive, optional completion, and optional close callbacks");
1075
+ }
1076
+ const receive = value.receive.bind(value);
1077
+ const closed = value.closed === undefined ? undefined : Promise.resolve(value.closed);
1078
+ const close = value.close?.bind(value);
1079
+ return Object.freeze({
1080
+ receive: (data, signal) => receive(data, signal),
1081
+ ...(closed === undefined ? {} : { closed }),
1082
+ ...(close === undefined ? {} : { close: (context) => close(context) }),
1083
+ });
1084
+ }
1085
+ function isPluginPeerChannel(value) {
1086
+ return isRecord(value)
1087
+ && typeof value["receive"] === "function"
1088
+ && (value["closed"] === undefined || isPromiseLike(value["closed"]))
1089
+ && (value["close"] === undefined || typeof value["close"] === "function");
1090
+ }
1091
+ function isPromiseLike(value) {
1092
+ return isRecord(value) && typeof value["then"] === "function";
1093
+ }
316
1094
  function snapshotWorkspaceProvider(value) {
317
1095
  if (!isRecord(value))
318
1096
  throw new IncompatibleServerPluginError("Server plugin workspaceProvider is invalid");
@@ -320,20 +1098,17 @@ function snapshotWorkspaceProvider(value) {
320
1098
  fallback: value["fallback"],
321
1099
  probe: value["probe"],
322
1100
  list: value["list"],
323
- request: value["request"],
324
1101
  prepareRemove: value["prepareRemove"],
325
1102
  };
326
1103
  if (!isWorkspaceProvider(candidate))
327
1104
  throw new IncompatibleServerPluginError("Server plugin workspaceProvider is invalid");
328
1105
  const probe = candidate.probe.bind(value);
329
1106
  const list = candidate.list.bind(value);
330
- const request = candidate.request?.bind(value);
331
1107
  const prepareRemove = candidate.prepareRemove?.bind(value);
332
1108
  return Object.freeze({
333
1109
  ...(candidate.fallback === undefined ? {} : { fallback: candidate.fallback }),
334
1110
  probe: (project, signal) => probe(project, signal),
335
1111
  list: (project, signal) => list(project, signal),
336
- ...(request === undefined ? {} : { request: (context) => request(context) }),
337
1112
  ...(prepareRemove === undefined ? {} : { prepareRemove: (context) => prepareRemove(context) }),
338
1113
  });
339
1114
  }
@@ -343,12 +1118,10 @@ function isWorkspaceProvider(value) {
343
1118
  const fallback = value["fallback"];
344
1119
  const probe = value["probe"];
345
1120
  const list = value["list"];
346
- const request = value["request"];
347
1121
  const prepareRemove = value["prepareRemove"];
348
1122
  return (fallback === undefined || typeof fallback === "boolean")
349
1123
  && typeof probe === "function"
350
1124
  && typeof list === "function"
351
- && (request === undefined || typeof request === "function")
352
1125
  && (prepareRemove === undefined || typeof prepareRemove === "function");
353
1126
  }
354
1127
  function parseHealth(value) {
@@ -370,6 +1143,67 @@ function parseHealth(value) {
370
1143
  ...(clonedDetails === undefined ? {} : { details: clonedDetails }),
371
1144
  });
372
1145
  }
1146
+ function createScopedNoticeReporter(pluginId, sink) {
1147
+ if (sink === undefined)
1148
+ return undefined;
1149
+ const source = `${SERVER_PLUGIN_NOTICE_SOURCE_PREFIX}${pluginId}`;
1150
+ let active = true;
1151
+ const reporter = Object.freeze({
1152
+ version: 1,
1153
+ record(input) {
1154
+ if (!active)
1155
+ throw new Error(`Server plugin notice reporter for ${pluginId} is no longer active`);
1156
+ sink(source, parseServerPluginNoticeInput(input));
1157
+ },
1158
+ });
1159
+ return Object.freeze({
1160
+ reporter,
1161
+ revoke() { active = false; },
1162
+ });
1163
+ }
1164
+ function parseServerPluginNoticeInput(value) {
1165
+ if (!isPlainRecord(value))
1166
+ throw new Error("Server plugin notice input must be an object");
1167
+ if ("source" in value)
1168
+ throw new Error("Server plugin notices cannot set their source");
1169
+ for (const key in value) {
1170
+ if (!Object.hasOwn(value, key))
1171
+ continue;
1172
+ if (key !== "severity" && key !== "message" && key !== "scope" && key !== "context") {
1173
+ throw new Error(`Unsupported server plugin notice field: ${key}`);
1174
+ }
1175
+ }
1176
+ const severity = value["severity"];
1177
+ if (severity !== "info" && severity !== "warning" && severity !== "error") {
1178
+ throw new Error("Server plugin notice severity must be info, warning, or error");
1179
+ }
1180
+ const message = value["message"];
1181
+ if (typeof message !== "string") {
1182
+ throw new Error("Server plugin notice message must be a non-empty string");
1183
+ }
1184
+ if (serverNoticeStringExceedsUtf8ByteLimit(message, SERVER_PLUGIN_NOTICE_MESSAGE_MAX_BYTES)) {
1185
+ throw new Error(`Server plugin notice message exceeds the ${String(SERVER_PLUGIN_NOTICE_MESSAGE_MAX_BYTES)} byte limit`);
1186
+ }
1187
+ // This scan and allocation are bounded because the UTF-8 limit passed first.
1188
+ if (message.trim() === "") {
1189
+ throw new Error("Server plugin notice message must be a non-empty string");
1190
+ }
1191
+ const scope = value["scope"] === undefined
1192
+ ? undefined
1193
+ : parseServerNoticeScope(value["scope"], "Server plugin notice scope");
1194
+ const context = value["context"] === undefined
1195
+ ? undefined
1196
+ : cloneJsonObject(value["context"], "server plugin notice context", {
1197
+ maxDepth: SERVER_PLUGIN_NOTICE_CONTEXT_MAX_DEPTH,
1198
+ maxBytes: SERVER_PLUGIN_NOTICE_CONTEXT_MAX_BYTES,
1199
+ });
1200
+ return Object.freeze({
1201
+ severity,
1202
+ message,
1203
+ ...(scope === undefined ? {} : { scope }),
1204
+ ...(context === undefined ? {} : { context }),
1205
+ });
1206
+ }
373
1207
  function createScopedLogger(pluginId, logger) {
374
1208
  return Object.freeze({
375
1209
  debug(message, details) {
@@ -404,40 +1238,136 @@ async function runBounded(pluginId, phase, timeoutMs, operation) {
404
1238
  controller.abort(new DOMException("Server plugin operation completed", "AbortError"));
405
1239
  }
406
1240
  }
407
- function cloneJsonObject(value, label) {
408
- if (!isRecord(value))
1241
+ function cloneJsonObject(value, label, limits = {}) {
1242
+ if (!isPlainRecord(value))
409
1243
  throw new IncompatibleServerPluginError(`${label} must be a JSON object`);
410
- return cloneJsonRecord(value, new Set(), label);
1244
+ const byteBudget = limits.maxBytes === undefined
1245
+ ? undefined
1246
+ : { label, maxBytes: limits.maxBytes, remaining: limits.maxBytes };
1247
+ return cloneJsonRecord(value, new Set(), label, 0, limits.maxDepth, byteBudget);
411
1248
  }
412
- function cloneJsonRecord(value, ancestors, label) {
1249
+ function cloneJsonRecord(value, ancestors, label, depth, maxDepth, byteBudget) {
1250
+ requireJsonDepth(depth, maxDepth, label);
413
1251
  if (ancestors.has(value))
414
1252
  throw new IncompatibleServerPluginError(`${label} must not contain cycles`);
1253
+ consumeJsonBytes(byteBudget, 2); // Opening and closing braces.
415
1254
  ancestors.add(value);
416
1255
  const output = {};
417
- for (const [key, child] of Object.entries(value))
418
- output[key] = cloneJsonValue(child, ancestors, label);
1256
+ let propertyCount = 0;
1257
+ for (const key in value) {
1258
+ if (!Object.hasOwn(value, key))
1259
+ continue;
1260
+ if (propertyCount > 0)
1261
+ consumeJsonBytes(byteBudget, 1); // Comma.
1262
+ consumeJsonStringBytes(byteBudget, key);
1263
+ consumeJsonBytes(byteBudget, 1); // Colon.
1264
+ defineJsonProperty(output, key, cloneJsonValue(value[key], ancestors, label, depth + 1, maxDepth, byteBudget));
1265
+ propertyCount += 1;
1266
+ }
419
1267
  ancestors.delete(value);
420
1268
  return Object.freeze(output);
421
1269
  }
422
- function cloneJsonValue(value, ancestors, label) {
423
- if (value === null || typeof value === "string" || typeof value === "boolean")
1270
+ function cloneJsonValue(value, ancestors, label, depth, maxDepth, byteBudget) {
1271
+ requireJsonDepth(depth, maxDepth, label);
1272
+ if (value === null) {
1273
+ consumeJsonBytes(byteBudget, 4);
1274
+ return value;
1275
+ }
1276
+ if (typeof value === "string") {
1277
+ consumeJsonStringBytes(byteBudget, value);
1278
+ return value;
1279
+ }
1280
+ if (typeof value === "boolean") {
1281
+ consumeJsonBytes(byteBudget, value ? 4 : 5);
424
1282
  return value;
1283
+ }
425
1284
  if (typeof value === "number") {
426
1285
  if (!Number.isFinite(value))
427
1286
  throw new IncompatibleServerPluginError(`${label} must contain only finite JSON numbers`);
1287
+ consumeJsonBytes(byteBudget, String(value).length);
428
1288
  return value;
429
1289
  }
430
- if (Array.isArray(value)) {
431
- if (ancestors.has(value))
432
- throw new IncompatibleServerPluginError(`${label} must not contain cycles`);
433
- ancestors.add(value);
434
- const output = value.map((child) => cloneJsonValue(child, ancestors, label));
435
- ancestors.delete(value);
1290
+ if (Array.isArray(value))
1291
+ return cloneJsonArray(value, ancestors, label, depth, maxDepth, byteBudget);
1292
+ if (isPlainRecord(value))
1293
+ return cloneJsonRecord(value, ancestors, label, depth, maxDepth, byteBudget);
1294
+ throw new IncompatibleServerPluginError(`${label} must contain only JSON values`);
1295
+ }
1296
+ function cloneJsonArray(value, ancestors, label, depth, maxDepth, byteBudget) {
1297
+ requireJsonDepth(depth, maxDepth, label);
1298
+ if (ancestors.has(value))
1299
+ throw new IncompatibleServerPluginError(`${label} must not contain cycles`);
1300
+ consumeJsonBytes(byteBudget, 2); // Opening and closing brackets.
1301
+ ancestors.add(value);
1302
+ try {
1303
+ const output = [];
1304
+ const length = value.length;
1305
+ // Plugin-owned arrays may override iteration helpers; inspect each dense element directly.
1306
+ for (let index = 0; index < length; index += 1) {
1307
+ if (!Object.hasOwn(value, index)) {
1308
+ throw new IncompatibleServerPluginError(`${label} must not contain sparse arrays`);
1309
+ }
1310
+ if (index > 0)
1311
+ consumeJsonBytes(byteBudget, 1); // Comma.
1312
+ output[index] = cloneJsonValue(value[index], ancestors, label, depth + 1, maxDepth, byteBudget);
1313
+ }
436
1314
  return Object.freeze(output);
437
1315
  }
438
- if (isRecord(value))
439
- return cloneJsonRecord(value, ancestors, label);
440
- throw new IncompatibleServerPluginError(`${label} must contain only JSON values`);
1316
+ finally {
1317
+ ancestors.delete(value);
1318
+ }
1319
+ }
1320
+ function consumeJsonStringBytes(byteBudget, value) {
1321
+ if (byteBudget === undefined)
1322
+ return;
1323
+ consumeJsonBytes(byteBudget, 2); // Opening and closing quotes.
1324
+ for (let index = 0; index < value.length; index += 1) {
1325
+ const codeUnit = value.charCodeAt(index);
1326
+ if (codeUnit === 0x22 || codeUnit === 0x5c
1327
+ || codeUnit === 0x08 || codeUnit === 0x09 || codeUnit === 0x0a
1328
+ || codeUnit === 0x0c || codeUnit === 0x0d) {
1329
+ consumeJsonBytes(byteBudget, 2);
1330
+ }
1331
+ else if (codeUnit <= 0x1f) {
1332
+ consumeJsonBytes(byteBudget, 6);
1333
+ }
1334
+ else if (codeUnit <= 0x7f) {
1335
+ consumeJsonBytes(byteBudget, 1);
1336
+ }
1337
+ else if (codeUnit <= 0x7ff) {
1338
+ consumeJsonBytes(byteBudget, 2);
1339
+ }
1340
+ else if (codeUnit >= 0xd800 && codeUnit <= 0xdbff
1341
+ && index + 1 < value.length
1342
+ && value.charCodeAt(index + 1) >= 0xdc00
1343
+ && value.charCodeAt(index + 1) <= 0xdfff) {
1344
+ consumeJsonBytes(byteBudget, 4);
1345
+ index += 1;
1346
+ }
1347
+ else if (codeUnit >= 0xd800 && codeUnit <= 0xdfff) {
1348
+ // Well-formed JSON.stringify escapes isolated UTF-16 surrogates as \uXXXX.
1349
+ consumeJsonBytes(byteBudget, 6);
1350
+ }
1351
+ else {
1352
+ consumeJsonBytes(byteBudget, 3);
1353
+ }
1354
+ }
1355
+ }
1356
+ function consumeJsonBytes(byteBudget, count) {
1357
+ if (byteBudget === undefined)
1358
+ return;
1359
+ byteBudget.remaining -= count;
1360
+ if (byteBudget.remaining < 0) {
1361
+ throw new Error(`${byteBudget.label} exceeds the ${String(byteBudget.maxBytes)} byte limit`);
1362
+ }
1363
+ }
1364
+ function requireJsonDepth(depth, maxDepth, label) {
1365
+ if (maxDepth !== undefined && depth > maxDepth) {
1366
+ throw new IncompatibleServerPluginError(`${label} exceeds the maximum JSON depth of ${String(maxDepth)}`);
1367
+ }
1368
+ }
1369
+ function defineJsonProperty(record, key, value) {
1370
+ Object.defineProperty(record, key, { value, enumerable: true, configurable: true, writable: true });
441
1371
  }
442
1372
  function abortError(signal) {
443
1373
  const reason = signal.reason;
@@ -449,6 +1379,12 @@ class IncompatibleServerPluginError extends Error {
449
1379
  this.name = "IncompatibleServerPluginError";
450
1380
  }
451
1381
  }
1382
+ export class RequiredTerminalPluginError extends Error {
1383
+ constructor() {
1384
+ super(...arguments);
1385
+ this.name = "RequiredTerminalPluginError";
1386
+ }
1387
+ }
452
1388
  class ServerPluginTimeoutError extends Error {
453
1389
  constructor() {
454
1390
  super(...arguments);
@@ -477,4 +1413,10 @@ function formatUnknown(value) {
477
1413
  function isRecord(value) {
478
1414
  return typeof value === "object" && value !== null && !Array.isArray(value);
479
1415
  }
1416
+ function isPlainRecord(value) {
1417
+ if (!isRecord(value))
1418
+ return false;
1419
+ const prototype = Object.getPrototypeOf(value);
1420
+ return prototype === Object.prototype || prototype === null;
1421
+ }
480
1422
  //# sourceMappingURL=serverPluginRuntime.js.map