@harperfast/harper 5.2.0-beta.4 → 5.2.1

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 (320) hide show
  1. package/bin/backup.ts +169 -0
  2. package/bin/cliOperations.ts +92 -73
  3. package/bin/copyDb.ts +21 -4
  4. package/bin/harper.ts +43 -55
  5. package/bin/help.ts +216 -0
  6. package/components/Application.ts +236 -46
  7. package/components/ApplicationScope.ts +26 -0
  8. package/components/EntryHandler.ts +410 -105
  9. package/components/RuntimeModuleTracker.ts +189 -0
  10. package/components/Scope.ts +120 -30
  11. package/components/componentLoader.ts +177 -25
  12. package/components/deployLifecycle.ts +119 -33
  13. package/components/mcp/toolRegistry.ts +10 -0
  14. package/components/mcp/tools/application.ts +12 -5
  15. package/components/mcp/tools/operations.ts +9 -0
  16. package/components/mcp/tools/schemas/operationDescriptions.ts +3 -0
  17. package/components/mcp/tools/schemas/operations.ts +9 -0
  18. package/components/operations.js +5 -6
  19. package/components/operationsValidation.js +32 -2
  20. package/components/scopeMount.ts +150 -0
  21. package/config/configUtils.ts +12 -9
  22. package/config-root.schema.json +14 -0
  23. package/dataLayer/backupManifest.ts +102 -0
  24. package/dataLayer/blobBackup.ts +286 -0
  25. package/dataLayer/harperBridge/ResourceBridge.ts +69 -5
  26. package/dataLayer/hdbInfoController.ts +8 -0
  27. package/dataLayer/restoreMarker.ts +276 -0
  28. package/dataLayer/rocksdbBackup.ts +1100 -0
  29. package/dataLayer/schemaDescribe.ts +2 -1
  30. package/dist/bin/backup.d.ts +9 -0
  31. package/dist/bin/backup.js +192 -0
  32. package/dist/bin/backup.js.map +1 -0
  33. package/dist/bin/cliOperations.d.ts +13 -0
  34. package/dist/bin/cliOperations.js +89 -70
  35. package/dist/bin/cliOperations.js.map +1 -1
  36. package/dist/bin/copyDb.js +13 -2
  37. package/dist/bin/copyDb.js.map +1 -1
  38. package/dist/bin/harper.d.ts +13 -0
  39. package/dist/bin/harper.js +45 -56
  40. package/dist/bin/harper.js.map +1 -1
  41. package/dist/bin/help.d.ts +8 -0
  42. package/dist/bin/help.js +192 -0
  43. package/dist/bin/help.js.map +1 -0
  44. package/dist/components/Application.d.ts +16 -1
  45. package/dist/components/Application.js +210 -38
  46. package/dist/components/Application.js.map +1 -1
  47. package/dist/components/ApplicationScope.d.ts +7 -0
  48. package/dist/components/ApplicationScope.js +22 -0
  49. package/dist/components/ApplicationScope.js.map +1 -1
  50. package/dist/components/EntryHandler.d.ts +4 -4
  51. package/dist/components/EntryHandler.js +386 -95
  52. package/dist/components/EntryHandler.js.map +1 -1
  53. package/dist/components/RuntimeModuleTracker.d.ts +11 -0
  54. package/dist/components/RuntimeModuleTracker.js +189 -0
  55. package/dist/components/RuntimeModuleTracker.js.map +1 -0
  56. package/dist/components/Scope.d.ts +38 -1
  57. package/dist/components/Scope.js +117 -28
  58. package/dist/components/Scope.js.map +1 -1
  59. package/dist/components/componentLoader.d.ts +2 -6
  60. package/dist/components/componentLoader.js +160 -18
  61. package/dist/components/componentLoader.js.map +1 -1
  62. package/dist/components/deployLifecycle.d.ts +6 -2
  63. package/dist/components/deployLifecycle.js +109 -31
  64. package/dist/components/deployLifecycle.js.map +1 -1
  65. package/dist/components/mcp/toolRegistry.d.ts +7 -0
  66. package/dist/components/mcp/toolRegistry.js +10 -0
  67. package/dist/components/mcp/toolRegistry.js.map +1 -1
  68. package/dist/components/mcp/tools/application.js +11 -5
  69. package/dist/components/mcp/tools/application.js.map +1 -1
  70. package/dist/components/mcp/tools/operations.js +9 -0
  71. package/dist/components/mcp/tools/operations.js.map +1 -1
  72. package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -0
  73. package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
  74. package/dist/components/mcp/tools/schemas/operations.js +9 -0
  75. package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
  76. package/dist/components/operations.js +6 -6
  77. package/dist/components/operations.js.map +1 -1
  78. package/dist/components/operationsValidation.js +34 -2
  79. package/dist/components/operationsValidation.js.map +1 -1
  80. package/dist/components/scopeMount.d.ts +86 -0
  81. package/dist/components/scopeMount.js +131 -0
  82. package/dist/components/scopeMount.js.map +1 -0
  83. package/dist/config/configUtils.js +13 -9
  84. package/dist/config/configUtils.js.map +1 -1
  85. package/dist/dataLayer/backupManifest.d.ts +26 -0
  86. package/dist/dataLayer/backupManifest.js +97 -0
  87. package/dist/dataLayer/backupManifest.js.map +1 -0
  88. package/dist/dataLayer/blobBackup.d.ts +87 -0
  89. package/dist/dataLayer/blobBackup.js +282 -0
  90. package/dist/dataLayer/blobBackup.js.map +1 -0
  91. package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +3 -0
  92. package/dist/dataLayer/harperBridge/ResourceBridge.js +45 -5
  93. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  94. package/dist/dataLayer/hdbInfoController.js +4 -0
  95. package/dist/dataLayer/hdbInfoController.js.map +1 -1
  96. package/dist/dataLayer/restoreMarker.d.ts +122 -0
  97. package/dist/dataLayer/restoreMarker.js +261 -0
  98. package/dist/dataLayer/restoreMarker.js.map +1 -0
  99. package/dist/dataLayer/rocksdbBackup.d.ts +127 -0
  100. package/dist/dataLayer/rocksdbBackup.js +1039 -0
  101. package/dist/dataLayer/rocksdbBackup.js.map +1 -0
  102. package/dist/dataLayer/schemaDescribe.js +2 -1
  103. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  104. package/dist/resources/DatabaseTransaction.d.ts +55 -0
  105. package/dist/resources/DatabaseTransaction.js +282 -109
  106. package/dist/resources/DatabaseTransaction.js.map +1 -1
  107. package/dist/resources/ResourceInterface.d.ts +8 -5
  108. package/dist/resources/ResourceInterface.js.map +1 -1
  109. package/dist/resources/Resources.js +22 -4
  110. package/dist/resources/Resources.js.map +1 -1
  111. package/dist/resources/Table.d.ts +6 -5
  112. package/dist/resources/Table.js +114 -37
  113. package/dist/resources/Table.js.map +1 -1
  114. package/dist/resources/analytics/write.js +6 -6
  115. package/dist/resources/analytics/write.js.map +1 -1
  116. package/dist/resources/blob.d.ts +8 -1
  117. package/dist/resources/blob.js +31 -14
  118. package/dist/resources/blob.js.map +1 -1
  119. package/dist/resources/databases.d.ts +59 -1
  120. package/dist/resources/databases.js +386 -41
  121. package/dist/resources/databases.js.map +1 -1
  122. package/dist/resources/jsResource.d.ts +4 -26
  123. package/dist/resources/jsResource.js +5 -59
  124. package/dist/resources/jsResource.js.map +1 -1
  125. package/dist/resources/models/Models.d.ts +11 -1
  126. package/dist/resources/models/Models.js +10 -1
  127. package/dist/resources/models/Models.js.map +1 -1
  128. package/dist/resources/models/backendRegistry.d.ts +9 -0
  129. package/dist/resources/models/backendRegistry.js +10 -0
  130. package/dist/resources/models/backendRegistry.js.map +1 -1
  131. package/dist/resources/models/openaiStream.d.ts +16 -1
  132. package/dist/resources/models/openaiStream.js +113 -21
  133. package/dist/resources/models/openaiStream.js.map +1 -1
  134. package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
  135. package/dist/resources/models/v1/chatCompletions.js +115 -0
  136. package/dist/resources/models/v1/chatCompletions.js.map +1 -0
  137. package/dist/resources/models/v1/embeddings.d.ts +11 -0
  138. package/dist/resources/models/v1/embeddings.js +71 -0
  139. package/dist/resources/models/v1/embeddings.js.map +1 -0
  140. package/dist/resources/models/v1/errors.d.ts +54 -0
  141. package/dist/resources/models/v1/errors.js +130 -0
  142. package/dist/resources/models/v1/errors.js.map +1 -0
  143. package/dist/resources/models/v1/index.d.ts +36 -0
  144. package/dist/resources/models/v1/index.js +75 -0
  145. package/dist/resources/models/v1/index.js.map +1 -0
  146. package/dist/resources/models/v1/models.d.ts +26 -0
  147. package/dist/resources/models/v1/models.js +44 -0
  148. package/dist/resources/models/v1/models.js.map +1 -0
  149. package/dist/resources/models/v1/translation.d.ts +133 -0
  150. package/dist/resources/models/v1/translation.js +298 -0
  151. package/dist/resources/models/v1/translation.js.map +1 -0
  152. package/dist/resources/roles.d.ts +1 -1
  153. package/dist/resources/roles.js +54 -7
  154. package/dist/resources/roles.js.map +1 -1
  155. package/dist/resources/transaction.js +0 -3
  156. package/dist/resources/transaction.js.map +1 -1
  157. package/dist/security/jsLoader.js +84 -33
  158. package/dist/security/jsLoader.js.map +1 -1
  159. package/dist/security/role.js +4 -0
  160. package/dist/security/role.js.map +1 -1
  161. package/dist/security/superUserGuard.d.ts +7 -0
  162. package/dist/security/superUserGuard.js +23 -0
  163. package/dist/security/superUserGuard.js.map +1 -0
  164. package/dist/security/tokenAuthentication.d.ts +0 -1
  165. package/dist/security/tokenAuthentication.js +6 -2
  166. package/dist/security/tokenAuthentication.js.map +1 -1
  167. package/dist/security/user.d.ts +6 -1
  168. package/dist/security/user.js +23 -1
  169. package/dist/security/user.js.map +1 -1
  170. package/dist/server/REST.js +25 -9
  171. package/dist/server/REST.js.map +1 -1
  172. package/dist/server/fastifyRoutes.js +15 -1
  173. package/dist/server/fastifyRoutes.js.map +1 -1
  174. package/dist/server/http.d.ts +23 -2
  175. package/dist/server/http.js +99 -12
  176. package/dist/server/http.js.map +1 -1
  177. package/dist/server/itc/serverHandlers.js +7 -1
  178. package/dist/server/itc/serverHandlers.js.map +1 -1
  179. package/dist/server/jobs/jobProcess.js +20 -1
  180. package/dist/server/jobs/jobProcess.js.map +1 -1
  181. package/dist/server/jobs/jobRunner.js +10 -0
  182. package/dist/server/jobs/jobRunner.js.map +1 -1
  183. package/dist/server/jobs/jobs.js +11 -0
  184. package/dist/server/jobs/jobs.js.map +1 -1
  185. package/dist/server/middlewareChain.d.ts +10 -1
  186. package/dist/server/middlewareChain.js +81 -21
  187. package/dist/server/middlewareChain.js.map +1 -1
  188. package/dist/server/operationsServer.d.ts +0 -2
  189. package/dist/server/operationsServer.js.map +1 -1
  190. package/dist/server/serverHelpers/multipartParser.js +9 -0
  191. package/dist/server/serverHelpers/multipartParser.js.map +1 -1
  192. package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
  193. package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
  194. package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
  195. package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
  196. package/dist/server/serverHelpers/registeredOperations.js +14 -13
  197. package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
  198. package/dist/server/serverHelpers/serverHandlers.js +27 -7
  199. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  200. package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
  201. package/dist/server/serverHelpers/serverUtilities.js +21 -7
  202. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  203. package/dist/server/static.js +68 -21
  204. package/dist/server/static.js.map +1 -1
  205. package/dist/server/storageReclamation.d.ts +17 -0
  206. package/dist/server/storageReclamation.js +90 -6
  207. package/dist/server/storageReclamation.js.map +1 -1
  208. package/dist/server/threads/socketRouter.js +20 -0
  209. package/dist/server/threads/socketRouter.js.map +1 -1
  210. package/dist/server/threads/threadServer.js +12 -0
  211. package/dist/server/threads/threadServer.js.map +1 -1
  212. package/dist/sqlEngine/diff/differential.js +7 -3
  213. package/dist/sqlEngine/diff/differential.js.map +1 -1
  214. package/dist/sqlTranslator/index.js +6 -1
  215. package/dist/sqlTranslator/index.js.map +1 -1
  216. package/dist/upgrade/upgradePrompt.d.ts +2 -2
  217. package/dist/upgrade/upgradePrompt.js +22 -3
  218. package/dist/upgrade/upgradePrompt.js.map +1 -1
  219. package/dist/utility/OperationFunctionCaller.js +25 -6
  220. package/dist/utility/OperationFunctionCaller.js.map +1 -1
  221. package/dist/utility/errors/commonErrors.d.ts +1 -0
  222. package/dist/utility/errors/commonErrors.js +1 -0
  223. package/dist/utility/errors/commonErrors.js.map +1 -1
  224. package/dist/utility/hdbTerms.d.ts +13 -1
  225. package/dist/utility/hdbTerms.js +13 -1
  226. package/dist/utility/hdbTerms.js.map +1 -1
  227. package/dist/utility/install/installer.js +32 -0
  228. package/dist/utility/install/installer.js.map +1 -1
  229. package/dist/utility/logging/harper_logger.d.ts +47 -0
  230. package/dist/utility/logging/harper_logger.js +803 -38
  231. package/dist/utility/logging/harper_logger.js.map +1 -1
  232. package/dist/utility/operation_authorization.js +13 -3
  233. package/dist/utility/operation_authorization.js.map +1 -1
  234. package/npm-shrinkwrap.json +198 -193
  235. package/package.json +12 -7
  236. package/resources/DESIGN.md +2 -0
  237. package/resources/DatabaseTransaction.ts +310 -104
  238. package/resources/ResourceInterface.ts +8 -5
  239. package/resources/Resources.ts +22 -4
  240. package/resources/Table.ts +341 -263
  241. package/resources/analytics/write.ts +22 -20
  242. package/resources/blob.ts +31 -14
  243. package/resources/databases.ts +387 -46
  244. package/resources/jsResource.ts +5 -62
  245. package/resources/models/Models.ts +14 -1
  246. package/resources/models/backendRegistry.ts +10 -0
  247. package/resources/models/openaiStream.ts +131 -19
  248. package/resources/models/v1/chatCompletions.ts +128 -0
  249. package/resources/models/v1/embeddings.ts +70 -0
  250. package/resources/models/v1/errors.ts +141 -0
  251. package/resources/models/v1/index.ts +72 -0
  252. package/resources/models/v1/models.ts +53 -0
  253. package/resources/models/v1/translation.ts +362 -0
  254. package/resources/roles.ts +67 -7
  255. package/resources/transaction.ts +0 -3
  256. package/security/jsLoader.ts +84 -30
  257. package/security/role.ts +7 -0
  258. package/security/superUserGuard.ts +20 -0
  259. package/security/tokenAuthentication.ts +6 -3
  260. package/security/user.ts +26 -1
  261. package/server/DESIGN.md +61 -34
  262. package/server/REST.ts +25 -9
  263. package/server/fastifyRoutes.ts +20 -1
  264. package/server/http.ts +100 -13
  265. package/server/itc/serverHandlers.js +7 -1
  266. package/server/jobs/jobProcess.ts +18 -1
  267. package/server/jobs/jobRunner.ts +10 -0
  268. package/server/jobs/jobs.ts +11 -0
  269. package/server/middlewareChain.ts +79 -20
  270. package/server/operationsServer.ts +0 -2
  271. package/server/serverHelpers/multipartParser.ts +9 -0
  272. package/server/serverHelpers/operationAuthorizationState.ts +11 -0
  273. package/server/serverHelpers/registeredOperations.ts +19 -15
  274. package/server/serverHelpers/serverHandlers.js +28 -7
  275. package/server/serverHelpers/serverUtilities.ts +29 -7
  276. package/server/static.ts +87 -23
  277. package/server/storageReclamation.ts +104 -8
  278. package/server/threads/socketRouter.ts +20 -0
  279. package/server/threads/threadServer.js +11 -0
  280. package/sqlTranslator/index.ts +6 -1
  281. package/static/defaultConfig.yaml +3 -0
  282. package/studio/web/assets/{Chat-DK3GlWEb.js → Chat-aApwhRmz.js} +2 -2
  283. package/studio/web/assets/{Chat-DK3GlWEb.js.map → Chat-aApwhRmz.js.map} +1 -1
  284. package/studio/web/assets/{FloatingChat-fBcC1Ew_.js → FloatingChat-sC0H91n9.js} +4 -4
  285. package/studio/web/assets/{FloatingChat-fBcC1Ew_.js.map → FloatingChat-sC0H91n9.js.map} +1 -1
  286. package/studio/web/assets/{apiToken-DJo1nakA.js → apiToken-DZ8JhHOI.js} +2 -2
  287. package/studio/web/assets/{apiToken-DJo1nakA.js.map → apiToken-DZ8JhHOI.js.map} +1 -1
  288. package/studio/web/assets/{applications-BDfH8urd.js → applications-9SEVLIO5.js} +2 -2
  289. package/studio/web/assets/{applications-BDfH8urd.js.map → applications-9SEVLIO5.js.map} +1 -1
  290. package/studio/web/assets/{index-BHo3c2Gk.js → index-Dy3uDGXb.js} +6 -6
  291. package/studio/web/assets/index-Dy3uDGXb.js.map +1 -0
  292. package/studio/web/assets/{index.lazy-BTo0y6UM.js → index.lazy-CN1zq4I4.js} +4 -4
  293. package/studio/web/assets/{index.lazy-BTo0y6UM.js.map → index.lazy-CN1zq4I4.js.map} +1 -1
  294. package/studio/web/assets/{notifications-CMxvWNnz.js → notifications-BFAF07xr.js} +2 -2
  295. package/studio/web/assets/{notifications-CMxvWNnz.js.map → notifications-BFAF07xr.js.map} +1 -1
  296. package/studio/web/assets/{notifications-D3GoB26g.js → notifications-CKlYVvVN.js} +2 -2
  297. package/studio/web/assets/{notifications-D3GoB26g.js.map → notifications-CKlYVvVN.js.map} +1 -1
  298. package/studio/web/assets/{profile-Doj5FVDE.js → profile-A1zhEdFG.js} +2 -2
  299. package/studio/web/assets/{profile-Doj5FVDE.js.map → profile-A1zhEdFG.js.map} +1 -1
  300. package/studio/web/assets/{setComponentFile-yinsqJy0.js → setComponentFile-SEtBt_GV.js} +2 -2
  301. package/studio/web/assets/{setComponentFile-yinsqJy0.js.map → setComponentFile-SEtBt_GV.js.map} +1 -1
  302. package/studio/web/assets/{setup-DJwR0BHd.js → setup-BisINqdH.js} +2 -2
  303. package/studio/web/assets/{setup-DJwR0BHd.js.map → setup-BisINqdH.js.map} +1 -1
  304. package/studio/web/assets/{status-Br_AbsJs.js → status-BuQoCc7l.js} +2 -2
  305. package/studio/web/assets/{status-Br_AbsJs.js.map → status-BuQoCc7l.js.map} +1 -1
  306. package/studio/web/assets/{swagger-ui-react-02XH5sVf.js → swagger-ui-react-CAi_s1PC.js} +2 -2
  307. package/studio/web/assets/{swagger-ui-react-02XH5sVf.js.map → swagger-ui-react-CAi_s1PC.js.map} +1 -1
  308. package/studio/web/assets/{tsMode-D1DMKY7h.js → tsMode-DT74tlkM.js} +2 -2
  309. package/studio/web/assets/{tsMode-D1DMKY7h.js.map → tsMode-DT74tlkM.js.map} +1 -1
  310. package/studio/web/assets/{useEntityRestURL-DtRblRgw.js → useEntityRestURL-CGRGc1n7.js} +2 -2
  311. package/studio/web/assets/{useEntityRestURL-DtRblRgw.js.map → useEntityRestURL-CGRGc1n7.js.map} +1 -1
  312. package/studio/web/index.html +1 -1
  313. package/upgrade/upgradePrompt.ts +22 -3
  314. package/utility/OperationFunctionCaller.ts +24 -3
  315. package/utility/errors/commonErrors.ts +2 -0
  316. package/utility/hdbTerms.ts +13 -1
  317. package/utility/install/installer.ts +37 -0
  318. package/utility/logging/harper_logger.ts +786 -41
  319. package/utility/operation_authorization.ts +31 -3
  320. package/studio/web/assets/index-BHo3c2Gk.js.map +0 -1
