@harperfast/harper 5.3.0-beta.3 → 5.3.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/bin/cliOperations.ts +25 -3
  2. package/bin/run.ts +18 -0
  3. package/components/Application.ts +52 -78
  4. package/components/DESIGN.md +76 -3
  5. package/components/Scope.ts +10 -4
  6. package/components/anthropic/index.ts +10 -1
  7. package/components/bedrock/index.ts +10 -1
  8. package/components/componentLoader.ts +23 -14
  9. package/components/mcp/tools/operations.ts +3 -1
  10. package/components/ollama/index.ts +10 -1
  11. package/components/openai/index.ts +265 -18
  12. package/components/operations.js +40 -1
  13. package/config/configUtils.ts +3 -19
  14. package/dataLayer/DESIGN.md +29 -5
  15. package/dataLayer/harperBridge/ResourceBridge.ts +39 -3
  16. package/dataLayer/restoreMarker.ts +250 -14
  17. package/dataLayer/schema.ts +2 -7
  18. package/dataLayer/schemaDescribe.ts +10 -0
  19. package/dist/bin/cliOperations.js +28 -3
  20. package/dist/bin/cliOperations.js.map +1 -1
  21. package/dist/bin/run.js +19 -0
  22. package/dist/bin/run.js.map +1 -1
  23. package/dist/components/Application.d.ts +8 -2
  24. package/dist/components/Application.js +43 -69
  25. package/dist/components/Application.js.map +1 -1
  26. package/dist/components/Scope.d.ts +1 -1
  27. package/dist/components/Scope.js +9 -4
  28. package/dist/components/Scope.js.map +1 -1
  29. package/dist/components/anthropic/index.d.ts +0 -10
  30. package/dist/components/anthropic/index.js +9 -1
  31. package/dist/components/anthropic/index.js.map +1 -1
  32. package/dist/components/bedrock/index.d.ts +0 -12
  33. package/dist/components/bedrock/index.js +9 -1
  34. package/dist/components/bedrock/index.js.map +1 -1
  35. package/dist/components/componentLoader.js +23 -14
  36. package/dist/components/componentLoader.js.map +1 -1
  37. package/dist/components/mcp/tools/operations.js +1 -1
  38. package/dist/components/mcp/tools/operations.js.map +1 -1
  39. package/dist/components/ollama/index.d.ts +0 -13
  40. package/dist/components/ollama/index.js +9 -1
  41. package/dist/components/ollama/index.js.map +1 -1
  42. package/dist/components/openai/index.d.ts +11 -15
  43. package/dist/components/openai/index.js +224 -12
  44. package/dist/components/openai/index.js.map +1 -1
  45. package/dist/components/operations.d.ts +9 -0
  46. package/dist/components/operations.js +32 -2
  47. package/dist/components/operations.js.map +1 -1
  48. package/dist/config/configUtils.d.ts +1 -2
  49. package/dist/config/configUtils.js +7 -22
  50. package/dist/config/configUtils.js.map +1 -1
  51. package/dist/dataLayer/harperBridge/ResourceBridge.js +29 -2
  52. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  53. package/dist/dataLayer/restoreMarker.d.ts +25 -3
  54. package/dist/dataLayer/restoreMarker.js +213 -13
  55. package/dist/dataLayer/restoreMarker.js.map +1 -1
  56. package/dist/dataLayer/schema.js +1 -4
  57. package/dist/dataLayer/schema.js.map +1 -1
  58. package/dist/dataLayer/schemaDescribe.js +10 -0
  59. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/json/systemSchema.json +28 -0
  63. package/dist/resources/DatabaseTransaction.d.ts +21 -1
  64. package/dist/resources/DatabaseTransaction.js +147 -8
  65. package/dist/resources/DatabaseTransaction.js.map +1 -1
  66. package/dist/resources/PrimaryRocksDatabase.js +1 -1
  67. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  68. package/dist/resources/Table.d.ts +31 -3
  69. package/dist/resources/Table.js +397 -79
  70. package/dist/resources/Table.js.map +1 -1
  71. package/dist/resources/auditStore.js +16 -10
  72. package/dist/resources/auditStore.js.map +1 -1
  73. package/dist/resources/blob.d.ts +1 -0
  74. package/dist/resources/blob.js +14 -5
  75. package/dist/resources/blob.js.map +1 -1
  76. package/dist/resources/branchDatabase.js +3 -3
  77. package/dist/resources/branchDatabase.js.map +1 -1
  78. package/dist/resources/databaseDropPreparation.d.ts +20 -0
  79. package/dist/resources/databaseDropPreparation.js +106 -0
  80. package/dist/resources/databaseDropPreparation.js.map +1 -0
  81. package/dist/resources/databases.d.ts +26 -21
  82. package/dist/resources/databases.js +1040 -204
  83. package/dist/resources/databases.js.map +1 -1
  84. package/dist/resources/derivedIndexRegistry.d.ts +23 -0
  85. package/dist/resources/derivedIndexRegistry.js +92 -0
  86. package/dist/resources/derivedIndexRegistry.js.map +1 -1
  87. package/dist/resources/derivedIndexRuntime.d.ts +10 -0
  88. package/dist/resources/derivedIndexRuntime.js +136 -25
  89. package/dist/resources/derivedIndexRuntime.js.map +1 -1
  90. package/dist/resources/derivedIndexes.d.ts +39 -0
  91. package/dist/resources/derivedIndexes.js +733 -0
  92. package/dist/resources/derivedIndexes.js.map +1 -0
  93. package/dist/resources/fullTextSchema.d.ts +8 -0
  94. package/dist/resources/fullTextSchema.js +42 -0
  95. package/dist/resources/fullTextSchema.js.map +1 -1
  96. package/dist/resources/fullTextSchemaLifecycle.d.ts +0 -5
  97. package/dist/resources/fullTextSchemaLifecycle.js +0 -9
  98. package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
  99. package/dist/resources/graphql.js +101 -9
  100. package/dist/resources/graphql.js.map +1 -1
  101. package/dist/resources/indexes/hnswDerivedIndex.d.ts +0 -10
  102. package/dist/resources/indexes/hnswDerivedIndex.js +0 -230
  103. package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -1
  104. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +3 -0
  105. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +57 -8
  106. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
  107. package/dist/resources/models/Models.d.ts +26 -3
  108. package/dist/resources/models/Models.js +256 -13
  109. package/dist/resources/models/Models.js.map +1 -1
  110. package/dist/resources/models/TestBackend.d.ts +3 -1
  111. package/dist/resources/models/TestBackend.js +35 -1
  112. package/dist/resources/models/TestBackend.js.map +1 -1
  113. package/dist/resources/models/analyticsTable.d.ts +2 -2
  114. package/dist/resources/models/analyticsTable.js +4 -2
  115. package/dist/resources/models/analyticsTable.js.map +1 -1
  116. package/dist/resources/models/backendHelpers.d.ts +24 -0
  117. package/dist/resources/models/backendHelpers.js +18 -1
  118. package/dist/resources/models/backendHelpers.js.map +1 -1
  119. package/dist/resources/models/backendRegistry.d.ts +10 -21
  120. package/dist/resources/models/backendRegistry.js +74 -18
  121. package/dist/resources/models/backendRegistry.js.map +1 -1
  122. package/dist/resources/models/bootstrap.d.ts +7 -0
  123. package/dist/resources/models/bootstrap.js +36 -4
  124. package/dist/resources/models/bootstrap.js.map +1 -1
  125. package/dist/resources/models/decideHook.d.ts +33 -0
  126. package/dist/resources/models/decideHook.js +105 -0
  127. package/dist/resources/models/decideHook.js.map +1 -0
  128. package/dist/resources/models/decision.d.ts +64 -0
  129. package/dist/resources/models/decision.js +507 -0
  130. package/dist/resources/models/decision.js.map +1 -0
  131. package/dist/resources/models/decisionStore.d.ts +134 -0
  132. package/dist/resources/models/decisionStore.js +350 -0
  133. package/dist/resources/models/decisionStore.js.map +1 -0
  134. package/dist/resources/models/embedHook.d.ts +66 -8
  135. package/dist/resources/models/embedHook.js +228 -61
  136. package/dist/resources/models/embedHook.js.map +1 -1
  137. package/dist/resources/models/generativeDecision.d.ts +58 -0
  138. package/dist/resources/models/generativeDecision.js +315 -0
  139. package/dist/resources/models/generativeDecision.js.map +1 -0
  140. package/dist/resources/models/types.d.ts +232 -4
  141. package/dist/security/authn/oidc/tokenExchange.js +3 -33
  142. package/dist/security/authn/oidc/tokenExchange.js.map +1 -1
  143. package/dist/security/authn/oidc/tokenUseTable.d.ts +9 -0
  144. package/dist/security/authn/oidc/tokenUseTable.js +28 -0
  145. package/dist/security/authn/oidc/tokenUseTable.js.map +1 -0
  146. package/dist/security/certificateVerification/certificateVerificationSource.d.ts +0 -1
  147. package/dist/security/certificateVerification/certificateVerificationSource.js +3 -3
  148. package/dist/security/certificateVerification/certificateVerificationSource.js.map +1 -1
  149. package/dist/security/certificateVerification/crlVerification.js +77 -165
  150. package/dist/security/certificateVerification/crlVerification.js.map +1 -1
  151. package/dist/security/certificateVerification/types.d.ts +0 -3
  152. package/dist/security/certificateVerification/verificationTables.d.ts +19 -0
  153. package/dist/security/certificateVerification/verificationTables.js +125 -0
  154. package/dist/security/certificateVerification/verificationTables.js.map +1 -0
  155. package/dist/security/certificateVerification/verificationUtils.d.ts +1 -1
  156. package/dist/security/certificateVerification/verificationUtils.js +6 -31
  157. package/dist/security/certificateVerification/verificationUtils.js.map +1 -1
  158. package/dist/security/jsLoader.d.ts +5 -0
  159. package/dist/security/jsLoader.js +8 -0
  160. package/dist/security/jsLoader.js.map +1 -1
  161. package/dist/server/Server.d.ts +1 -0
  162. package/dist/server/Server.js.map +1 -1
  163. package/dist/server/http.js +2 -2
  164. package/dist/server/http.js.map +1 -1
  165. package/dist/server/itc/serverHandlers.js +21 -2
  166. package/dist/server/itc/serverHandlers.js.map +1 -1
  167. package/dist/server/jobs/jobOwnership.d.ts +25 -0
  168. package/dist/server/jobs/jobOwnership.js +127 -0
  169. package/dist/server/jobs/jobOwnership.js.map +1 -0
  170. package/dist/server/jobs/jobProcess.js +9 -2
  171. package/dist/server/jobs/jobProcess.js.map +1 -1
  172. package/dist/server/jobs/jobs.js +18 -5
  173. package/dist/server/jobs/jobs.js.map +1 -1
  174. package/dist/server/loadRootComponents.js +10 -0
  175. package/dist/server/loadRootComponents.js.map +1 -1
  176. package/dist/server/serverHelpers/contentTypes.js +11 -1
  177. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  178. package/dist/server/serverHelpers/serverHandlers.js +12 -2
  179. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  180. package/dist/server/threads/itc.d.ts +4 -1
  181. package/dist/server/threads/itc.js +29 -6
  182. package/dist/server/threads/itc.js.map +1 -1
  183. package/dist/server/threads/manageThreads.d.ts +12 -4
  184. package/dist/server/threads/manageThreads.js +241 -119
  185. package/dist/server/threads/manageThreads.js.map +1 -1
  186. package/dist/server/threads/threadServer.d.ts +1 -0
  187. package/dist/server/threads/threadServer.js +15 -1
  188. package/dist/server/threads/threadServer.js.map +1 -1
  189. package/dist/server/threads/windowsProcessTree.d.ts +91 -0
  190. package/dist/server/threads/windowsProcessTree.js +361 -0
  191. package/dist/server/threads/windowsProcessTree.js.map +1 -0
  192. package/dist/server/throttle.d.ts +1 -1
  193. package/dist/server/throttle.js +8 -1
  194. package/dist/server/throttle.js.map +1 -1
  195. package/dist/upgrade/directives/5-3-0.js +53 -46
  196. package/dist/upgrade/directives/5-3-0.js.map +1 -1
  197. package/dist/utility/errors/hdbError.d.ts +10 -1
  198. package/dist/utility/errors/hdbError.js +55 -11
  199. package/dist/utility/errors/hdbError.js.map +1 -1
  200. package/dist/utility/fsync.d.ts +2 -0
  201. package/dist/utility/fsync.js +20 -0
  202. package/dist/utility/fsync.js.map +1 -0
  203. package/dist/utility/globalSchema.d.ts +18 -0
  204. package/dist/utility/hdbTerms.d.ts +3 -0
  205. package/dist/utility/hdbTerms.js +3 -0
  206. package/dist/utility/hdbTerms.js.map +1 -1
  207. package/dist/utility/signalling.d.ts +7 -1
  208. package/dist/utility/signalling.js +27 -3
  209. package/dist/utility/signalling.js.map +1 -1
  210. package/dist/utility/when.d.ts +1 -0
  211. package/dist/utility/when.js +21 -0
  212. package/dist/utility/when.js.map +1 -1
  213. package/dist/validation/configValidator.js +30 -3
  214. package/dist/validation/configValidator.js.map +1 -1
  215. package/index.ts +13 -0
  216. package/json/systemSchema.json +28 -0
  217. package/npm-shrinkwrap.json +151 -55
  218. package/package.json +14 -13
  219. package/resources/DESIGN.md +80 -100
  220. package/resources/DatabaseTransaction.ts +157 -10
  221. package/resources/PrimaryRocksDatabase.ts +2 -2
  222. package/resources/Table.ts +494 -101
  223. package/resources/auditStore.ts +18 -10
  224. package/resources/blob.ts +13 -6
  225. package/resources/branchDatabase.ts +3 -3
  226. package/resources/databaseDropPreparation.ts +120 -0
  227. package/resources/databases.ts +1155 -205
  228. package/resources/derivedIndexRegistry.ts +122 -0
  229. package/resources/derivedIndexRuntime.ts +157 -30
  230. package/resources/derivedIndexes.ts +856 -0
  231. package/resources/fullTextSchema.ts +55 -0
  232. package/resources/fullTextSchemaLifecycle.ts +0 -15
  233. package/resources/graphql.ts +107 -12
  234. package/resources/indexes/DESIGN.md +7 -0
  235. package/resources/indexes/hnswDerivedIndex.ts +0 -260
  236. package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +72 -9
  237. package/resources/models/DESIGN.md +39 -0
  238. package/resources/models/Models.ts +361 -18
  239. package/resources/models/TestBackend.ts +56 -1
  240. package/resources/models/analyticsTable.ts +6 -4
  241. package/resources/models/backendHelpers.ts +17 -0
  242. package/resources/models/backendRegistry.ts +76 -21
  243. package/resources/models/bootstrap.ts +57 -7
  244. package/resources/models/decideHook.ts +135 -0
  245. package/resources/models/decision.ts +505 -0
  246. package/resources/models/decisionStore.ts +419 -0
  247. package/resources/models/embedHook.ts +272 -55
  248. package/resources/models/generativeDecision.ts +459 -0
  249. package/resources/models/types.ts +231 -4
  250. package/security/DESIGN.md +22 -2
  251. package/security/authn/oidc/tokenExchange.ts +6 -34
  252. package/security/authn/oidc/tokenUseTable.ts +27 -0
  253. package/security/certificateVerification/certificateVerificationSource.ts +2 -3
  254. package/security/certificateVerification/crlVerification.ts +96 -174
  255. package/security/certificateVerification/types.ts +0 -3
  256. package/security/certificateVerification/verificationTables.ts +124 -0
  257. package/security/certificateVerification/verificationUtils.ts +8 -32
  258. package/security/jsLoader.ts +9 -1
  259. package/server/DESIGN.md +27 -1
  260. package/server/Server.ts +2 -1
  261. package/server/http.ts +4 -2
  262. package/server/itc/serverHandlers.js +29 -1
  263. package/server/jobs/jobOwnership.ts +89 -0
  264. package/server/jobs/jobProcess.ts +8 -2
  265. package/server/jobs/jobs.ts +15 -4
  266. package/server/loadRootComponents.js +11 -0
  267. package/server/serverHelpers/contentTypes.ts +13 -1
  268. package/server/serverHelpers/serverHandlers.js +11 -2
  269. package/server/threads/itc.js +32 -6
  270. package/server/threads/manageThreads.js +268 -122
  271. package/server/threads/threadServer.js +13 -1
  272. package/server/threads/windowsProcessTree.ts +409 -0
  273. package/server/throttle.ts +11 -1
  274. package/studio/web/assets/{Chat-CveGbxe6.js → Chat-Bq60AO-g.js} +1 -1
  275. package/studio/web/assets/{FloatingChat-IpCFgYWL.js → FloatingChat-D4560T0p.js} +3 -3
  276. package/studio/web/assets/{apiToken-CS8fFTgL.js → apiToken-BEvbFPMc.js} +1 -1
  277. package/studio/web/assets/{applications-D7F4Jopl.js → applications-CSUVTH4E.js} +1 -1
  278. package/studio/web/assets/{cssMode-Daw_oLPq.js → cssMode-DJyc9mHj.js} +1 -1
  279. package/studio/web/assets/{editor-B3Bs1zWX.js → editor-DDIsQfj6.js} +1 -1
  280. package/studio/web/assets/{html-CDaQwWiE.js → html-g1BJHxLy.js} +1 -1
  281. package/studio/web/assets/{htmlMode-Cb6emO4U.js → htmlMode-Vh5IglCQ.js} +1 -1
  282. package/studio/web/assets/{index-B4g2Fu_2.js → index-DGRAzvO4.js} +5 -5
  283. package/studio/web/assets/{index.lazy-NwhvF1q1.js → index.lazy-vgSic7et.js} +1 -1
  284. package/studio/web/assets/{javascript-2_86gB9Q.js → javascript-5VIolqf7.js} +1 -1
  285. package/studio/web/assets/{jsonMode-DX-i86uQ.js → jsonMode-DG4zQraI.js} +1 -1
  286. package/studio/web/assets/{languageServices-B85WN-Ej.js → languageServices-t5FBpX-i.js} +1 -1
  287. package/studio/web/assets/{lspLanguageFeatures-JCWHIsKg.js → lspLanguageFeatures-DgVnNPh-.js} +1 -1
  288. package/studio/web/assets/{notifications-D9ejzt52.js → notifications-BeWcX04m.js} +1 -1
  289. package/studio/web/assets/{notifications-B7hjKhai.js → notifications-DUGWrWoO.js} +1 -1
  290. package/studio/web/assets/{profile-D8B6No3F.js → profile-BvUEDBzW.js} +1 -1
  291. package/studio/web/assets/{regions-DFazIfTK.js → regions-CB-_2m7M.js} +1 -1
  292. package/studio/web/assets/{register-CwFLviLF.js → register-1w1Zr3b2.js} +2 -2
  293. package/studio/web/assets/{setComponentFile-VnJ80SSJ.js → setComponentFile-z4-qJCbA.js} +1 -1
  294. package/studio/web/assets/{setup-BYQxFy-C.js → setup-CS50uJ4x.js} +2 -2
  295. package/studio/web/assets/{status-Cn5XjmsL.js → status-CI0wOdOE.js} +1 -1
  296. package/studio/web/assets/{toggleHighContrast-g9xIh6TX.js → toggleHighContrast-BsAE4jPe.js} +1 -1
  297. package/studio/web/assets/{tsMode-EC-n0Wqc.js → tsMode-D83Gg3cq.js} +1 -1
  298. package/studio/web/assets/{typescript-DHmM9JCO.js → typescript-Dfp-GrNq.js} +1 -1
  299. package/studio/web/assets/{useEntityRestURL-BUdnVSVv.js → useEntityRestURL-BKetlINf.js} +1 -1
  300. package/studio/web/assets/{workers-BVl9bBB-.js → workers-CCvmDLH3.js} +1 -1
  301. package/studio/web/assets/{xml-C4_sqOkX.js → xml-06pZp00U.js} +1 -1
  302. package/studio/web/assets/{yaml-CVkbDqtW.js → yaml-iSnWMVlV.js} +1 -1
  303. package/studio/web/index.html +1 -1
  304. package/upgrade/directives/5-3-0.ts +61 -54
  305. package/utility/DESIGN.md +5 -1
  306. package/utility/errors/hdbError.ts +56 -8
  307. package/utility/fsync.ts +16 -0
  308. package/utility/hdbTerms.ts +3 -0
  309. package/utility/signalling.ts +33 -4
  310. package/utility/when.ts +22 -0
  311. package/validation/configValidator.ts +36 -7