@@ -29,6 +29,8 @@ import * as scheduler from '../resources/scheduler/scheduler.ts';
29
29
  import { restartWorkers, getWorkerIndex } from '../server/threads/manageThreads.js';
30
30
  import { resetRestartNeeded, subscribeToRestartRequests } from './requestRestart.ts';
31
31
  import { trackScopeClose } from './scopeShutdown.ts';
32
+ import { deployLifecycle } from './deployLifecycle.ts';
33
+ import { toScopeMount, nestScopeMount, type ScopeMount } from './scopeMount.ts';
32
34
  import { scopedImport } from '../security/jsLoader.ts';
33
35
  import { server } from '../server/Server.ts';
34
36
  import { Resources } from '../resources/Resources.ts';
@@ -63,6 +65,51 @@ let resources;
63
65
  * @param loadedPluginModules
64
66
  * @param loadedResources
65
67
  */
68
+ /**
69
+ * The application mount an operator declared for `appName` in the root config.
70
+ *
71
+ * Applications in the components root are loaded by directory scan, not from a root-config
72
+ * entry, so this is what makes a root-config entry authoritative for where an app is served:
73
+ *
74
+ * ```yaml
75
+ * my-app:
76
+ * host: api.example.com
77
+ * urlPath: /v1
78
+ * ```
79
+ *
80
+ * Works whether or not the entry also carries `package` — a payload-deployed app is mounted the
81
+ * same way as an installed one. A built-in plugin's config block is never an application mount;
82
+ * those keys (`http`, `mqtt`, …) configure the plugin itself.
83
+ */
84
+ function rootConfigMount(appName: string): ScopeMount | undefined {
85
+ if (Object.hasOwn(TRUSTED_RESOURCE_PLUGINS, appName)) return undefined;
86
+ return toScopeMount(getConfigObj()?.[appName]);
87
+ }
88
+
89
+ /**
90
+ * Resolves the mount for `appName`, or reports the failure and returns `undefined` if the
91
+ * configured `host`/`urlPath` is invalid. The caller must skip loading the application in that
92
+ * case rather than loading it anyway: an invalid mount is a request to CONSTRAIN where the app is
93
+ * served, so loading it unconstrained would silently drop the isolation the operator configured
94
+ * (review finding) — worse than not loading it at all. Isolated per-app so one bad mount doesn't
95
+ * take down every other application's load.
96
+ */
97
+ function tryRootConfigMount(appName: string): { ok: true; mount: ScopeMount | undefined } | { ok: false } {
98
+ try {
99
+ return { ok: true, mount: rootConfigMount(appName) };
100
+ } catch (error) {
101
+ (error as Error).message = `Not loading '${appName}': invalid routing configured: ${(error as Error).message}`;
102
+ errorReporter?.(error);
103
+ (getWorkerIndex() === 0 ? console : harperLogger).error(errorForLog(error as Error));
104
+ componentLifecycle.failed(
105
+ appName,
106
+ error as Error,
107
+ `Component '${appName}' failed to load due to invalid routing configuration`
108
+ );
109
+ return { ok: false };
110
+ }
111
+ }
112
+
66
113
  export async function loadComponentDirectories(loadedPluginModules?: Map<any, any>, loadedResources?: Resources) {
67
114
  if (loadedResources) resources = loadedResources;
68
115
  if (loadedPluginModules) loadedComponents = loadedPluginModules;
@@ -81,21 +128,32 @@ export async function loadComponentDirectories(loadedPluginModules?: Map<any, an
81
128
  if (appEntry.name.startsWith('.')) continue;
82
129
  const appName = appEntry.name;
83
130
  const appFolder = join(CF_ROUTES_DIR, appName);
131
+ const mountResult = tryRootConfigMount(appName);
132
+ if (!mountResult.ok) continue;
84
133
  cfsLoaded.push(
85
- loadComponent(appFolder, resources, HDB_ROOT_DIR_NAME, { isRoot: false, autoReload: false, appName })
134
+ loadComponent(appFolder, resources, HDB_ROOT_DIR_NAME, {
135
+ isRoot: false,
136
+ autoReload: false,
137
+ appName,
138
+ mount: mountResult.mount,
139
+ })
86
140
  );
87
141
  }
88
142
  }
89
143
  const hdbAppFolder = process.env.RUN_HDB_APP;
90
144
  if (hdbAppFolder) {
91
145
  if (getWorkerIndex() === 0) harperLogger.info?.('Loading application from ' + hdbAppFolder);
92
- cfsLoaded.push(
93
- loadComponent(hdbAppFolder, resources, hdbAppFolder, {
94
- isRoot: false,
95
- autoReload: Boolean(process.env.DEV_MODE),
96
- appName: hdbAppFolder,
97
- })
98
- );
146
+ const mountResult = tryRootConfigMount(basename(hdbAppFolder));
147
+ if (mountResult.ok) {
148
+ cfsLoaded.push(
149
+ loadComponent(hdbAppFolder, resources, hdbAppFolder, {
150
+ isRoot: false,
151
+ autoReload: Boolean(process.env.DEV_MODE),
152
+ appName: hdbAppFolder,
153
+ mount: mountResult.mount,
154
+ })
155
+ );
156
+ }
99
157
  }
100
158
  return Promise.all(cfsLoaded).then(() => {
101
159
  watchesSetup = true;
@@ -113,6 +171,16 @@ export const TRUSTED_RESOURCE_PLUGINS: any = {
113
171
  return require('../server/fastifyRoutes');
114
172
  },
115
173
  login,
174
+ // String entry: the loader `await import()`s these lazily when the component is actually
175
+ // processed, so the gateway's module graph is not pulled into componentLoader's own
176
+ // evaluation. `#src/*` is used rather than a relative path because it resolves under both
177
+ // conditions (source under --conditions=typestrip, dist otherwise); a relative extensionless
178
+ // require would only resolve against dist.
179
+ //
180
+ // The component is only *processed* when a `modelsGateway` key exists in the config, since
181
+ // the root loop skips absent keys (`if (!componentConfig) continue`). `defaultConfig.yaml`
182
+ // ships no such key, so an instance that never opts in pays nothing for this entry.
183
+ modelsGateway: '#src/resources/models/v1/index',
116
184
  static: staticFiles,
117
185
  customFunctions: {},
118
186
  http: httpComponent,
@@ -255,6 +323,7 @@ function symlinkHarperModule(componentDirectory: string) {
255
323
  */
256
324
  function sequentiallyHandleApplication(scope: Scope, plugin: PluginModule) {
257
325
  return scope.ready.then(async () => {
326
+ await scope.waitForDeployCompletion();
258
327
  // Timeout priority is user config, plugin default, finally 30 seconds
259
328
  const timeout = scope.options.get(['timeout']) || plugin.defaultTimeout || 30_000; // default 30 second timeout
260
329
  if (typeof timeout !== 'number') {
@@ -276,35 +345,76 @@ function sequentiallyHandleApplication(scope: Scope, plugin: PluginModule) {
276
345
  }, timeout + 5_000); // extra time for lock acquisition
277
346
  });
278
347
  }
279
- let loadTimeout: NodeJS.Timeout;
280
348
  try {
281
349
  // note that handleApplication can throw sync or async errors, need to run finally block for both
282
- await Promise.race([
350
+ await withDeployAwareTimeout(
283
351
  Promise.resolve(plugin.handleApplication(scope)).then(async () => {
284
352
  // Wait for any initial entry handler loads to complete
285
353
  // This ensures all async operations (like secureImport) finish before the component is marked as loaded
286
354
  await scope.waitForInitialLoads();
287
355
  }),
288
- new Promise(
289
- (_, reject) =>
290
- (loadTimeout = setTimeout(
291
- () =>
292
- reject(
293
- new Error(
294
- `handleApplication timed out after ${timeout}ms for ${scope.pluginName} on behalf of ${scope.appName}`
295
- )
296
- ),
297
- timeout
298
- ))
299
- ),
300
- ]);
356
+ scope,
357
+ timeout
358
+ );
301
359
  } finally {
302
360
  Status.primaryStore.unlock(scope.pluginName);
303
- clearTimeout(loadTimeout);
304
361
  }
305
362
  });
306
363
  }