@@ -978,27 +978,37 @@ async function cliOperations(req: any, skipResponseLog = false) {
978
978
  // when response headers arrive (streamResponse: true), which happens before the full
979
979
  // upload completes — calling endUpload() here would snap the bar prematurely.
980
980
 
981
+ const { statusCode } = response;
982
+ const failedStatus = statusCode < 200 || (statusCode >= 300 && statusCode !== 304);
983
+ const eventStream = useSse && response.headers['content-type']?.startsWith('text/event-stream');
981
984
  let responseData;
982
- if (useSse && response.headers['content-type']?.startsWith('text/event-stream')) {
985
+ if (eventStream && !failedStatus) {
983
986
  // Consume SSE: render phase events live, capture the final result from the `done`
984
987
  // event (or the error message from the `error` event). The HTTP status stays 200
985
988
  // until end-of-stream; failures are signaled in-band.
986
989
  let finalResult;
987
990
  let sseError;
991
+ let terminated = false;
992
+ // Older servers write a result as unnamed frames, with no `done` event.
993
+ let lastUnnamedData: string | undefined;
988
994
  for await (const message of parseSSE(response)) {
989
995
  renderer?.renderEvent(message);
990
996
  if (message.event === 'done') {
997
+ terminated = true;
991
998
  try {
992
999
  finalResult = JSON.parse(message.data)?.result;
993
1000
  } catch {
994
1001
  finalResult = message.data;
995
1002
  }
996
1003
  } else if (message.event === 'error') {
1004
+ terminated = true;
997
1005
  try {
998
1006
  sseError = JSON.parse(message.data);
999
1007
  } catch {
1000
1008
  sseError = { message: message.data };
1001
1009
  }
1010
+ } else if (message.event === 'message') {
1011
+ lastUnnamedData = message.data;
1002
1012
  }
1003
1013
  }
1004
1014
  if (sseError) {
@@ -1006,6 +1016,13 @@ async function cliOperations(req: any, skipResponseLog = false) {
1006
1016
  console.error(`error: ${errMsg}`);
1007
1017
  process.exit(1);
1008
1018
  }
1019
+ if (finalResult === undefined && !terminated && lastUnnamedData !== undefined) {
1020
+ try {
1021
+ finalResult = JSON.parse(lastUnnamedData);
1022
+ } catch {
1023
+ finalResult = { message: lastUnnamedData };
1024
+ }
1025
+ }
1009
1026
  responseData = finalResult ?? { message: 'Deploy completed (no result payload).' };
1010
1027
  } else {
1011
1028
  // When useSse is true, httpRequest returns a raw IncomingMessage (streamResponse mode),
@@ -1018,6 +1035,12 @@ async function cliOperations(req: any, skipResponseLog = false) {
1018
1035
  } else {
1019
1036
  bodyText = response.body;
1020
1037
  }
1038
+ // A request refused before its stream started can still arrive labeled as an event stream,
1039
+ // with the error written as an unnamed frame (older servers). Unwrap it; a body with no
1040
+ // frames at all (a proxy's error page) stays as it came.
1041
+ if (eventStream) {
1042
+ for await (const message of parseSSE(Readable.from([bodyText]))) bodyText = message.data;
1043
+ }
1021
1044
  try {
1022
1045
  responseData = JSON.parse(bodyText);
1023
1046
  } catch {
@@ -1035,8 +1058,7 @@ async function cliOperations(req: any, skipResponseLog = false) {
1035
1058
  responseLog = YAML.stringify(responseData).trim();
1036
1059
  }
1037
1060
 
1038
- const { statusCode } = response;
1039
- if (statusCode < 200 || (statusCode >= 300 && statusCode !== 304)) {
1061
+ if (failedStatus) {
1040
1062
  const errorPrefix = responseLog.startsWith('error:') ? '' : 'error: ';
1041
1063
  console.error(`${errorPrefix}${responseLog}`);
1042
1064
  process.exit(1);
package/bin/run.ts CHANGED
@@ -26,6 +26,8 @@ import { startHTTPThreads } from '../server/threads/socketRouter.ts';
26
26
  import { beginProcessShutdown } from '../server/threads/manageThreads.js';
27
27
  import * as hdbInfoController from '../dataLayer/hdbInfoController.ts';
28
28
  import { isReadOnlyMode } from '../resources/databases.ts';
29
+ import { declareTokenUseTable } from '../security/authn/oidc/tokenUseTable.ts';
30
+ import { ensureCertificateVerificationTables } from '../security/certificateVerification/verificationTables.ts';
29
31
  import { getThisNodeName, getThisNodeHostname } from '../server/nodeName.ts';
30
32
  import * as hdbTerms from '../utility/hdbTerms.ts';
31
33
  import { getHdbPid, isProcessRunning } from '../utility/processManagement/processManagement.js';
@@ -184,6 +186,22 @@ async function initialize(calledByInstall = false, calledByMain = false) {
184
186
  process.exit(1);
185
187
  }
186
188
 
189
+ // Every boot: data already at a 5.3.0 pre-release never runs the 5.3.0 directive (compareVersions sorts
190
+ // those versions above 5.3.0). Before worker threads start, so they load the table with its expiration.
191
+ if (!isReadOnlyMode()) {
192
+ try {
193
+ declareTokenUseTable();
194
+ } catch (error) {
195
+ hdbLogger.error('Starting without the declared system.hdb_oidc_token_use; the next start retries', error);
196
+ }
197
+ // A node that never verifies a client certificate still holds the rows its peers replicate.
198
+ try {
199
+ await ensureCertificateVerificationTables();
200
+ } catch (error) {
201
+ hdbLogger.error('Starting without the declared certificate verification tables; the next start retries', error);
202
+ }
203
+ }
204
+
187
205
  // A built-in component only activates when its config key is present. Fresh installs get these
188
206
  // keys from defaultConfig.yaml, but an in-place upgrade carries the old config forward without a
189
207
  // key added in a newer release (harper-pro#585). Ensure them on every boot — idempotent and
@@ -55,6 +55,10 @@ import {
55
55
  writeFile,
56
56
  } from 'node:fs/promises';
57
57
  import { spawn, type ChildProcess } from 'node:child_process';
58
+ import {
59
+ confirmWindowsProcessTreeGone,
60
+ type WindowsProcessTreeIdentity,
61
+ } from '../server/threads/windowsProcessTree.ts';
58
62
  import { tmpdir } from 'node:os';
59
63
  import { randomUUID } from 'node:crypto';
60
64
  import { chmodSync, createReadStream, existsSync, lstatSync, renameSync } from 'node:fs';
@@ -610,6 +614,19 @@ const RECOVERY_LOCK_WAIT = {
610
614
  const COMPONENT_RECOVERY_LOCK_PURPOSE = 'component-recovery';
611
615
  const MAX_GIT_EXTRACTION_COMMANDS = 4;
612
616
  const MAX_INSTALL_COMMANDS = 2;
617
+
618
+ /**
619
+ * The longest one preparation may legitimately run: the longest extraction path runs clone, tag listing,
620
+ * checkout, and npm pack, and a custom package manager configured to warn can then fall back to npm, yielding
621
+ * two install commands. What a preparation lock waits on, and what an origin allows a peer's replicated deploy.
622
+ */
623
+ export function componentPreparationBudgetMs(installTimeoutMs: number = DEFAULT_COMMAND_TIMEOUT_MS): number {
624
+ return (
625
+ MAX_GIT_EXTRACTION_COMMANDS * DEFAULT_COMMAND_TIMEOUT_MS +
626
+ MAX_INSTALL_COMMANDS * installTimeoutMs +
627
+ COMPONENT_PREPARATION_WAIT_MARGIN_MS
628
+ );
629
+ }
613
630
  const PRODUCTION_DEPENDENCY_FIELDS = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const;
614
631
  const INSTALL_LIFECYCLE_SCRIPTS = new Set([
615
632
  'preinstall',
@@ -4474,13 +4491,8 @@ export async function prepareApplication(application: Application, options: Prep
4474
4491
  }
4475
4492
  },
4476
4493
  {
4477
- // The longest extraction path runs clone, tag listing, checkout, and npm pack. A custom
4478
- // package manager configured to warn can then fall back to npm, yielding two install
4479
- // commands. Bound orphaned same-process worker locks without rejecting behind a valid holder.
4480
- timeoutMs:
4481
- MAX_GIT_EXTRACTION_COMMANDS * DEFAULT_COMMAND_TIMEOUT_MS +
4482
- MAX_INSTALL_COMMANDS * commandTimeoutMs +
4483
- COMPONENT_PREPARATION_WAIT_MARGIN_MS,
4494
+ // Bound orphaned same-process worker locks without rejecting behind a valid holder.
4495
+ timeoutMs: componentPreparationBudgetMs(commandTimeoutMs),
4484
4496
  onWait: (owner) =>
4485
4497
  application.logger.info(
4486
4498
  `Waiting for in-progress preparation of ${application.name}` +
@@ -5209,6 +5221,10 @@ function spawnWithEnv(
5209
5221
  }
5210
5222
  const spawnLogger = logger.loggerWithTag(`${applicationName}:spawn:${command}`);
5211
5223
 
5224
+ // The child is created inside spawn(), so the interval around the call bounds its creation
5225
+ // time exactly — what the Windows tree scan needs to tell its children from an orphan of the
5226
+ // PID's previous owner.
5227
+ const spawnStartedAt = Date.now();
5212
5228
  const childProcess = spawn(command, args, {
5213
5229
  shell: true,
5214
5230
  cwd,
@@ -5218,13 +5234,29 @@ function spawnWithEnv(
5218
5234
  // and install-script descendant before the component preparation lock is released.
5219
5235
  detached: process.platform !== 'win32',
5220
5236
  });
5237
+ const rootKnownAt = Date.now();
5221
5238
  const trackedProcessId = childProcess.pid;
5222
- if (trackedProcessId) registerProcessGroup(trackedProcessId);
5239
+ // Read live by the Windows tree scan: a root that exits mid-wait must bound its children
5240
+ // from that moment, since its PID is reusable from then on.
5241
+ const treeIdentity: WindowsProcessTreeIdentity = {
5242
+ rootPid: trackedProcessId ?? 0,
5243
+ rootKnownAt,
5244
+ // A backward wall-clock step between the two Date.now() calls above (e.g. an NTP
5245
+ // correction) would otherwise make this negative — the same case the registry path
5246
+ // already guards (manageThreads.js's addProcessGroup discards spawnStartedAt entirely
5247
+ // when it reads later than spawnedAt). Falling back to the same allowance here keeps both
5248
+ // paths consistent instead of silently narrowing the window that admits the root's own
5249
+ // earliest children.
5250
+ rootStartedWithinMs: spawnStartedAt <= rootKnownAt ? rootKnownAt - spawnStartedAt : undefined,
5251
+ };
5252
+ const processGroupRegistration = trackedProcessId
5253
+ ? registerProcessGroup(trackedProcessId, rootKnownAt, spawnStartedAt)
5254
+ : undefined;
5223
5255
  let processGroupIsTracked = Boolean(trackedProcessId);
5224
5256
  const untrackProcessGroup = () => {
5225
5257
  if (!processGroupIsTracked || !trackedProcessId) return;
5226
5258
  processGroupIsTracked = false;
5227
- unregisterProcessGroup(trackedProcessId);
5259
+ unregisterProcessGroup(trackedProcessId, processGroupRegistration);
5228
5260
  };
5229
5261
 
5230
5262
  let didTimeout = false;
@@ -5235,7 +5267,7 @@ function spawnWithEnv(
5235
5267
  });
5236
5268
  const timeout = setTimeout(() => {
5237
5269
  didTimeout = true;
5238
- void terminateProcessTree(childProcess, closePromise).then(
5270
+ void terminateProcessTree(childProcess, closePromise, treeIdentity).then(
5239
5271
  () => {
5240
5272
  // Only untrack once terminateProcessTree has confirmed the group is actually gone.
5241
5273
  // The direct child can emit 'close' mid-grace-period (e.g. right after SIGTERM)
@@ -5302,6 +5334,7 @@ function spawnWithEnv(
5302
5334
  });
5303
5335
 
5304
5336
  childProcess.on('exit', (code, signal) => {
5337
+ treeIdentity.rootExitedAt = Date.now();
5305
5338
  spawnLogger.debug?.(`Direct child exited with code ${code}, signal ${signal}; awaiting stdio close`);
5306
5339
  });
5307
5340
 
@@ -5327,7 +5360,7 @@ function spawnWithEnv(
5327
5360
  // when a timeout is already driving its own terminateProcessTree call for this process.
5328
5361
  if (!didTimeout && trackedProcessId) {
5329
5362
  try {
5330
- await terminateProcessTree(childProcess, closePromise);
5363
+ await terminateProcessTree(childProcess, closePromise, treeIdentity);
5331
5364
  } catch (error) {
5332
5365
  untrackProcessGroup();
5333
5366
  flushOutput();
@@ -5397,71 +5430,6 @@ export async function waitForConfirmedTermination(
5397
5430
  while (await isAlive()) await delay(pollMs);
5398
5431
  }
5399
5432
 
5400
- export async function waitForWindowsTreeTermination(
5401
- attemptTermination: () => boolean | Promise<boolean>,
5402
- treeIsAlive: () => boolean | null | Promise<boolean | null>,
5403
- pollMs: number = PROCESS_TERMINATION_POLL_MS
5404
- ): Promise<void> {
5405
- for (;;) {
5406
- // A successful taskkill exit only proves the request was accepted, not that the whole tree
5407
- // has actually exited — Windows termination is asynchronous, and taskkill can report overall
5408
- // success even when a descendant is not yet (or never) reaped. Only an explicit `false` from
5409
- // treeIsAlive, independently confirming no member of the tree remains, is safe to return on;
5410
- // `true` or `null` (unknown) must keep the loop retrying.
5411
- await attemptTermination();
5412
- if ((await treeIsAlive()) === false) return;
5413
- await delay(pollMs);
5414
- }
5415
- }
5416
-
5417
- async function windowsProcessTreeIsAlive(rootPid: number): Promise<boolean | null> {
5418
- // Query the process table rather than probing only the parent PID: descendants retain their
5419
- // ParentProcessId after the parent exits, which is exactly the taskkill "process not found" race.
5420
- // Exit code 1 must mean "queried the process table and positively found nothing" — never
5421
- // "the query itself failed" (e.g. Get-CimInstance denied or WMI unavailable), which would
5422
- // otherwise read identically to a confirmed-gone tree and release the lock while a descendant
5423
- // may still be alive. ErrorActionPreference=Stop plus the wrapping try/catch turns a query
5424
- // failure into its own exit code (2), which the caller below already treats as unknown.
5425
- const script =
5426
- "$ErrorActionPreference = 'Stop'; try { " +
5427
- `$rootPid = ${rootPid}; ` +
5428
- '$all = @(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId); ' +
5429
- '$frontier = @($rootPid); $seen = @{}; $found = $false; ' +
5430
- 'while ($frontier.Count -gt 0) { ' +
5431
- '$next = @(); foreach ($parentPid in $frontier) { if ($seen[$parentPid]) { continue }; ' +
5432
- '$seen[$parentPid] = $true; foreach ($p in $all) { ' +
5433
- 'if ($p.ProcessId -eq $parentPid) { $found = $true }; ' +
5434
- 'if ($p.ParentProcessId -eq $parentPid) { $found = $true; $next += [int]$p.ProcessId } } }; ' +
5435
- '$frontier = $next }; ' +
5436
- 'if ($found) { exit 0 } else { exit 1 } ' +
5437
- '} catch { exit 2 }';
5438
- return new Promise<boolean | null>((resolve) => {
5439
- const query = spawn('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], {
5440
- stdio: 'ignore',
5441
- windowsHide: true,
5442
- });
5443
- query.once('close', (code) => resolve(code === 0 ? true : code === 1 ? false : null));
5444
- query.once('error', () => resolve(null));
5445
- });
5446
- }
5447
-
5448
- async function terminateWindowsProcessTree(childProcess: ChildProcess): Promise<void> {
5449
- if (!childProcess.pid) return;
5450
- const rootPid = childProcess.pid;
5451
- await waitForWindowsTreeTermination(
5452
- () =>
5453
- new Promise<boolean>((resolve) => {
5454
- const taskkill = spawn('taskkill', ['/pid', String(rootPid), '/T', '/F'], {
5455
- stdio: 'ignore',
5456
- windowsHide: true,
5457
- });
5458
- taskkill.once('close', (code) => resolve(code === 0));
5459
- taskkill.once('error', () => resolve(false));
5460
- }),
5461
- () => windowsProcessTreeIsAlive(rootPid)
5462
- );
5463
- }
5464
-
5465
5433
  async function waitForProcessClose(childProcess: ChildProcess, closePromise: Promise<void>): Promise<void> {
5466
5434
  let timer: ReturnType<typeof setTimeout>;
5467
5435
  const closeTimeout = new Promise<false>((resolve) => {
@@ -5478,13 +5446,19 @@ async function waitForProcessClose(childProcess: ChildProcess, closePromise: Pro
5478
5446
  }
5479
5447
  }
5480
5448
 
5481
- export async function terminateProcessTree(childProcess: ChildProcess, closePromise: Promise<void>): Promise<void> {
5449
+ export async function terminateProcessTree(
5450
+ childProcess: ChildProcess,
5451
+ closePromise: Promise<void>,
5452
+ treeIdentity: WindowsProcessTreeIdentity
5453
+ ): Promise<void> {
5482
5454
  if (!childProcess.pid) {
5483
5455
  await waitForProcessClose(childProcess, closePromise);
5484
5456
  return;
5485
5457
  }
5486
5458
  if (process.platform === 'win32') {
5487
- await terminateWindowsProcessTree(childProcess);
5459
+ await confirmWindowsProcessTreeGone(treeIdentity, {
5460
+ label: `pid ${childProcess.pid} (${childProcess.spawnargs.join(' ')})`,
5461
+ });
5488
5462
  await waitForProcessClose(childProcess, closePromise);
5489
5463
  return;
5490
5464
  }
@@ -49,12 +49,45 @@ The ordering is the design. Three things used to be wrong, the first two in a wa
49
49
  load-error PROBE, not a safety guarantee: it executes the component's own top-level code with
50
50
  incomplete side-effect isolation. It also remains a no-op on the main thread, and the operations API
51
51
  deploys on the main thread — so operator deploys are still unvalidated, exactly as before. Fixing that
52
- is separate work; this only fixed the order.
52
+ is separate work; this only fixed the order. Nor does it run on a worker under the default lockdown —
53
+ see the next section.
53
54
  - **Root config was written before the build and never rolled back**, so a build or validation that failed
54
55
  still left config naming the release, and `installApplications()` installed it at the next restart. The
55
56
  entry is now the transaction's third effect, published after the commit — see "Root config is an effect
56
57
  of the activation" below.
57
58
 
59
+ ### The load validation runs only where it can reproduce a boot load
60
+
61
+ `validateComponentLoads` (`components/operations.js`) loads the candidate in the thread running the deploy,
62
+ so its verdict is only worth what that thread has in common with a freshly started worker. Two kinds of
63
+ thread have too little, and it runs in neither:
64
+
65
+ - **The main thread**, where the operations API deploys: application code does not run there.
66
+ - **A worker under `applications.lockdown: freeze-after-load`**, the default, which is where a replicated
67
+ peer runs the deploy. The lockdown freezes a worker's intrinsics once its boot load finishes, and a freeze
68
+ is per realm and cannot be undone. A dependency that extends an intrinsic as it loads (reflect-metadata
69
+ defines `Reflect.decorate`) then throws in the deploying worker, yet loads in every worker a restart
70
+ starts. 5.2 hid this by swapping before it validated, so a failed first deploy still landed and
71
+ `restart: true` loaded it. Once validation moved before the swap, every peer refused every retry of a
72
+ working component while the origin, which does not validate, ran it (#2881).
73
+ `laterLoadsMeetFrozenIntrinsics()` (`security/jsLoader.ts`) is the test. It holds in any thread that
74
+ runs the boot load, so a deploy racing that load is excluded as well.
75
+
76
+ Workers still validate under the other modes
77
+ (`integrationTests/deploy/worker-deploy-frozen-intrinsics.test.ts`). `freeze` and `ses` freeze before the
78
+ boot load, so a later load meets exactly the intrinsics it met. `none` freezes nothing, but an intrinsic the
79
+ live version of a component changed, such as a property it made non-configurable, still differs from what a
80
+ fresh worker's load of the candidate meets. That narrower mismatch predates this and is left alone.
81
+
82
+ Under the default, no node load-validates a deploy. A candidate that throws at load goes live on every node
83
+ alike, and each restarted worker fails that component closed and serves the rest. That was always the
84
+ origin's behavior, and it replaces a cluster split between versions. A node on an earlier 5.3 build still
85
+ validates in its frozen workers and keeps refusing a candidate whose dependency extends an intrinsic at load,
86
+ so such a deploy converges only once every node is upgraded; retry it then. The `load` progress phase no
87
+ longer fires on a worker either; the operations API, on the main thread, never emitted it. Validating there
88
+ again needs a load in a fresh realm: #2315 step 2 plans it as a canary rollout, whose first replacement
89
+ worker boots the candidate before it takes traffic.
90
+
58
91
  ### Staging a build now and activating it later
59
92
 
60
93
  `deploy_component { activate: false }` stops after certification, leaving a dormant artifact; a later
@@ -94,7 +127,8 @@ did not need it:
94
127
  them:** nothing stops a dormant artifact being edited while it waits, so the link rule and the load
95
128
  validation are both re-run at activation rather than trusted from the marker. Content tampering is still
96
129
  not detected — that needs a manifest the marker is bound to, and the load validation that would catch a
97
- broken entry point is a no-op on the main thread until #2315 step 2.
130
+ broken entry point is a no-op on the main thread, and on a worker under the default lockdown, until
131
+ #2315 step 2.
98
132
 
99
133
  Certification moved out of `activateCandidateApplication` and up into `prepareApplication` for the same
100
134
  reason: `markCandidateComplete` fsyncs the whole candidate tree, and a delayed activation must not re-walk
@@ -196,7 +230,8 @@ and the journal removal: the journal is what would carry the activation forward
196
230
 
197
231
  Two limits are deliberate and tracked separately: activation is two renames, so the live _pathname_ is
198
232
  briefly absent (in-memory resources are unaffected, but a component that opens its own files during a
199
- request can still see a gap); and validation does not run on the main-thread deploy path.
233
+ request can still see a gap); and validation does not run on the main-thread deploy path, nor on a worker
234
+ under the default lockdown.
200
235
 
201
236
  ### Root config is an effect of the activation
202
237
 
@@ -346,6 +381,19 @@ starting `handleApplication`; if a deploy begins during the load, the plugin tim
346
381
  unpaused load time. This prevents a long install from looking like a hung plugin while its entry handlers
347
382
  are deliberately paused against the intermediate tree.
348
383
 
384
+ A deploy's own validation load is exempt: its Scopes (`isTransientValidation`, set at construction from
385
+ `collectScopes`) never read the deploy state, subscribe to `deploy:start`/`deploy:end`, pause their
386
+ watchers, or suspend their plugin timeout. The load runs inside the deploy's lifecycle bracket, between
387
+ build and swap, so any wait for a deploy to end can be a wait on itself. It was, for a component deployed
388
+ as `harper`: the validation load passes no `appName`, so its Scopes take the loader's default name
389
+ `harper`, matched the deploy in flight, and waited for its end while the deploy waited for them. A
390
+ replicated deploy of that component hung on every peer, since only worker threads validate. The same
391
+ cycle formed when a `harper` deploy started on a worker while another component's validation was running
392
+ there, because that deploy's own validation queues behind the running one (`validationChain`). The
393
+ candidate tree is complete before validation starts and nothing else writes to it, so there is nothing to
394
+ wait for. `unitTests/components/componentLoader.test.js` ("deploy validation loads never wait on a
395
+ deploy") holds both routes and the plain timeout.
396
+
349
397
  ### The component load lock is keyed by plugin type, so a plugin's promise is everyone's clock
350
398
 
351
399
  `sequentiallyHandleApplication` (`components/componentLoader.ts`) holds a cross-thread lock keyed by the
@@ -424,6 +472,31 @@ inputs retain their existing symlink behavior.
424
472
  - **Retry is only safe before any byte has reached the consumer.** Once a chunk is handed downstream, re-opening `Blob.stream()` from byte 0 would duplicate it (there's no cheap way to resume from an arbitrary offset across a fresh stream without also plumbing `Blob.slice()`, which was out of scope for this fix). So the helper retries only while the current attempt has yielded nothing yet; a stall after partial content fails immediately, same as before this existed.
425
473
  - **Backpressure and cancellation are two different problems, and both are easy to get wrong with a hand-rolled `ReadableStream`.** An early version wrapped the retry loop in `new ReadableStream({ async start(controller) { for await (...) controller.enqueue(chunk) } })` — this eagerly drains `streamFactory()` regardless of `controller.desiredSize`, defeating the whole point of not buffering a multi-GB payload in memory. Switching to an `async function*` consumed via `Readable.from()` restores real backpressure (the generator only resumes when the consumer wants more, matching how the un-wrapped `Blob.stream()` behaved pre-fix). But `Readable.from()`'s `return()`-on-destroy cancellation only takes effect at the generator's _next_ `yield` — while the loop is stuck retrying (no `yield` reached yet), destroying the `Readable` does nothing until the loop naturally exits, verified empirically (a generator that never yields kept retrying long after `.destroy()`). The fix: thread the constructed `Readable` back into the generator via a mutable cell (`readable` doesn't exist until after `Readable.from()` returns, so it can't be closed over directly) and check `.destroyed` explicitly at each loop iteration and after each backoff sleep.
426
474
 
475
+ ## An origin waits for a peer's deploy answer only as long as the peer may take
476
+
477
+ The origin of a replicated `deploy_component` hands `server.replication.replicateOperation` a `timeoutMs`
478
+ from `peerDeployAnswerTimeoutMs(req)` (`components/operations.js`). It is the sum of what the peer is
479
+ allowed for that request: its payload wait (`deployment_timeout`, counted twice when credential references
480
+ must also replicate in), two full preparation budgets (`componentPreparationBudgetMs`: every extraction
481
+ command and both install commands at their allowances), a margin for validation and the swap, and the
482
+ restart ceiling when the peer restarts before answering. It is clamped to the longest delay a timer holds.
483
+ One budget is the peer's own preparation. The other is the preparation lock's wait: a peer already preparing
484
+ the same component for another deploy holds this one at the lock for a budget before the lock re-checks the
485
+ holder.
486
+
487
+ It must never undercut a healthy peer. A shorter bound turns a slow success into a reported failure, and a
488
+ two-command install (a custom package manager falling back to npm) is exactly such a success. So the
489
+ default is hours, and its job is only that the origin eventually settles, rather than holding the
490
+ operation, the deployment row and its own restart for as long as a wedged peer stays wedged. Two things are
491
+ not budgeted. The lock keeps waiting while its holder is alive, so queueing behind a preparation that
492
+ outlasts the lock's wait, or behind several, can run past the deadline. So can plugin `timeout`s a component
493
+ configures beyond the validation margin, which live in the payload the origin does not parse. Covering the
494
+ first would take a deadline that follows the peer's progress rather than a sum of its allowances. The
495
+ deadline is not cancellation: a peer past it may still finish, so the failure the replicator records says
496
+ the outcome there is unknown.
497
+ It stays a `failed` peer result, because `getFailedPeers()` counts only that status, and a new one would
498
+ read as success.
499
+
427
500
  ## A dangling symlink silently truncates the deploy tarball (`components/packageComponent.ts`)
428
501
 
429
502
  Packaging uses `tar-fs.pack(dir, { dereference: true })` by default (`skip_symlinks` off).
@@ -81,6 +81,8 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
81
81
  // Set by the loader on deploy pre-flight validation loads (collectScopes):
82
82
  // the scope exists to validate a component, not to run it. Plugins with
83
83
  // process-global side effects should validate fully but skip activation.
84
+ // Such a scope never follows a deploy: it loads a finished candidate from inside
85
+ // its own deploy's lifecycle, so waiting or pausing for a deploy waits on itself.
84
86
  isTransientValidation?: boolean;
85
87
 
86
88
  /**
@@ -103,18 +105,20 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
103
105
  applicationScope: ApplicationScope,
104
106
  origin: string = appName,
105
107
  isRootConfig?: boolean,
106
- mount?: ScopeMount
108
+ mount?: ScopeMount,
109
+ isTransientValidation?: boolean
107
110
  ) {
108
111
  super();
109
112
 
110
113
  this.mount = mount;
114
+ this.isTransientValidation = isTransientValidation;
111
115
  this.#appName = appName;
112
116
  this.#pluginName = pluginName;
113
117
  this.#origin = typeof origin === 'string' ? origin : appName;
114
118
  this.#directory = directory;
115
119
  this.#configFilePath = configFilePath;
116
120
  this.#logger = loggerWithTag(this.#appName);
117
- this.#deployInFlight = deployLifecycle.loadsAwaitDeploy(this.#appName);
121
+ this.#deployInFlight = !isTransientValidation && deployLifecycle.loadsAwaitDeploy(this.#appName);
118
122
 
119
123
  this.databaseEvents = databaseEventsEmitter;
120
124
  this.applicationScope = applicationScope;
@@ -170,8 +174,10 @@ export class Scope extends EventEmitter<ScopeEventsMap> {
170
174
  this.#deployEndHandler = (name) => {
171
175
  if (name === this.#appName) this.#onDeployEnd(name);
172
176
  };
173
- deployLifecycle.on('deploy:start', this.#deployStartHandler);
174
- deployLifecycle.on('deploy:end', this.#deployEndHandler);
177
+ if (!isTransientValidation) {
178
+ deployLifecycle.on('deploy:start', this.#deployStartHandler);
179
+ deployLifecycle.on('deploy:end', this.#deployEndHandler);
180
+ }
175
181
  }
176
182
 
177
183
  get logger(): Logger {
@@ -89,6 +89,15 @@ export interface AnthropicBackendConfig {
89
89
  * `tools: true` — Anthropic has first-class tool-use support via
90
90
  * `tool_use` / `tool_result` content blocks.
91
91
  */
92
+ const CAPABILITIES: ModelCapabilities = Object.freeze({
93
+ embed: false,
94
+ generate: true,
95
+ stream: true,
96
+ tools: true,
97
+ adapters: false,
98
+ structuredOutput: false,
99
+ });
100
+
92
101
  export class AnthropicBackend implements ModelBackend {
93
102
  readonly name = 'anthropic';
94
103
  readonly #baseUrl: string;
@@ -106,7 +115,7 @@ export class AnthropicBackend implements ModelBackend {
106
115
  }
107
116
 
108
117
  capabilities(): ModelCapabilities {
109
- return { embed: false, generate: true, stream: true, tools: true, adapters: false };
118
+ return CAPABILITIES;
110
119
  }
111
120
 
112
121
  async generate(input: GenerateInput, opts: BackendOpts<GenerateOpts>): Promise<ModelCallResult<GenerateResult>> {
@@ -145,6 +145,15 @@ export function _injectSdkForTests(sdk: SdkLike): void {
145
145
  * support tools (or that haven't been wired into the per-family dispatcher
146
146
  * yet) raise structured errors.
147
147
  */
148
+ const CAPABILITIES: ModelCapabilities = Object.freeze({
149
+ embed: true,
150
+ generate: true,
151
+ stream: true,
152
+ tools: true,
153
+ adapters: false,
154
+ structuredOutput: false,
155
+ });
156
+
148
157
  export class BedrockBackend implements ModelBackend {
149
158
  readonly name = 'bedrock';
150
159
  readonly #region?: string;
@@ -159,7 +168,7 @@ export class BedrockBackend implements ModelBackend {
159
168
  }
160
169
 
161
170
  capabilities(): ModelCapabilities {
162
- return { embed: true, generate: true, stream: true, tools: true, adapters: false };
171
+ return CAPABILITIES;
163
172
  }
164
173
 
165
174
  async embed(input: string | string[], opts: BackendOpts<EmbedOpts>): Promise<ModelCallResult<Float32Array[]>> {
@@ -49,6 +49,7 @@ import * as auth from '../security/auth.ts';
49
49
  import * as mqtt from '../server/mqtt.ts';
50
50
  import { getConfigFilePath, getConfigObj, getConfigPath } from '../config/configUtils.ts';
51
51
  import { bootstrapModels, startModelsConfigHotReload } from '../resources/models/bootstrap.ts';
52
+ import { declareDecisionTablesAtBoot } from '../resources/models/decisionStore.ts';
52
53
  import { ErrorResource } from '../resources/ErrorResource.ts';
53
54
  import { Scope } from './Scope.ts';
54
55
  import { ApplicationScope } from './ApplicationScope.ts';
@@ -690,13 +691,18 @@ function sequentiallyHandleApplication(scope: Scope, plugin: PluginModule) {
690
691
  whenResolved(sequentiallyHandleApplication(scope, plugin));
691
692
  };
692
693
  const store = Status.primaryStore;
693
- const lockAcquired = store.tryLock(scope.pluginName, callback);
694
+ // Keyed per (application, plugin), not the plugin name alone: applications load concurrently,
695
+ // and a plugin-wide lock lets one application's hung handleApplication starve every other
696
+ // application's load of that plugin into the timeout below (#3184). NUL separator: appName
697
+ // can contain dots and slashes, so a printable separator could alias two distinct pairs.
698
+ const lockId = `${scope.appName}\0${scope.pluginName}`;
699
+ const lockAcquired = store.tryLock(lockId, callback);
694
700
 
695
701
  if (!lockAcquired) {
696
702
  return new Promise((resolve, reject) => {
697
703
  whenResolved = resolve;
698
704
  timer = setTimeout(() => {
699
- reject(new Error(`Timeout waiting for lock on ${scope.pluginName}`));
705
+ reject(new Error(`Timeout waiting for lock on ${scope.pluginName} on behalf of ${scope.appName}`));
700
706
  }, timeout + 5_000); // extra time for lock acquisition
701
707
  });
702
708
  }
@@ -712,13 +718,14 @@ function sequentiallyHandleApplication(scope: Scope, plugin: PluginModule) {
712
718
  timeout
713
719
  );
714
720
  } finally {
715
- Status.primaryStore.unlock(scope.pluginName);
721
+ Status.primaryStore.unlock(lockId);
716
722
  }
717
723
  });
718
724
  }
719
725
 
720
726
  function withDeployAwareTimeout<T>(operation: Promise<T>, scope: Scope, timeout: number): Promise<T> {
721
727
  return new Promise((resolve, reject) => {
728
+ const followsDeploys = !scope.isTransientValidation;
722
729
  const absoluteTimeout = timeout + 6 * 60 * 60 * 1000;
723
730
  let remaining = timeout;
724
731
  let activeSince = 0;
@@ -739,7 +746,7 @@ function withDeployAwareTimeout<T>(operation: Promise<T>, scope: Scope, timeout:
739
746
  absoluteTimer = setTimeout(() => rejectTimeout(absoluteTimeout), absoluteTimeout);
740
747
  absoluteTimer.unref?.();
741
748
  const arm = () => {
742
- if (timer || deployLifecycle.loadsAwaitDeploy(scope.appName)) return;
749
+ if (timer || (followsDeploys && deployLifecycle.loadsAwaitDeploy(scope.appName))) return;
743
750
  if (remaining <= 0) return rejectTimeout();
744
751
  activeSince = Date.now();
745
752
  timer = setTimeout(rejectTimeout, remaining);
@@ -754,8 +761,10 @@ function withDeployAwareTimeout<T>(operation: Promise<T>, scope: Scope, timeout:
754
761
  if (componentName === scope.appName) arm();
755
762
  }
756
763
 
757
- deployLifecycle.on('deploy:start', handleDeployStart);
758
- deployLifecycle.on('deploy:end', handleDeployEnd);
764
+ if (followsDeploys) {
765
+ deployLifecycle.on('deploy:start', handleDeployStart);
766
+ deployLifecycle.on('deploy:end', handleDeployEnd);
767
+ }
759
768
  operation.then(
760
769
  (value) => {
761
770
  cleanup();
@@ -885,6 +894,7 @@ export async function loadComponent(
885
894
  if (isRoot) {
886
895
  await bootstrapModels(config);
887
896
  startModelsConfigHotReload();
897
+ declareDecisionTablesAtBoot();
888
898
  }
889
899
 
890
900
  // The `env:` block declares the component's environment expectations (string literal →
@@ -1073,19 +1083,18 @@ export async function loadComponent(
1073
1083
  isRoot,
1074
1084
  // A root-declared plugin reads its own `host`/`urlPath` straight from this
1075
1085
  // config, so only an inherited application mount applies here.
1076
- mount
1086
+ mount,
1087
+ // Marked so plugins with process-global side effects (e.g. the scheduler
1088
+ // registering jobs into its engine) can validate without activating —
1089
+ // validation scopes may reuse a live component's identity, so activating
1090
+ // from one can displace the real component's registrations.
1091
+ Boolean(options.collectScopes)
1077
1092
  );
1078
1093
 
1079
1094
  if (options.collectScopes) {
1080
1095
  // A transient/validation load owns these scopes and closes them itself once the
1081
1096
  // load is validated (see operations.js deploy pre-flight). Skip the worker-shutdown
1082
- // auto-close so their deploy-lifecycle listeners — and this SHUTDOWN handler — don't
1083
- // accumulate across deploys (#1462).
1084
- // Mark it so plugins with process-global side effects (e.g. the scheduler
1085
- // registering jobs into its engine) can validate without activating —
1086
- // validation scopes may reuse a live component's identity, so activating
1087
- // from one can displace the real component's registrations.
1088
- scope.isTransientValidation = true;
1097
+ // auto-close so this SHUTDOWN handler doesn't accumulate across deploys (#1462).
1089
1098
  options.collectScopes.add(scope);
1090
1099
  } else {
1091
1100
  // Track the close so the worker's shutdown path waits for it (and thus for any async
@@ -360,7 +360,9 @@ export function makeOperationToolHandler(operationName: string) {
360
360
  } catch (err) {
361
361
  const e = err as { message?: string; http_resp_msg?: string; statusCode?: number };
362
362
  const message = e?.http_resp_msg ?? e?.message ?? `operation '${operationName}' failed`;
363
- harperLogger.trace(`MCP operations/${operationName} threw: ${(err as Error).stack ?? message}`);
363
+ harperLogger.trace(
364
+ `MCP operations/${operationName} threw: ${(err as Error)?.stack ?? (err as Error)?.message ?? message}`
365
+ );
364
366
  return {
365
367
  isError: true,
366
368
  content: [