307
364
 
365
+ function withDeployAwareTimeout<T>(operation: Promise<T>, scope: Scope, timeout: number): Promise<T> {
366
+ return new Promise((resolve, reject) => {
367
+ const absoluteTimeout = timeout + 6 * 60 * 60 * 1000;
368
+ let remaining = timeout;
369
+ let activeSince = 0;
370
+ let timer: NodeJS.Timeout | undefined;
371
+ let absoluteTimer: NodeJS.Timeout;
372
+ const cleanup = () => {
373
+ if (timer) clearTimeout(timer);
374
+ clearTimeout(absoluteTimer);
375
+ deployLifecycle.off('deploy:start', handleDeployStart);
376
+ deployLifecycle.off('deploy:end', handleDeployEnd);
377
+ };
378
+ const rejectTimeout = (limit = timeout) => {
379
+ cleanup();
380
+ reject(
381
+ new Error(`handleApplication timed out after ${limit}ms for ${scope.pluginName} on behalf of ${scope.appName}`)
382
+ );
383
+ };
384
+ absoluteTimer = setTimeout(() => rejectTimeout(absoluteTimeout), absoluteTimeout);
385
+ absoluteTimer.unref?.();
386
+ const arm = () => {
387
+ if (deployLifecycle.isDeployInFlight(scope.appName)) return;
388
+ if (remaining <= 0) return rejectTimeout();
389
+ activeSince = Date.now();
390
+ timer = setTimeout(rejectTimeout, remaining);
391
+ };
392
+ function handleDeployStart(componentName: string) {
393
+ if (componentName !== scope.appName || !timer) return;
394
+ remaining -= Date.now() - activeSince;
395
+ clearTimeout(timer);
396
+ timer = undefined;
397
+ }
398
+ function handleDeployEnd(componentName: string) {
399
+ if (componentName === scope.appName) arm();
400
+ }
401
+
402
+ deployLifecycle.on('deploy:start', handleDeployStart);
403
+ deployLifecycle.on('deploy:end', handleDeployEnd);
404
+ operation.then(
405
+ (value) => {
406
+ cleanup();
407
+ resolve(value);
408
+ },
409
+ (error) => {
410
+ cleanup();
411
+ reject(error);
412
+ }
413
+ );
414
+ arm();
415
+ });
416
+ }
417
+
308
418
  export interface LoadComponentOptions {
309
419
  isRoot?: boolean;
310
420
  applicationScope?: ApplicationScope;
@@ -316,6 +426,10 @@ export interface LoadComponentOptions {
316
426
  // (e.g. the deploy pre-flight validation) so their deploy-lifecycle listeners don't accumulate
317
427
  // across deploys (#1462).
318
428
  collectScopes?: Set<Scope>;
429
+ // Routing the operator declared for this application in the root config (`host`/`urlPath` on
430
+ // the application's entry). Applied to every plugin scope this load creates, and inherited by
431
+ // components the application itself declares, so the whole subtree moves together.
432
+ mount?: ScopeMount;
319
433
  }
320
434
 
321
435
  /**
@@ -342,7 +456,9 @@ export async function loadComponent(
342
456
  isRoot,
343
457
  autoReload,
344
458
  appName,
459
+ mount,
345
460
  } = options;
461
+ applicationScope.runtimeRoot ??= resolvedFolder;
346
462
  applicationScope.allowedPath ??= realpathSync(componentDirectory);
347
463
  if (providedLoadedComponents) loadedComponents = providedLoadedComponents;
348
464
  try {
@@ -485,6 +601,13 @@ export async function loadComponent(
485
601
  autoReload: false,
486
602
  appName: appName || componentName,
487
603
  collectScopes: options.collectScopes,
604
+ // `host`/`urlPath` on this entry route the component being loaded. For an
605
+ // application (no plugin module of its own) that entry is the only place an
606
+ // operator can say where the app is served — its own config.yaml declares the
607
+ // plugins, not the deployment. A nested component's own mount nests inside
608
+ // the parent's rather than replacing it, and the parent keeps hostname
609
+ // authority, so a child can't escape the host it is served on.
610
+ mount: nestScopeMount(mount, toScopeMount(componentConfig)),
488
611
  });
489
612
  componentFunctionality[componentName] = true;
490
613
  }
@@ -554,7 +677,10 @@ export async function loadComponent(
554
677
  // authoritative root-ness: only root-load scopes watch THE root config and
555
678
  // get the runtime env-config overlay (#1618) — an app component that happens
556
679
  // to ship a root-named config file does not
557
- isRoot
680
+ isRoot,
681
+ // A root-declared plugin reads its own `host`/`urlPath` straight from this
682
+ // config, so only an inherited application mount applies here.
683
+ mount
558
684
  );
559
685
 
560
686
  if (options.collectScopes) {
@@ -597,6 +723,32 @@ export async function loadComponent(
597
723
  );
598
724
  }
599
725
 
726
+ // `start`/`startOnMainThread` hand the plugin the raw, unmounted `server` directly —
727
+ // unlike the new Plugin API's `handleApplication(scope)`, which receives the mount-aware
728
+ // scope — so routes registered there would silently escape a configured host/urlPath
729
+ // mount and stay reachable unconstrained (review finding). Same isolation gap
730
+ // fastifyRoutes.ts already closes for its own legacy fallback (host-only there, since
731
+ // urlPath composes into its route prefix; here neither composes, so both are rejected).
732
+ // Refuse rather than imply an isolation the hook can't honor; contained to this
733
+ // component by the loader's per-component try/catch.
734
+ if (
735
+ mount &&
736
+ (typeof extensionModule.start === 'function' || typeof extensionModule.startOnMainThread === 'function')
737
+ ) {
738
+ const error = new Error(
739
+ `Component '${componentName}' is mounted (${[
740
+ mount.host && `host '${mount.host}'`,
741
+ mount.urlPath && `urlPath '${mount.urlPath}'`,
742
+ ]
743
+ .filter(Boolean)
744
+ .join(
745
+ ', '
746
+ )}), but its deprecated 'start'/'startOnMainThread' extension API receives the bare, unmounted server — routes registered there would stay reachable unconstrained. Upgrade to the new Plugin API (handleApplication(scope)) or drop the mount.`
747
+ );
748
+ componentLifecycle.failed(componentStatusName, error, `Component '${componentStatusName}' failed to load`);
749
+ throw error;
750
+ }
751
+
600
752
  if (isMainThread) {
601
753
  // `startOnMainThread` is one-time main-thread init: run it at most once per component
602
754
  // for the life of the process (first load / first deploy). On a reload of an
@@ -9,25 +9,39 @@
9
9
  //
10
10
  // This module solves that by broadcasting a structured "deploy:start" /
11
11
  // "deploy:end" signal to every Harper thread. Each thread's deploy emitter
12
- // fires locally so Scopes (and any plugin subscribers) can react: Scopes close
13
- // their EntryHandlers on start, recreate them on end. The result is that
12
+ // fires locally so Scopes (and any plugin subscribers) can react: Scopes pause
13
+ // their EntryHandlers on start and resume them on end. The result is that
14
14
  // during a deploy, no watcher fires events for the deployed component, and
15
- // after the deploy the recreated watcher emits one fresh add per current file
16
- // collapsed by the existing restart debounce into a single restart.
15
+ // after the deploy the watcher compares the new tree with its retained snapshot
16
+ // and emits the same logical add/change/unlink events consumers saw before #1806.
17
17
  //
18
- // State sharing across threads is intentionally narrow: the local Set of
19
- // in-flight component names is rebuilt from the broadcast stream. Plugins that
20
- // need to gate their own work on deploy progress import `deployLifecycle` and
21
- // listen to the events directly.
18
+ // State sharing across threads is intentionally narrow: local deployment-owner
19
+ // records are rebuilt from the broadcast stream and reclaimed when their owner
20
+ // thread exits. Plugins that need to gate their own work on deploy progress
21
+ // import `deployLifecycle` and listen to the events directly.
22
22
 
23
23
  import { EventEmitter } from 'node:events';
24
- import { isMainThread } from 'node:worker_threads';
25
- import { broadcast, broadcastWithAcknowledgement, onMessageByType } from '../server/threads/manageThreads.js';
24
+ import { randomUUID } from 'node:crypto';
25
+ import { setTimeout as delay } from 'node:timers/promises';
26
+ import { isMainThread, threadId } from 'node:worker_threads';
27
+ import {
28
+ broadcast,
29
+ broadcastWithAcknowledgement,
30
+ isThreadRunning,
31
+ onMessageByType,
32
+ onThreadExit,
33
+ } from '../server/threads/manageThreads.js';
34
+ import harperLogger from '../utility/logging/harper_logger.ts';
26
35
 
27
36
  const DEPLOY_LIFECYCLE_MSG = 'harper:deploy:lifecycle';
28
37
 
29
38
  export type DeployPhase = 'start' | 'end';
30
- export type DeployLifecycleEvent = { name: string; phase: DeployPhase };
39
+ export type DeployLifecycleEvent = {
40
+ name: string;
41
+ phase: DeployPhase;
42
+ deploymentId?: string;
43
+ ownerThreadId?: number;
44
+ };
31
45
 
32
46
  type DeployLifecycleEventsMap = {
33
47
  'deploy:start': [componentName: string];
@@ -35,44 +49,109 @@ type DeployLifecycleEventsMap = {
35
49
  };
36
50
 
37
51
  class DeployLifecycle extends EventEmitter<DeployLifecycleEventsMap> {
38
- // Ref-counts in-flight deploys per component. Concurrent deploys of the
39
- // same name (rare, but possible if an operator queues two before the first
40
- // resolves) overlap: 0→1 fires deploy:start, 1→0 fires deploy:end. Any
41
- // intermediate increment/decrement is silent so watchers aren't toggled
42
- // out from under an active deploy by a peer ending early.
43
- #inFlight = new Map<string, number>();
52
+ #deployments = new Map<string, { name: string; ownerThreadId: number }>();
53
+ #byComponent = new Map<string, Set<string>>();
54
+ #deadOwners = new Set<number>();
55
+ #legacyDeployments = new Map<string, string[]>();
56
+ #legacySequence = 0;
44
57
 
45
58
  isDeployInFlight(componentName: string): boolean {
46
- return (this.#inFlight.get(componentName) ?? 0) > 0;
59
+ return (this.#byComponent.get(componentName)?.size ?? 0) > 0;
47
60
  }
48
61
 
49
62
  // Process a deploy lifecycle event in-process. Called both from the
50
63
  // broadcast receiver (for events originating on another thread) and from
51
64
  // the broadcaster (so the originating thread also reacts locally).
52
65
  _handle(event: DeployLifecycleEvent): void {
53
- const current = this.#inFlight.get(event.name) ?? 0;
66
+ const ownerThreadId = event.ownerThreadId ?? threadId;
67
+ let deploymentId = event.deploymentId;
54
68
  if (event.phase === 'start') {
55
- this.#inFlight.set(event.name, current + 1);
56
- if (current === 0) this.emit('deploy:start', event.name);
57
- } else {
58
- const next = Math.max(0, current - 1);
59
- if (next === 0) {
60
- this.#inFlight.delete(event.name);
61
- if (current > 0) this.emit('deploy:end', event.name);
62
- } else {
63
- this.#inFlight.set(event.name, next);
69
+ if (this.#deadOwners.has(ownerThreadId)) return;
70
+ if (!deploymentId) {
71
+ deploymentId = `legacy:${threadId}:${++this.#legacySequence}`;
72
+ const legacy = this.#legacyDeployments.get(event.name) ?? [];
73
+ legacy.push(deploymentId);
74
+ this.#legacyDeployments.set(event.name, legacy);
75
+ }
76
+ if (this.#deployments.has(deploymentId)) return;
77
+ const active = this.#byComponent.get(event.name) ?? new Set<string>();
78
+ this.#deployments.set(deploymentId, { name: event.name, ownerThreadId });
79
+ this.#byComponent.set(event.name, active);
80
+ active.add(deploymentId);
81
+ if (active.size === 1) this.#emitSafely('deploy:start', event.name);
82
+ return;
83
+ }
84
+ if (!deploymentId) deploymentId = this.#legacyDeployments.get(event.name)?.pop();
85
+ if (deploymentId) this.#removeDeployment(deploymentId);
86
+ }
87
+
88
+ _reclaimOwner(ownerThreadId: number): void {
89
+ this.#deadOwners.add(ownerThreadId);
90
+ for (const [deploymentId, deployment] of this.#deployments) {
91
+ if (deployment.ownerThreadId === ownerThreadId) this.#removeDeployment(deploymentId);
92
+ }
93
+ }
94
+
95
+ async _reclaimOwnerAfterTermination(
96
+ ownerThreadId: number,
97
+ waitForTermination: (ownerThreadId: number) => Promise<void> = waitForOwnerTermination
98
+ ): Promise<void> {
99
+ if (ownerThreadId === 0 || ownerThreadId === threadId) return;
100
+ this.#deadOwners.add(ownerThreadId);
101
+ await waitForTermination(ownerThreadId);
102
+ this._reclaimOwner(ownerThreadId);
103
+ }
104
+
105
+ #removeDeployment(deploymentId: string): void {
106
+ const deployment = this.#deployments.get(deploymentId);
107
+ if (!deployment) return;
108
+ this.#deployments.delete(deploymentId);
109
+ const active = this.#byComponent.get(deployment.name);
110
+ if (!active) return;
111
+ active.delete(deploymentId);
112
+ if (active.size === 0) {
113
+ this.#byComponent.delete(deployment.name);
114
+ this.#emitSafely('deploy:end', deployment.name);
115
+ }
116
+ }
117
+
118
+ #emitSafely(event: 'deploy:start' | 'deploy:end', componentName: string): void {
119
+ for (const listener of this.rawListeners(event)) {
120
+ try {
121
+ listener.call(this, componentName);
122
+ } catch (error) {
123
+ harperLogger.error(`Listener for ${event} threw for ${componentName}:`, error);
64
124
  }
65
125
  }
66
126
  }
67
127
 
68
128
  // Test-only: clear in-flight state without firing events.
69
129
  _clearForTests(): void {
70
- this.#inFlight.clear();
130
+ this.#deployments.clear();
131
+ this.#byComponent.clear();
132
+ this.#deadOwners.clear();
133
+ this.#legacyDeployments.clear();
134
+ this.#legacySequence = 0;
71
135
  }
72
136
  }
73
137
 
74
138
  export const deployLifecycle = new DeployLifecycle();
75
139
 
140
+ async function waitForOwnerTermination(ownerThreadId: number): Promise<void> {
141
+ let timeoutReported = false;
142
+ for (;;) {
143
+ try {
144
+ if (!(await isThreadRunning(ownerThreadId))) return;
145
+ } catch (error) {
146
+ if (!timeoutReported) {
147
+ timeoutReported = true;
148
+ harperLogger.warn(`Retrying liveness check for exited thread ${ownerThreadId}:`, error);
149
+ }
150
+ }
151
+ await delay(25, undefined, { ref: false });
152
+ }
153
+ }
154
+
76
155
  let receiverInstalled = false;
77
156
  function ensureReceiver() {
78
157
  if (receiverInstalled) return;
@@ -88,6 +167,11 @@ function ensureReceiver() {
88
167
  // suppress only the main thread's watchers and the worker watchers would keep
89
168
  // firing restart storms (harper#488).
90
169
  ensureReceiver();
170
+ onThreadExit((deadThreadId: number) => {
171
+ void deployLifecycle
172
+ ._reclaimOwnerAfterTermination(deadThreadId)
173
+ .catch((error) => harperLogger.error(`Could not reclaim deployments from thread ${deadThreadId}:`, error));
174
+ });
91
175
 
92
176
  /**
93
177
  * Announce the start of a deploy for `componentName`. Awaits acknowledgement
@@ -98,9 +182,10 @@ ensureReceiver();
98
182
  * component compose correctly (each call must be paired with exactly one
99
183
  * broadcastDeployEnd).
100
184
  */
101
- export async function broadcastDeployStart(componentName: string): Promise<void> {
185
+ export async function broadcastDeployStart(componentName: string): Promise<string> {
102
186
  ensureReceiver();
103
- const event: DeployLifecycleEvent = { name: componentName, phase: 'start' };
187
+ const deploymentId = randomUUID();
188
+ const event: DeployLifecycleEvent = { name: componentName, phase: 'start', deploymentId, ownerThreadId: threadId };
104
189
  deployLifecycle._handle(event); // local thread first
105
190
  let timer: ReturnType<typeof setTimeout> | undefined;
106
191
  try {
@@ -125,6 +210,7 @@ export async function broadcastDeployStart(componentName: string): Promise<void>
125
210
  } finally {
126
211
  if (timer) clearTimeout(timer);
127
212
  }
213
+ return deploymentId;
128
214
  }
129
215
 
130
216
  /**
@@ -135,9 +221,9 @@ export async function broadcastDeployStart(componentName: string): Promise<void>
135
221
  * Must be called exactly once per matching broadcastDeployStart, even if the
136
222
  * deploy errored — typically from a `finally` block.
137
223
  */
138
- export function broadcastDeployEnd(componentName: string): void {
224
+ export function broadcastDeployEnd(componentName: string, deploymentId: string): void {
139
225
  ensureReceiver();
140
- const event: DeployLifecycleEvent = { name: componentName, phase: 'end' };
226
+ const event: DeployLifecycleEvent = { name: componentName, phase: 'end', deploymentId, ownerThreadId: threadId };
141
227
  deployLifecycle._handle(event);
142
228
  try {
143
229
  void broadcast({ type: DEPLOY_LIFECYCLE_MSG, event }, false);
@@ -362,6 +362,16 @@ export function isSuperUser(user: AuthedUser | undefined): boolean {
362
362
  return user?.role?.permission?.super_user === true;
363
363
  }
364
364
 
365
+ /**
366
+ * True if the request carries an authenticated principal. Anonymous MCP sessions are a supported
367
+ * deployment (the public-docs case, #1609) — the adapter hands them through as `{ username: '' }`
368
+ * rather than rejecting them — so a listing filter that means "any real user" has to test for this
369
+ * explicitly. `user?.role` is not a proxy: an authenticated user may legitimately carry no role.
370
+ */
371
+ export function isAuthenticated(user: AuthedUser | undefined): boolean {
372
+ return typeof user?.username === 'string' && user.username.length > 0;
373
+ }
374
+
365
375
  /**
366
376
  * Class-level verb introspection. Mirrors `resources/openApi.ts:149-153` so
367
377
  * a class is considered to "have" a verb only if its prototype overrides the
@@ -27,6 +27,7 @@ import harperLogger from '../../../utility/logging/harper_logger.ts';
27
27
  import {
28
28
  addTool,
29
29
  clearProfileTools,
30
+ isAuthenticated,
30
31
  snapshotProfileTools,
31
32
  type AuthedUser,
32
33
  type ToolCallContext,
@@ -708,11 +709,17 @@ function makeVisibleTo(
708
709
  return function visibleTo(user: AuthedUser): boolean {
709
710
  // Super-user sees everything.
710
711
  if (user?.role?.permission?.super_user === true) return true;
711
- // Non-table Resources have no static permission gate runtime allow*
712
- // predicates enforce. Conservative default: don't list them for
713
- // non-super users; they can still call via tools/call if they know
714
- // the tool name (pass-through is documented behavior).
715
- if (!databaseName || !tableName) return false;
712
+ // A Resource with no backing table has no static permission gate to consult, so listing is
713
+ // open to any authenticated caller and the Resource's own `allow*` predicates enforce at call
714
+ // time hiding these while still accepting `tools/call` on them bought no security, only
715
+ // undiscoverable tools (#1940).
716
+ //
717
+ // Anonymous sessions stay excluded. They are a supported deployment (#1609) and reach here as
718
+ // `{ username: '' }`, so this must be tested explicitly. Custom `mcpTools` DO list to them, but
719
+ // that is an author opt-in — declaring `static mcpTools` is a deliberate act of publishing.
720
+ // Verb tools are generated for every exported Resource with no author action, so publishing an
721
+ // app's field names and docstrings to unauthenticated callers by default is not equivalent.
722
+ if (!databaseName || !tableName) return isAuthenticated(user);
716
723
  const perm = getUserTablePermissions(user, databaseName, tableName);
717
724
  if (!perm) return false;
718
725
  if (mode === 'read') return perm.read === true || perm.describe === true;
@@ -176,6 +176,9 @@ const DESTRUCTIVE_OPERATIONS: ReadonlySet<string> = new Set([
176
176
  'restart_service',
177
177
  'set_configuration',
178
178
  'remove_node',
179
+ 'restore_backup',
180
+ 'delete_backup',
181
+ 'purge_backups',
179
182
  'delete_deployment_payload',
180
183
  ]);
181
184
 
@@ -300,6 +303,12 @@ export function makeOperationToolHandler(operationName: string) {
300
303
  operation: operationName,
301
304
  hdb_user: context.user,
302
305
  };
306
+ // Authorization bypass is internal dispatch state, never client-supplied tool
307
+ // arguments — mirrors handlePostRequest's stripping in serverHandlers.js. Without
308
+ // this, a caller-supplied `bypass_auth`/`bypassAuth` argument would reach handlers
309
+ // (e.g. createTokens) that trust it directly from the body.
310
+ delete body.bypass_auth;
311
+ delete body.bypassAuth;
303
312
  if (STREAMING_OPERATIONS.has(operationName)) {
304
313
  // Reject before the handler runs at all — see STREAMING_OPERATIONS' comment for why
305
314
  // destroying an already-returned stream isn't sufficient cleanup for these producers.
@@ -60,6 +60,9 @@ export const OPERATION_DESCRIPTIONS: Record<string, string> = {
60
60
  // list_deployments: components/deploymentOperations.ts:50 — Deployment history with filtering and pagination.
61
61
  list_deployments:
62
62
  'Lists deployments from system.hdb_deployment with optional filtering by project, status, and date range, plus pagination.',
63
+ // list_backups: dataLayer/rocksdbBackup.ts (listBackups) — Managed RocksDB backup metadata for a database.
64
+ list_backups:
65
+ 'Lists the managed RocksDB directory backups for a database (id, timestamp, size, file count). Requires super_user; RocksDB databases only.',
63
66
 
64
67
  // ─── DEFAULT_ALLOW: search_* ──────────────────────────────────────────
65
68
  // search_by_conditions: dataLayer/search.ts:6 — Multi-condition search with comparators.
@@ -101,6 +101,15 @@ export const OPERATION_INPUT_SCHEMAS: Record<string, object> = {
101
101
  properties: {},
102
102
  description: 'Lists deployed component versions in this Harper instance.',
103
103
  },
104
+ list_backups: {
105
+ type: 'object',
106
+ properties: {
107
+ database: {
108
+ type: 'string',
109
+ description: 'Database name whose managed RocksDB backups to list. Defaults to `data`.',
110
+ },
111
+ },
112
+ },
104
113
 
105
114
  // ─── search_* ─────────────────────────────────────────────────────────
106
115
  search_by_hash: {
@@ -410,6 +410,7 @@ async function deployComponent(req) {
410
410
  };
411
411
  }
412
412
  if (req.urlPath !== undefined) applicationConfig.urlPath = req.urlPath;
413
+ if (req.host !== undefined) applicationConfig.host = req.host;
413
414
  // Persist credential references (never tokens) so every cold install of this component —
414
415
  // reboot, new peer, rollback — re-resolves the credential from the store.
415
416
  if (credentialReferences.length) applicationConfig.credentials = credentialReferences;
@@ -645,12 +646,10 @@ async function deployComponent(req) {
645
646
  // deploy checks its own local isNewComponent, since directory state (and therefore
646
647
  // whether the component was already active) can differ per node.
647
648
  //
648
- // An existing, already-active component being redeployed does NOT force a restart
649
- // here: some updates (e.g. static files only) may not need one at all, and when one
650
- // genuinely is needed, that component's already-running file watcher (Scope/
651
- // EntryHandler, see deployLifecycle.ts) independently detects the post-deploy file
652
- // changes and requests the restart itself.
653
- if (application.isNewComponent) {
649
+ // An existing component's watched files are handled by Scope/EntryHandler. Package
650
+ // metadata is deliberately outside most plugin globs, so compare it across the atomic
651
+ // swap as well: a dependency or module-entry change also invalidates loaded code.
652
+ if (application.isNewComponent || application.packageMetadataChanged) {
654
653
  const { requestRestart } = require('./requestRestart.ts');
655
654
  requestRestart();
656
655
  }