harper 5.1.20 → 5.2.0-alpha.5

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 (395) hide show
  1. package/agent/operations.ts +13 -16
  2. package/components/Application.ts +188 -7
  3. package/components/ApplicationScope.ts +3 -0
  4. package/components/Scope.ts +12 -0
  5. package/components/anthropic/index.ts +9 -2
  6. package/components/bedrock/index.ts +25 -2
  7. package/components/componentLoader.ts +56 -7
  8. package/components/componentSecrets.ts +451 -0
  9. package/components/deploymentRecorder.ts +7 -1
  10. package/components/mcp/adapters/fastify.ts +3 -0
  11. package/components/mcp/adapters/harperHttp.ts +3 -0
  12. package/components/mcp/audit.ts +21 -6
  13. package/components/mcp/customResourceRegistry.ts +17 -38
  14. package/components/mcp/listChanged.ts +37 -20
  15. package/components/mcp/quota.ts +145 -0
  16. package/components/mcp/rateLimit.ts +152 -14
  17. package/components/mcp/resources.ts +2 -4
  18. package/components/mcp/toolRegistry.ts +73 -4
  19. package/components/mcp/tools/application.ts +77 -8
  20. package/components/mcp/tools/operations.ts +106 -36
  21. package/components/mcp/tools/schemas/operationDescriptions.ts +4 -4
  22. package/components/mcp/tools/schemas/operations.ts +5 -0
  23. package/components/mcp/transport.ts +55 -4
  24. package/components/ollama/index.ts +6 -2
  25. package/components/openai/index.ts +9 -2
  26. package/components/operations.js +204 -10
  27. package/components/operationsValidation.js +173 -0
  28. package/components/secretOperations.ts +498 -0
  29. package/config/configUtils.ts +25 -1
  30. package/config-app.schema.json +81 -0
  31. package/config-root.schema.json +8 -1
  32. package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -3
  33. package/dataLayer/readAuditLog.ts +14 -0
  34. package/dataLayer/schemaDescribe.ts +1 -0
  35. package/dist/agent/operations.d.ts +7 -4
  36. package/dist/agent/operations.js +13 -15
  37. package/dist/agent/operations.js.map +1 -1
  38. package/dist/components/Application.d.ts +25 -2
  39. package/dist/components/Application.js +166 -7
  40. package/dist/components/Application.js.map +1 -1
  41. package/dist/components/ApplicationScope.d.ts +2 -0
  42. package/dist/components/ApplicationScope.js +3 -0
  43. package/dist/components/ApplicationScope.js.map +1 -1
  44. package/dist/components/Scope.d.ts +8 -0
  45. package/dist/components/Scope.js +11 -0
  46. package/dist/components/Scope.js.map +1 -1
  47. package/dist/components/anthropic/index.d.ts +4 -1
  48. package/dist/components/anthropic/index.js +4 -2
  49. package/dist/components/anthropic/index.js.map +1 -1
  50. package/dist/components/bedrock/index.d.ts +4 -1
  51. package/dist/components/bedrock/index.js +22 -2
  52. package/dist/components/bedrock/index.js.map +1 -1
  53. package/dist/components/componentLoader.d.ts +2 -0
  54. package/dist/components/componentLoader.js +52 -10
  55. package/dist/components/componentLoader.js.map +1 -1
  56. package/dist/components/componentSecrets.d.ts +63 -0
  57. package/dist/components/componentSecrets.js +405 -0
  58. package/dist/components/componentSecrets.js.map +1 -0
  59. package/dist/components/deploymentRecorder.d.ts +3 -0
  60. package/dist/components/deploymentRecorder.js +4 -3
  61. package/dist/components/deploymentRecorder.js.map +1 -1
  62. package/dist/components/mcp/adapters/fastify.d.ts +2 -0
  63. package/dist/components/mcp/adapters/fastify.js +1 -0
  64. package/dist/components/mcp/adapters/fastify.js.map +1 -1
  65. package/dist/components/mcp/adapters/harperHttp.d.ts +2 -0
  66. package/dist/components/mcp/adapters/harperHttp.js +1 -0
  67. package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
  68. package/dist/components/mcp/audit.d.ts +2 -2
  69. package/dist/components/mcp/audit.js +20 -5
  70. package/dist/components/mcp/audit.js.map +1 -1
  71. package/dist/components/mcp/customResourceRegistry.d.ts +5 -12
  72. package/dist/components/mcp/customResourceRegistry.js +15 -30
  73. package/dist/components/mcp/customResourceRegistry.js.map +1 -1
  74. package/dist/components/mcp/listChanged.d.ts +7 -2
  75. package/dist/components/mcp/listChanged.js +28 -0
  76. package/dist/components/mcp/listChanged.js.map +1 -1
  77. package/dist/components/mcp/quota.d.ts +31 -0
  78. package/dist/components/mcp/quota.js +155 -0
  79. package/dist/components/mcp/quota.js.map +1 -0
  80. package/dist/components/mcp/rateLimit.d.ts +19 -3
  81. package/dist/components/mcp/rateLimit.js +129 -13
  82. package/dist/components/mcp/rateLimit.js.map +1 -1
  83. package/dist/components/mcp/resources.js.map +1 -1
  84. package/dist/components/mcp/toolRegistry.d.ts +39 -1
  85. package/dist/components/mcp/toolRegistry.js +60 -5
  86. package/dist/components/mcp/toolRegistry.js.map +1 -1
  87. package/dist/components/mcp/tools/application.d.ts +8 -1
  88. package/dist/components/mcp/tools/application.js +65 -5
  89. package/dist/components/mcp/tools/application.js.map +1 -1
  90. package/dist/components/mcp/tools/operations.d.ts +15 -3
  91. package/dist/components/mcp/tools/operations.js +99 -38
  92. package/dist/components/mcp/tools/operations.js.map +1 -1
  93. package/dist/components/mcp/tools/schemas/operationDescriptions.js +4 -4
  94. package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
  95. package/dist/components/mcp/tools/schemas/operations.js +4 -0
  96. package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
  97. package/dist/components/mcp/transport.d.ts +6 -0
  98. package/dist/components/mcp/transport.js +47 -3
  99. package/dist/components/mcp/transport.js.map +1 -1
  100. package/dist/components/ollama/index.d.ts +4 -1
  101. package/dist/components/ollama/index.js +4 -2
  102. package/dist/components/ollama/index.js.map +1 -1
  103. package/dist/components/openai/index.d.ts +4 -1
  104. package/dist/components/openai/index.js +4 -2
  105. package/dist/components/openai/index.js.map +1 -1
  106. package/dist/components/operations.d.ts +39 -0
  107. package/dist/components/operations.js +194 -2
  108. package/dist/components/operations.js.map +1 -1
  109. package/dist/components/operationsValidation.d.ts +38 -0
  110. package/dist/components/operationsValidation.js +155 -0
  111. package/dist/components/operationsValidation.js.map +1 -1
  112. package/dist/components/secretOperations.d.ts +113 -0
  113. package/dist/components/secretOperations.js +475 -0
  114. package/dist/components/secretOperations.js.map +1 -0
  115. package/dist/config/configUtils.d.ts +4 -1
  116. package/dist/config/configUtils.js +18 -1
  117. package/dist/config/configUtils.js.map +1 -1
  118. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -3
  119. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
  120. package/dist/dataLayer/readAuditLog.js +11 -0
  121. package/dist/dataLayer/readAuditLog.js.map +1 -1
  122. package/dist/dataLayer/schemaDescribe.js +2 -0
  123. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  124. package/dist/globals.d.ts +1 -1
  125. package/dist/globals.js +1 -0
  126. package/dist/globals.js.map +1 -1
  127. package/dist/index.d.ts +3 -0
  128. package/dist/index.js +1 -0
  129. package/dist/index.js.map +1 -1
  130. package/dist/json/systemSchema.json +38 -0
  131. package/dist/resources/DatabaseTransaction.d.ts +31 -3
  132. package/dist/resources/DatabaseTransaction.js +190 -29
  133. package/dist/resources/DatabaseTransaction.js.map +1 -1
  134. package/dist/resources/LMDBTransaction.d.ts +1 -1
  135. package/dist/resources/LMDBTransaction.js +43 -5
  136. package/dist/resources/LMDBTransaction.js.map +1 -1
  137. package/dist/resources/PrimaryRocksDatabase.d.ts +51 -0
  138. package/dist/resources/PrimaryRocksDatabase.js +194 -0
  139. package/dist/resources/PrimaryRocksDatabase.js.map +1 -0
  140. package/dist/resources/RecordEncoder.d.ts +1 -0
  141. package/dist/resources/RecordEncoder.js +81 -75
  142. package/dist/resources/RecordEncoder.js.map +1 -1
  143. package/dist/resources/RequestTarget.d.ts +19 -0
  144. package/dist/resources/RequestTarget.js.map +1 -1
  145. package/dist/resources/Resource.d.ts +0 -1
  146. package/dist/resources/Resource.js +103 -16
  147. package/dist/resources/Resource.js.map +1 -1
  148. package/dist/resources/ResourceInterface.d.ts +6 -2
  149. package/dist/resources/ResourceInterface.js.map +1 -1
  150. package/dist/resources/Resources.js.map +1 -1
  151. package/dist/resources/Table.d.ts +6 -4
  152. package/dist/resources/Table.js +247 -24
  153. package/dist/resources/Table.js.map +1 -1
  154. package/dist/resources/analytics/read.js +25 -28
  155. package/dist/resources/analytics/read.js.map +1 -1
  156. package/dist/resources/blob.js +165 -22
  157. package/dist/resources/blob.js.map +1 -1
  158. package/dist/resources/crdt.d.ts +2 -0
  159. package/dist/resources/crdt.js +45 -36
  160. package/dist/resources/crdt.js.map +1 -1
  161. package/dist/resources/databases.d.ts +1 -0
  162. package/dist/resources/databases.js +134 -7
  163. package/dist/resources/databases.js.map +1 -1
  164. package/dist/resources/graphql.js +8 -0
  165. package/dist/resources/graphql.js.map +1 -1
  166. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +35 -5
  167. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +239 -14
  168. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  169. package/dist/resources/jsResource.js +6 -0
  170. package/dist/resources/jsResource.js.map +1 -1
  171. package/dist/resources/loadEnv.js +30 -3
  172. package/dist/resources/loadEnv.js.map +1 -1
  173. package/dist/resources/login.js +2 -2
  174. package/dist/resources/login.js.map +1 -1
  175. package/dist/resources/models/Models.js +14 -3
  176. package/dist/resources/models/Models.js.map +1 -1
  177. package/dist/resources/models/embedHook.js +10 -2
  178. package/dist/resources/models/embedHook.js.map +1 -1
  179. package/dist/resources/models/openaiStream.d.ts +56 -0
  180. package/dist/resources/models/openaiStream.js +94 -0
  181. package/dist/resources/models/openaiStream.js.map +1 -0
  182. package/dist/resources/replayLogs.js +4 -3
  183. package/dist/resources/replayLogs.js.map +1 -1
  184. package/dist/resources/search.d.ts +1 -1
  185. package/dist/resources/search.js +104 -11
  186. package/dist/resources/search.js.map +1 -1
  187. package/dist/resources/secretDecryptor.d.ts +54 -0
  188. package/dist/resources/secretDecryptor.js +131 -0
  189. package/dist/resources/secretDecryptor.js.map +1 -0
  190. package/dist/resources/tracked.js +15 -5
  191. package/dist/resources/tracked.js.map +1 -1
  192. package/dist/security/auth.js +71 -8
  193. package/dist/security/auth.js.map +1 -1
  194. package/dist/security/jsLoader.js +11 -0
  195. package/dist/security/jsLoader.js.map +1 -1
  196. package/dist/security/tokenAuthentication.d.ts +9 -1
  197. package/dist/security/tokenAuthentication.js +31 -1
  198. package/dist/security/tokenAuthentication.js.map +1 -1
  199. package/dist/server/DurableSubscriptionsSession.d.ts +1 -1
  200. package/dist/server/DurableSubscriptionsSession.js +8 -4
  201. package/dist/server/DurableSubscriptionsSession.js.map +1 -1
  202. package/dist/server/REST.js +28 -9
  203. package/dist/server/REST.js.map +1 -1
  204. package/dist/server/fastifyRoutes.js +11 -1
  205. package/dist/server/fastifyRoutes.js.map +1 -1
  206. package/dist/server/graphqlQuerying.js +4 -3
  207. package/dist/server/graphqlQuerying.js.map +1 -1
  208. package/dist/server/http.d.ts +40 -2
  209. package/dist/server/http.js +560 -77
  210. package/dist/server/http.js.map +1 -1
  211. package/dist/server/itc/serverHandlers.js +64 -0
  212. package/dist/server/itc/serverHandlers.js.map +1 -1
  213. package/dist/server/liveSubscriptionAuth.d.ts +12 -0
  214. package/dist/server/liveSubscriptionAuth.js +140 -0
  215. package/dist/server/liveSubscriptionAuth.js.map +1 -0
  216. package/dist/server/loadRootComponents.js +2 -1
  217. package/dist/server/loadRootComponents.js.map +1 -1
  218. package/dist/server/middlewareChain.d.ts +44 -1
  219. package/dist/server/middlewareChain.js +86 -10
  220. package/dist/server/middlewareChain.js.map +1 -1
  221. package/dist/server/mqtt.js +2 -1
  222. package/dist/server/mqtt.js.map +1 -1
  223. package/dist/server/operationsServer.js +1 -1
  224. package/dist/server/operationsServer.js.map +1 -1
  225. package/dist/server/serverHelpers/Headers.d.ts +9 -0
  226. package/dist/server/serverHelpers/Headers.js +22 -0
  227. package/dist/server/serverHelpers/Headers.js.map +1 -1
  228. package/dist/server/serverHelpers/JSONStream.js +3 -3
  229. package/dist/server/serverHelpers/JSONStream.js.map +1 -1
  230. package/dist/server/serverHelpers/Request.d.ts +52 -1
  231. package/dist/server/serverHelpers/Request.js +122 -1
  232. package/dist/server/serverHelpers/Request.js.map +1 -1
  233. package/dist/server/serverHelpers/contentTypes.js +3 -8
  234. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  235. package/dist/server/serverHelpers/progressEmitter.d.ts +19 -0
  236. package/dist/server/serverHelpers/progressEmitter.js +52 -4
  237. package/dist/server/serverHelpers/progressEmitter.js.map +1 -1
  238. package/dist/server/serverHelpers/registeredOperations.d.ts +40 -0
  239. package/dist/server/serverHelpers/registeredOperations.js +286 -0
  240. package/dist/server/serverHelpers/registeredOperations.js.map +1 -0
  241. package/dist/server/serverHelpers/serverHandlers.js +11 -5
  242. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  243. package/dist/server/serverHelpers/serverUtilities.d.ts +1 -0
  244. package/dist/server/serverHelpers/serverUtilities.js +67 -5
  245. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  246. package/dist/server/serverHelpers/uwsServer.d.ts +80 -0
  247. package/dist/server/serverHelpers/uwsServer.js +433 -0
  248. package/dist/server/serverHelpers/uwsServer.js.map +1 -0
  249. package/dist/server/static.d.ts +0 -15
  250. package/dist/server/static.js +161 -2
  251. package/dist/server/static.js.map +1 -1
  252. package/dist/server/status/index.d.ts +4 -1
  253. package/dist/server/status/index.js +64 -4
  254. package/dist/server/status/index.js.map +1 -1
  255. package/dist/server/threads/manageThreads.d.ts +10 -0
  256. package/dist/server/threads/manageThreads.js +107 -4
  257. package/dist/server/threads/manageThreads.js.map +1 -1
  258. package/dist/server/threads/socketRouter.js +5 -1
  259. package/dist/server/threads/socketRouter.js.map +1 -1
  260. package/dist/server/threads/threadServer.js +92 -14
  261. package/dist/server/threads/threadServer.js.map +1 -1
  262. package/dist/upgrade/directives/5-2-0.d.ts +7 -0
  263. package/dist/upgrade/directives/5-2-0.js +108 -0
  264. package/dist/upgrade/directives/5-2-0.js.map +1 -0
  265. package/dist/upgrade/directives/directivesController.js +2 -1
  266. package/dist/upgrade/directives/directivesController.js.map +1 -1
  267. package/dist/utility/common_utils.js +5 -0
  268. package/dist/utility/common_utils.js.map +1 -1
  269. package/dist/utility/envFile.d.ts +41 -0
  270. package/dist/utility/envFile.js +221 -0
  271. package/dist/utility/envFile.js.map +1 -0
  272. package/dist/utility/globalSchema.d.ts +9 -0
  273. package/dist/utility/hdbTerms.d.ts +25 -0
  274. package/dist/utility/hdbTerms.js +31 -0
  275. package/dist/utility/hdbTerms.js.map +1 -1
  276. package/dist/utility/logging/harper_logger.d.ts +21 -0
  277. package/dist/utility/logging/harper_logger.js +160 -8
  278. package/dist/utility/logging/harper_logger.js.map +1 -1
  279. package/dist/utility/logging/logRotator.js +29 -17
  280. package/dist/utility/logging/logRotator.js.map +1 -1
  281. package/dist/utility/logging/readLog.d.ts +1 -1
  282. package/dist/utility/logging/readLog.js +305 -2
  283. package/dist/utility/logging/readLog.js.map +1 -1
  284. package/dist/utility/operationPermissions.d.ts +18 -0
  285. package/dist/utility/operationPermissions.js +35 -1
  286. package/dist/utility/operationPermissions.js.map +1 -1
  287. package/dist/utility/operation_authorization.d.ts +17 -0
  288. package/dist/utility/operation_authorization.js +38 -0
  289. package/dist/utility/operation_authorization.js.map +1 -1
  290. package/dist/utility/secretEnvelope.d.ts +30 -0
  291. package/dist/utility/secretEnvelope.js +94 -0
  292. package/dist/utility/secretEnvelope.js.map +1 -0
  293. package/dist/utility/signalling.d.ts +7 -0
  294. package/dist/utility/signalling.js +16 -0
  295. package/dist/utility/signalling.js.map +1 -1
  296. package/dist/validation/configValidator.js +70 -24
  297. package/dist/validation/configValidator.js.map +1 -1
  298. package/index.ts +4 -0
  299. package/json/systemSchema.json +38 -0
  300. package/npm-shrinkwrap.json +15066 -9347
  301. package/package.json +13 -8
  302. package/resources/DESIGN.md +15 -12
  303. package/resources/DatabaseTransaction.ts +206 -37
  304. package/resources/LMDBTransaction.ts +43 -9
  305. package/resources/PrimaryRocksDatabase.ts +201 -0
  306. package/resources/RecordEncoder.ts +81 -69
  307. package/resources/RequestTarget.ts +21 -0
  308. package/resources/Resource.ts +109 -25
  309. package/resources/ResourceInterface.ts +6 -2
  310. package/resources/Resources.ts +1 -3
  311. package/resources/Table.ts +238 -29
  312. package/resources/analytics/read.ts +25 -30
  313. package/resources/blob.ts +159 -23
  314. package/resources/crdt.ts +37 -31
  315. package/resources/databases.ts +143 -6
  316. package/resources/graphql.ts +8 -0
  317. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +251 -17
  318. package/resources/jsResource.ts +6 -0
  319. package/resources/loadEnv.ts +34 -3
  320. package/resources/login.ts +2 -2
  321. package/resources/models/Models.ts +19 -3
  322. package/resources/models/embedHook.ts +13 -2
  323. package/resources/models/openaiStream.ts +133 -0
  324. package/resources/replayLogs.ts +4 -3
  325. package/resources/search.ts +113 -11
  326. package/resources/secretDecryptor.ts +159 -0
  327. package/resources/tracked.ts +24 -17
  328. package/security/auth.ts +70 -10
  329. package/security/jsLoader.ts +12 -0
  330. package/security/tokenAuthentication.ts +42 -1
  331. package/server/DESIGN.md +10 -0
  332. package/server/DurableSubscriptionsSession.ts +8 -4
  333. package/server/REST.ts +26 -9
  334. package/server/fastifyRoutes.ts +10 -1
  335. package/server/graphqlQuerying.ts +4 -3
  336. package/server/http.ts +563 -77
  337. package/server/itc/serverHandlers.js +70 -0
  338. package/server/liveSubscriptionAuth.ts +152 -0
  339. package/server/loadRootComponents.js +2 -1
  340. package/server/middlewareChain.ts +107 -17
  341. package/server/mqtt.ts +2 -1
  342. package/server/operationsServer.ts +2 -2
  343. package/server/serverHelpers/Headers.ts +24 -0
  344. package/server/serverHelpers/JSONStream.ts +3 -3
  345. package/server/serverHelpers/Request.ts +127 -0
  346. package/server/serverHelpers/contentTypes.ts +3 -8
  347. package/server/serverHelpers/progressEmitter.ts +55 -5
  348. package/server/serverHelpers/registeredOperations.ts +259 -0
  349. package/server/serverHelpers/serverHandlers.js +11 -5
  350. package/server/serverHelpers/serverUtilities.ts +88 -5
  351. package/server/serverHelpers/uwsServer.ts +488 -0
  352. package/server/static.ts +180 -2
  353. package/server/status/index.ts +72 -4
  354. package/server/threads/manageThreads.js +102 -4
  355. package/server/threads/socketRouter.ts +5 -1
  356. package/server/threads/threadServer.js +93 -10
  357. package/studio/web/assets/{Chat-SdD1EZca.js → Chat-Cv_2paZE.js} +2 -2
  358. package/studio/web/assets/{Chat-SdD1EZca.js.map → Chat-Cv_2paZE.js.map} +1 -1
  359. package/studio/web/assets/{FloatingChat-GMq6tHK9.js → FloatingChat-CPyPIcVR.js} +4 -4
  360. package/studio/web/assets/{FloatingChat-GMq6tHK9.js.map → FloatingChat-CPyPIcVR.js.map} +1 -1
  361. package/studio/web/assets/{applications-B5hhv7uA.js → applications-C0jT2vdZ.js} +2 -2
  362. package/studio/web/assets/{applications-B5hhv7uA.js.map → applications-C0jT2vdZ.js.map} +1 -1
  363. package/studio/web/assets/{index-VFcpNWgq.js → index-D_GKOkhn.js} +6 -6
  364. package/studio/web/assets/index-D_GKOkhn.js.map +1 -0
  365. package/studio/web/assets/{index.lazy-DgcJojIA.js → index.lazy-CAmCIA65.js} +4 -4
  366. package/studio/web/assets/{index.lazy-DgcJojIA.js.map → index.lazy-CAmCIA65.js.map} +1 -1
  367. package/studio/web/assets/{profile-BqN-8vzm.js → profile-CaF-4aZe.js} +2 -2
  368. package/studio/web/assets/{profile-BqN-8vzm.js.map → profile-CaF-4aZe.js.map} +1 -1
  369. package/studio/web/assets/{setComponentFile-CHFvFq9u.js → setComponentFile-DLW1DHCt.js} +2 -2
  370. package/studio/web/assets/{setComponentFile-CHFvFq9u.js.map → setComponentFile-DLW1DHCt.js.map} +1 -1
  371. package/studio/web/assets/{setup-CQ3vWJSr.js → setup-TGCErIhq.js} +2 -2
  372. package/studio/web/assets/{setup-CQ3vWJSr.js.map → setup-TGCErIhq.js.map} +1 -1
  373. package/studio/web/assets/{status-BsW5fjuh.js → status-DG0Maoao.js} +2 -2
  374. package/studio/web/assets/{status-BsW5fjuh.js.map → status-DG0Maoao.js.map} +1 -1
  375. package/studio/web/assets/{swagger-ui-react-DITdeB9-.js → swagger-ui-react-8T4SgQ1m.js} +2 -2
  376. package/studio/web/assets/{swagger-ui-react-DITdeB9-.js.map → swagger-ui-react-8T4SgQ1m.js.map} +1 -1
  377. package/studio/web/assets/{tsMode-Dyph-OUs.js → tsMode-DJ6Sl24Q.js} +2 -2
  378. package/studio/web/assets/{tsMode-Dyph-OUs.js.map → tsMode-DJ6Sl24Q.js.map} +1 -1
  379. package/studio/web/assets/{useEntityRestURL-D0QTUqex.js → useEntityRestURL-JO2mfWTQ.js} +2 -2
  380. package/studio/web/assets/{useEntityRestURL-D0QTUqex.js.map → useEntityRestURL-JO2mfWTQ.js.map} +1 -1
  381. package/studio/web/index.html +1 -1
  382. package/upgrade/directives/5-2-0.ts +82 -0
  383. package/upgrade/directives/directivesController.ts +2 -1
  384. package/utility/common_utils.ts +5 -0
  385. package/utility/envFile.ts +218 -0
  386. package/utility/hdbTerms.ts +31 -0
  387. package/utility/logging/harper_logger.ts +156 -8
  388. package/utility/logging/logRotator.ts +24 -14
  389. package/utility/logging/readLog.ts +323 -2
  390. package/utility/operationPermissions.ts +35 -1
  391. package/utility/operation_authorization.ts +74 -1
  392. package/utility/secretEnvelope.ts +113 -0
  393. package/utility/signalling.ts +15 -0
  394. package/validation/configValidator.ts +78 -25
  395. package/studio/web/assets/index-VFcpNWgq.js.map +0 -1
@@ -58,27 +58,20 @@ interface CompiledDef {
58
58
  def: CustomResourceDef;
59
59
  /** Present for template entries only. */
60
60
  regex?: RegExp;
61
- params?: TemplateParam[];
62
- }
63
-
64
- interface TemplateParam {
65
- name: string;
66
- /** `{+name}` — reserved expansion, may span segments. */
67
- reserved: boolean;
61
+ paramNames?: string[];
68
62
  }
69
63
 
70
64
  const registry = new Map<McpProfile, CompiledDef[]>();
71
65
 
72
66
  /**
73
67
  * Compile a URI template into a matcher. `{name}` matches a single path
74
- * segment (`[^/]+`), and an encoded separator `%2F`/`%5C` — in the capture
75
- * rejects the match, so the one-segment contract survives percent-decoding);
76
- * `{+name}` matches across segments (`.+`), mirroring RFC 6570 level-2
77
- * reserved expansion, which is how MCP clients construct URIs from templates.
78
- * Throws on malformed templates so registration can warn-and-skip the entry.
68
+ * segment (`[^/]+`); `{+name}` matches across segments (`.+`), mirroring
69
+ * RFC 6570 level-2 reserved expansion, which is how MCP clients construct
70
+ * URIs from templates. Throws on malformed templates so registration can
71
+ * warn-and-skip the entry.
79
72
  */
80
- export function compileUriTemplate(template: string): { regex: RegExp; params: TemplateParam[] } {
81
- const params: TemplateParam[] = [];
73
+ export function compileUriTemplate(template: string): { regex: RegExp; paramNames: string[] } {
74
+ const paramNames: string[] = [];
82
75
  let pattern = '';
83
76
  let index = 0;
84
77
  while (index < template.length) {
@@ -96,25 +89,18 @@ export function compileUriTemplate(template: string): { regex: RegExp; params: T
96
89
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
97
90
  throw new Error(`invalid template parameter '{${template.slice(open + 1, close)}}' in uriTemplate: ${template}`);
98
91
  }
99
- if (params.some((p) => p.name === name)) {
92
+ if (paramNames.includes(name)) {
100
93
  // a repeated name would silently overwrite the earlier captured value
101
94
  throw new Error(`duplicate template parameter '{${name}}' in uriTemplate: ${template}`);
102
95
  }
103
- params.push({ name, reserved });
96
+ paramNames.push(name);
104
97
  pattern += reserved ? '(.+)' : '([^/]+)';
105
98
  index = close + 1;
106
99
  }
107
- if (params.length === 0) throw new Error(`uriTemplate has no parameters (use \`uri\` instead): ${template}`);
108
- return { regex: new RegExp(`^${pattern}$`), params };
100
+ if (paramNames.length === 0) throw new Error(`uriTemplate has no parameters (use \`uri\` instead): ${template}`);
101
+ return { regex: new RegExp(`^${pattern}$`), paramNames };
109
102
  }
110
103
 
111
- // `{name}` captures run against the still-encoded URI, so a client can smuggle
112
- // a separator through the `[^/]+` class as %2F (or %5C) and have it decode to
113
- // a real slash/backslash AFTER the segment boundary was checked — defeating
114
- // the one-segment contract authors rely on for path construction. Reject the
115
- // match instead (the URI simply doesn't fit a single-segment slot).
116
- const ENCODED_SEPARATOR = /%2f|%5c/i;
117
-
118
104
  function escapeRegex(literal: string): string {
119
105
  return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
120
106
  }
@@ -123,9 +109,9 @@ function escapeRegex(literal: string): string {
123
109
  export function addCustomResource(def: CustomResourceDef): void {
124
110
  const compiled: CompiledDef = { def };
125
111
  if (def.uriTemplate) {
126
- const { regex, params } = compileUriTemplate(def.uriTemplate);
112
+ const { regex, paramNames } = compileUriTemplate(def.uriTemplate);
127
113
  compiled.regex = regex;
128
- compiled.params = params;
114
+ compiled.paramNames = paramNames;
129
115
  }
130
116
  let list = registry.get(def.profile);
131
117
  if (!list) {
@@ -195,21 +181,14 @@ export function matchCustomResource(
195
181
  for (const { def } of list) {
196
182
  if (def.uri === uri) return { def, params: {} };
197
183
  }
198
- for (const { def, regex, params: templateParams } of list) {
199
- if (!regex || !templateParams) continue;
184
+ for (const { def, regex, paramNames } of list) {
185
+ if (!regex || !paramNames) continue;
200
186
  const match = regex.exec(uri);
201
187
  if (!match) continue;
202
188
  const params: Record<string, string> = {};
203
- let separatorSmuggled = false;
204
- for (let i = 0; i < templateParams.length; i++) {
205
- const raw = match[i + 1];
206
- if (!templateParams[i].reserved && ENCODED_SEPARATOR.test(raw)) {
207
- separatorSmuggled = true;
208
- break;
209
- }
210
- params[templateParams[i].name] = decodeURIComponentSafe(raw);
189
+ for (let i = 0; i < paramNames.length; i++) {
190
+ params[paramNames[i]] = decodeURIComponentSafe(match[i + 1]);
211
191
  }
212
- if (separatorSmuggled) continue;
213
192
  return { def, params };
214
193
  }
215
194
  return undefined;
@@ -32,23 +32,18 @@ const MAX_RESOURCES_PAGE = 1000;
32
32
  let initialized = false;
33
33
  let onUserChangeBound: (() => void) | undefined;
34
34
  let onSchemaChangeBound: (() => void) | undefined;
35
+ let onResourcesRegisteredBound: (() => void) | undefined;
36
+
37
+ interface ItcHandlers {
38
+ userHandler?: { addListener?: (fn: () => void) => void };
39
+ schemaHandler?: { addListener?: (fn: () => void) => void };
40
+ resourceHandler?: { addListener?: (fn: () => void) => void };
41
+ }
35
42
 
36
43
  // Test seams: avoid importing the real ITC handler from unit tests.
37
- let _itcHandlersOverride:
38
- | {
39
- userHandler?: { addListener?: (fn: () => void) => void };
40
- schemaHandler?: { addListener?: (fn: () => void) => void };
41
- }
42
- | undefined;
44
+ let _itcHandlersOverride: ItcHandlers | undefined;
43
45
 
44
- export function _setItcHandlersForTest(
45
- h:
46
- | {
47
- userHandler?: { addListener?: (fn: () => void) => void };
48
- schemaHandler?: { addListener?: (fn: () => void) => void };
49
- }
50
- | undefined
51
- ): void {
46
+ export function _setItcHandlersForTest(h: ItcHandlers | undefined): void {
52
47
  _itcHandlersOverride = h;
53
48
  }
54
49
 
@@ -56,14 +51,10 @@ export function _resetListChangedForTest(): void {
56
51
  initialized = false;
57
52
  onUserChangeBound = undefined;
58
53
  onSchemaChangeBound = undefined;
54
+ onResourcesRegisteredBound = undefined;
59
55
  }
60
56
 
61
- function loadItcHandlers():
62
- | {
63
- userHandler?: { addListener?: (fn: () => void) => void };
64
- schemaHandler?: { addListener?: (fn: () => void) => void };
65
- }
66
- | undefined {
57
+ function loadItcHandlers(): ItcHandlers | undefined {
67
58
  if (_itcHandlersOverride) return _itcHandlersOverride;
68
59
  try {
69
60
  return require('../../server/itc/serverHandlers');
@@ -286,6 +277,27 @@ async function onSchemaChange(): Promise<void> {
286
277
  }
287
278
  }
288
279
 
280
+ /**
281
+ * A component's JS resources (resources.js) register on the local worker AFTER the MCP component's
282
+ * own boot scan and after the schema-change rebuilds (which fire while the @table classes register —
283
+ * still before the JS subclass that extends them). That JS subclass is where authors declare
284
+ * `static mcpTools`/`mcpPrompts`, so without a rebuild here the application tool list never reflects
285
+ * them (#1448). Rebuild and re-notify application sessions; no user re-resolution (a resource
286
+ * registration doesn't change the acting user) and no operations-profile work (its tool/resource
287
+ * set isn't derived from JS resources).
288
+ */
289
+ function onResourcesRegistered(): void {
290
+ try {
291
+ refreshApplicationTools();
292
+ } catch (err) {
293
+ harperLogger.warn(`MCP listChanged refreshApplicationTools (resources) failed: ${(err as Error).message}`);
294
+ }
295
+ for (const r of snapshotSessions('application')) {
296
+ maybeNotifyToolsChanged(r);
297
+ maybeNotifyResourcesChanged(r);
298
+ }
299
+ }
300
+
289
301
  /**
290
302
  * Idempotent: subscribe once at component boot. Repeated calls are
291
303
  * no-ops. Returns true if subscriptions were actually installed (false
@@ -313,6 +325,11 @@ export function initListChanged(): boolean {
313
325
  handlers.schemaHandler.addListener(onSchemaChangeBound);
314
326
  installed++;
315
327
  }
328
+ if (handlers.resourceHandler?.addListener) {
329
+ onResourcesRegisteredBound = onResourcesRegistered;
330
+ handlers.resourceHandler.addListener(onResourcesRegisteredBound);
331
+ installed++;
332
+ }
316
333
  initialized = installed > 0;
317
334
  if (initialized) {
318
335
  harperLogger.info(`MCP listChanged: subscribed to ${installed} event channel(s)`);
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Durable, operator-pluggable quota hook for MCP `tools/call` (#1610).
3
+ *
4
+ * The in-memory buckets in `rateLimit.ts` bound instantaneous rates but are
5
+ * per-worker and reset on restart — insufficient as a COST control for a
6
+ * public unauthenticated tool (an LLM-backed `answer`, say). This hook lets
7
+ * the operator implement a durable policy (e.g. a persisted per-IP daily
8
+ * counter table) behind config:
9
+ *
10
+ * mcp:
11
+ * application:
12
+ * quota:
13
+ * resource: McpQuota # exported Resource path
14
+ * method: allowMcpCall # static method on it (this is the default)
15
+ *
16
+ * Before each admitted tools/call, Harper calls
17
+ * `QuotaClass.allowMcpCall({ identity, tool, user, profile, sessionId })`.
18
+ * Return `true` (or any truthy non-object) to allow; return
19
+ * `{ allowed: false, message?, retryAfterSeconds? }` to deny — the denial
20
+ * surfaces to the client as `isError` with `kind: 'quota_exceeded'`.
21
+ * Counting is the hook's business: increment on check, or on success via
22
+ * your own bookkeeping — Harper calls once per attempted tool call.
23
+ *
24
+ * FAIL-CLOSED: a hook that throws (or a configured resource/method that
25
+ * doesn't resolve) DENIES the call. Cost protection that silently disables
26
+ * itself on a bug is worse than a hard failure (#1422 set this precedent
27
+ * for allow* hooks). The raw error goes to the server log only.
28
+ *
29
+ * RACE-SAFETY: the hook can run concurrently for the SAME identity — within
30
+ * a worker (interleaving across the hook's own await boundaries) and across
31
+ * workers (separate processes sharing the database). A naive read-then-write
32
+ * counter (`get` → `put used+1`) can undercount under that concurrency and
33
+ * admit calls past the configured limit. Production implementations should
34
+ * make the read-modify-write atomic: run it in a transaction that serializes
35
+ * conflicting writers, use a compare-and-set retry loop, or maintain the
36
+ * counter in a store with native atomic increments.
37
+ *
38
+ * Dispatch uses the LIVE registry class, same as custom tools — an exported
39
+ * subclass replacing the entry on reload wins.
40
+ */
41
+ import * as env from '../../utility/environment/environmentManager.ts';
42
+ import { CONFIG_PARAMS } from '../../utility/hdbTerms.ts';
43
+ import harperLogger from '../../utility/logging/harper_logger.ts';
44
+ import type { McpProfile } from './transport.ts';
45
+ import type { AuthedUser } from './toolRegistry.ts';
46
+
47
+ export interface QuotaCheckInfo {
48
+ /** Client identity from `resolveClientIdentity` (socket IP or trusted-header value); may be undefined. */
49
+ identity?: string;
50
+ tool: string;
51
+ user: AuthedUser;
52
+ profile: McpProfile;
53
+ sessionId: string;
54
+ }
55
+
56
+ export interface QuotaDenial {
57
+ allowed: false;
58
+ /** Shown to the client verbatim — author-controlled, keep it safe. */
59
+ message?: string;
60
+ retryAfterSeconds?: number;
61
+ }
62
+
63
+ export type QuotaDecision = { allowed: true } | QuotaDenial;
64
+
65
+ const CONFIG_KEYS: Record<McpProfile, { resource: string; method: string }> = {
66
+ operations: {
67
+ resource: CONFIG_PARAMS.MCP_OPERATIONS_QUOTA_RESOURCE,
68
+ method: CONFIG_PARAMS.MCP_OPERATIONS_QUOTA_METHOD,
69
+ },
70
+ application: {
71
+ resource: CONFIG_PARAMS.MCP_APPLICATION_QUOTA_RESOURCE,
72
+ method: CONFIG_PARAMS.MCP_APPLICATION_QUOTA_METHOD,
73
+ },
74
+ };
75
+
76
+ const DEFAULT_METHOD = 'allowMcpCall';
77
+
78
+ type ResourcesLike = Map<string, { Resource: unknown }> | undefined;
79
+
80
+ // Test seam — mirrors resources.ts: the real registry initializes the whole
81
+ // Harper graph at import, which unit tests can't do.
82
+ let _resourcesOverride: ResourcesLike;
83
+ export function _setQuotaResourcesForTest(r: ResourcesLike): void {
84
+ _resourcesOverride = r;
85
+ }
86
+
87
+ function getResources(): ResourcesLike {
88
+ if (_resourcesOverride) return _resourcesOverride;
89
+ const { resources } = require('../../resources/Resources');
90
+ return resources as ResourcesLike;
91
+ }
92
+
93
+ /** Warn-once-per-profile state for a misconfigured hook (missing resource/method). */
94
+ const warnedMisconfigured = new Set<McpProfile>();
95
+ export function _resetQuotaWarningsForTest(): void {
96
+ warnedMisconfigured.clear();
97
+ }
98
+
99
+ /**
100
+ * Run the configured durable quota hook, if any. Returns `{allowed: true}`
101
+ * when no hook is configured (the feature is opt-in). Misconfiguration and
102
+ * hook errors DENY (fail-closed) with a sanitized message.
103
+ */
104
+ export async function checkDurableQuota(info: QuotaCheckInfo): Promise<QuotaDecision> {
105
+ const keys = CONFIG_KEYS[info.profile];
106
+ const resourcePath = env.get(keys.resource);
107
+ if (typeof resourcePath !== 'string' || !resourcePath) {
108
+ return { allowed: true };
109
+ }
110
+ const methodName =
111
+ typeof env.get(keys.method) === 'string' && env.get(keys.method) ? env.get(keys.method) : DEFAULT_METHOD;
112
+ const entry = getResources()?.get(resourcePath);
113
+ const QuotaClass = entry?.Resource as Record<string, unknown> | undefined;
114
+ const method = QuotaClass?.[methodName as string];
115
+ if (typeof method !== 'function') {
116
+ if (!warnedMisconfigured.has(info.profile)) {
117
+ warnedMisconfigured.add(info.profile);
118
+ harperLogger.warn(
119
+ `MCP ${info.profile} quota hook misconfigured: no exported resource '${resourcePath}' with static method '${methodName}'; DENYING tool calls (fail-closed)`
120
+ );
121
+ }
122
+ return { allowed: false, message: 'quota policy unavailable' };
123
+ }
124
+ try {
125
+ const result = await (method as (i: QuotaCheckInfo) => unknown).call(QuotaClass, info);
126
+ if (result && typeof result === 'object') {
127
+ const decision = result as { allowed?: unknown; message?: unknown; retryAfterSeconds?: unknown };
128
+ if (decision.allowed === false) {
129
+ return {
130
+ allowed: false,
131
+ ...(typeof decision.message === 'string' ? { message: decision.message } : {}),
132
+ ...(typeof decision.retryAfterSeconds === 'number' ? { retryAfterSeconds: decision.retryAfterSeconds } : {}),
133
+ };
134
+ }
135
+ return { allowed: true };
136
+ }
137
+ return result ? { allowed: true } : { allowed: false };
138
+ } catch (error) {
139
+ harperLogger.error(
140
+ `MCP ${info.profile} quota hook '${resourcePath}.${methodName}' threw; denying (fail-closed)`,
141
+ error
142
+ );
143
+ return { allowed: false, message: 'quota check failed' };
144
+ }
145
+ }
@@ -1,11 +1,20 @@
1
1
  /**
2
- * Per-session, per-tool rate limiting for `tools/call`.
2
+ * Per-session, per-tool, and per-client rate limiting for `tools/call`.
3
3
  *
4
- * Four configurable limits per profile (operations / application):
4
+ * Configurable limits per profile (operations / application):
5
5
  * - perToolPerSecond: sustained per-tool rate (token bucket refill)
6
6
  * - perToolBurst: per-tool burst capacity (token bucket size)
7
7
  * - sessionConcurrency: max in-flight tool calls per session
8
8
  * - sessionPerSecond: sustained per-session rate across all tools
9
+ * - perClientPerSecond / perClientBurst (#1610, default OFF): sustained
10
+ * rate keyed on CLIENT IDENTITY rather than session. Session buckets are
11
+ * trivially cycled by an anonymous client (initialize → call → drop
12
+ * session → repeat); the client bucket survives that loop.
13
+ * - identityHeader (#1610): identity is the socket IP by default; proxied
14
+ * deployments can name a trusted header (e.g. `x-forwarded-for`) whose
15
+ * first (client-most) value is used instead. Only set this when the
16
+ * proxy strips the header from untrusted traffic — a client-controlled
17
+ * header is a limit bypass.
9
18
  *
10
19
  * Limit hits surface as `result.isError = true` with `kind: 'rate_limited'`
11
20
  * (NOT a JSON-RPC error) per the MCP spec's tools-call convention. The LLM
@@ -14,21 +23,43 @@
14
23
  * State is in-memory per worker process. Buckets are evicted lazily when
15
24
  * a session's record is removed (#619 cleanup) or after they've been idle
16
25
  * past the idle eviction threshold. Multi-process coordination isn't
17
- * attempted in v1 — the limits are per-worker.
26
+ * attempted in v1 — the limits are per-worker. For durable cross-restart
27
+ * quotas (per-IP daily counters and the like), see the config-named quota
28
+ * hook in `quota.ts` (#1610).
18
29
  */
19
30
  import * as env from '../../utility/environment/environmentManager.ts';
20
31
  import { CONFIG_PARAMS } from '../../utility/hdbTerms.ts';
32
+ import harperLogger from '../../utility/logging/harper_logger.ts';
21
33
 
22
34
  export interface RateLimitConfig {
23
35
  perToolPerSecond: number;
24
36
  perToolBurst: number;
25
37
  sessionConcurrency: number;
26
38
  sessionPerSecond: number;
39
+ /** 0 disables the per-client-identity bucket (the default). */
40
+ perClientPerSecond: number;
41
+ perClientBurst: number;
42
+ /** Trusted header (lowercased) to derive client identity from; absent = socket IP. */
43
+ identityHeader?: string;
27
44
  }
28
45
 
29
46
  const DEFAULTS: Record<'operations' | 'application', RateLimitConfig> = {
30
- operations: { perToolPerSecond: 10, perToolBurst: 20, sessionConcurrency: 25, sessionPerSecond: 100 },
31
- application: { perToolPerSecond: 25, perToolBurst: 50, sessionConcurrency: 50, sessionPerSecond: 200 },
47
+ operations: {
48
+ perToolPerSecond: 10,
49
+ perToolBurst: 20,
50
+ sessionConcurrency: 25,
51
+ sessionPerSecond: 100,
52
+ perClientPerSecond: 0,
53
+ perClientBurst: 0,
54
+ },
55
+ application: {
56
+ perToolPerSecond: 25,
57
+ perToolBurst: 50,
58
+ sessionConcurrency: 50,
59
+ sessionPerSecond: 200,
60
+ perClientPerSecond: 0,
61
+ perClientBurst: 0,
62
+ },
32
63
  };
33
64
 
34
65
  const CONFIG_KEYS = {
@@ -37,30 +68,91 @@ const CONFIG_KEYS = {
37
68
  perToolBurst: CONFIG_PARAMS.MCP_OPERATIONS_RATELIMIT_PERTOOLBURST,
38
69
  sessionConcurrency: CONFIG_PARAMS.MCP_OPERATIONS_RATELIMIT_SESSIONCONCURRENCY,
39
70
  sessionPerSecond: CONFIG_PARAMS.MCP_OPERATIONS_RATELIMIT_SESSIONPERSECOND,
71
+ perClientPerSecond: CONFIG_PARAMS.MCP_OPERATIONS_RATELIMIT_PERCLIENTPERSECOND,
72
+ perClientBurst: CONFIG_PARAMS.MCP_OPERATIONS_RATELIMIT_PERCLIENTBURST,
73
+ identityHeader: CONFIG_PARAMS.MCP_OPERATIONS_RATELIMIT_IDENTITYHEADER,
40
74
  },
41
75
  application: {
42
76
  perToolPerSecond: CONFIG_PARAMS.MCP_APPLICATION_RATELIMIT_PERTOOLPERSECOND,
43
77
  perToolBurst: CONFIG_PARAMS.MCP_APPLICATION_RATELIMIT_PERTOOLBURST,
44
78
  sessionConcurrency: CONFIG_PARAMS.MCP_APPLICATION_RATELIMIT_SESSIONCONCURRENCY,
45
79
  sessionPerSecond: CONFIG_PARAMS.MCP_APPLICATION_RATELIMIT_SESSIONPERSECOND,
80
+ perClientPerSecond: CONFIG_PARAMS.MCP_APPLICATION_RATELIMIT_PERCLIENTPERSECOND,
81
+ perClientBurst: CONFIG_PARAMS.MCP_APPLICATION_RATELIMIT_PERCLIENTBURST,
82
+ identityHeader: CONFIG_PARAMS.MCP_APPLICATION_RATELIMIT_IDENTITYHEADER,
46
83
  },
47
84
  };
48
85
 
86
+ // tools/call is a hot path and `resolveClientIdentity` runs per call; reading
87
+ // 6–7 env keys and allocating a config object each time is avoidable GC/CPU
88
+ // pressure. Cache per profile with a short TTL — config edits still take
89
+ // effect within seconds, matching the per-session capture semantics closely
90
+ // enough while keeping the steady-state cost to a Map hit.
91
+ const CONFIG_TTL_MS = 10_000;
92
+ const configCache = new Map<string, { config: RateLimitConfig; at: number }>();
93
+ let warnedIdentityHeader = false;
94
+
49
95
  export function configFor(profile: 'operations' | 'application'): RateLimitConfig {
96
+ const cached = configCache.get(profile);
97
+ const t = now();
98
+ if (cached && t - cached.at < CONFIG_TTL_MS) return cached.config;
99
+ const config = buildConfig(profile);
100
+ if (config.identityHeader && !warnedIdentityHeader) {
101
+ warnedIdentityHeader = true;
102
+ harperLogger.warn(
103
+ `MCP ${profile} rateLimit.identityHeader='${config.identityHeader}' derives client identity from a request header; ensure the fronting proxy STRIPS or REPLACES this header on untrusted traffic, or clients can spoof identities and bypass per-client limits`
104
+ );
105
+ }
106
+ configCache.set(profile, { config, at: t });
107
+ return config;
108
+ }
109
+
110
+ function buildConfig(profile: 'operations' | 'application'): RateLimitConfig {
50
111
  const keys = CONFIG_KEYS[profile];
51
112
  const defaults = DEFAULTS[profile];
52
113
  const read = (key: string, fallback: number): number => {
53
114
  const v = env.get(key);
54
115
  return typeof v === 'number' && v > 0 ? v : fallback;
55
116
  };
117
+ const identityHeader = env.get(keys.identityHeader);
118
+ const perClientPerSecond = read(keys.perClientPerSecond, defaults.perClientPerSecond);
56
119
  return {
57
120
  perToolPerSecond: read(keys.perToolPerSecond, defaults.perToolPerSecond),
58
121
  perToolBurst: read(keys.perToolBurst, defaults.perToolBurst),
59
122
  sessionConcurrency: read(keys.sessionConcurrency, defaults.sessionConcurrency),
60
123
  sessionPerSecond: read(keys.sessionPerSecond, defaults.sessionPerSecond),
124
+ perClientPerSecond,
125
+ // Burst defaults to the sustained rate when unset, so enabling the
126
+ // limit is a one-key change — floored at 1 token, else a fractional
127
+ // rate (0.1/s = "6 per minute") yields a bucket that can never admit
128
+ // (consume requires a whole token and refill caps at burst).
129
+ perClientBurst: read(keys.perClientBurst, perClientPerSecond > 0 ? Math.max(1, perClientPerSecond) : 0),
130
+ ...(typeof identityHeader === 'string' && identityHeader ? { identityHeader: identityHeader.toLowerCase() } : {}),
61
131
  };
62
132
  }
63
133
 
134
+ /**
135
+ * Derive the client identity for per-client limiting and the durable quota
136
+ * hook (#1610): the configured trusted header's first (client-most) value
137
+ * when set, else the transport-provided socket IP. Returns undefined when
138
+ * neither is available — callers skip client-scoped checks then.
139
+ */
140
+ export function resolveClientIdentity(
141
+ headers: Record<string, string | undefined>,
142
+ clientIp: string | undefined,
143
+ profile: 'operations' | 'application'
144
+ ): string | undefined {
145
+ const config = configFor(profile);
146
+ if (config.identityHeader) {
147
+ const raw = headers[config.identityHeader];
148
+ if (raw) {
149
+ const first = raw.split(',')[0].trim();
150
+ if (first) return first;
151
+ }
152
+ }
153
+ return clientIp || undefined;
154
+ }
155
+
64
156
  /**
65
157
  * Token bucket: starts full at `burst`, refills at `rate` tokens per
66
158
  * second up to `burst`, drained by `tryConsume(1)`. Stateless aside from
@@ -119,12 +211,21 @@ interface SessionState {
119
211
 
120
212
  const sessions = new Map<string, SessionState>();
121
213
 
214
+ // Per-client-identity buckets, keyed `${profile}\n${identity}`. Deliberately
215
+ // SEPARATE from session state: the whole point is surviving session cycling,
216
+ // so their lifetime must not be tied to any session's (#1610).
217
+ interface ClientState {
218
+ rate: TokenBucket;
219
+ lastSeen: number;
220
+ }
221
+ const clients = new Map<string, ClientState>();
222
+
122
223
  // Belt-and-braces against state leaks: sessions that get TTL-evicted from the
123
224
  // system.mcp_session table never reach deleteSession() in this process, so
124
225
  // `clearSessionRateState` is never called for them. Prune any session that
125
226
  // hasn't admitted a call in this many ms on every getOrCreate. The threshold
126
227
  // is generously above the default idle timeout (1800s) — well-behaved live
127
- // sessions never get pruned by accident.
228
+ // sessions never get pruned by accident. Client buckets ride the same sweep.
128
229
  const IDLE_PRUNE_MS = 60 * 60 * 1000; // 1 hour
129
230
  const PRUNE_INTERVAL_MS = 5 * 60 * 1000; // run at most every 5 minutes
130
231
  let lastPruneAt = 0;
@@ -139,6 +240,11 @@ function pruneIdleSessions(): void {
139
240
  sessions.delete(id);
140
241
  }
141
242
  }
243
+ for (const [key, c] of clients) {
244
+ if (c.lastSeen < cutoff) {
245
+ clients.delete(key);
246
+ }
247
+ }
142
248
  }
143
249
 
144
250
  function getOrCreate(sessionId: string, profile: 'operations' | 'application'): SessionState {
@@ -161,29 +267,53 @@ function getOrCreate(sessionId: string, profile: 'operations' | 'application'):
161
267
  return s;
162
268
  }
163
269
 
270
+ function getOrCreateClient(
271
+ identity: string,
272
+ profile: 'operations' | 'application',
273
+ config: RateLimitConfig
274
+ ): ClientState {
275
+ const key = `${profile}\n${identity}`;
276
+ let c = clients.get(key);
277
+ if (!c) {
278
+ c = { rate: new TokenBucket(config.perClientPerSecond, config.perClientBurst), lastSeen: now() };
279
+ clients.set(key, c);
280
+ } else {
281
+ c.lastSeen = now();
282
+ }
283
+ return c;
284
+ }
285
+
164
286
  /** Drop a session's rate-limit state (called on session deletion). */
165
287
  export function clearSessionRateState(sessionId: string): void {
166
288
  sessions.delete(sessionId);
167
289
  }
168
290
 
169
- /** Test seam: drop all sessions. */
291
+ /** Test seam: drop all sessions, client buckets, and the config cache. */
170
292
  export function _resetForTest(): void {
171
293
  sessions.clear();
294
+ clients.clear();
295
+ configCache.clear();
296
+ warnedIdentityHeader = false;
172
297
  }
173
298
 
174
299
  export type RateLimitDecision =
175
300
  | { allowed: true; release: () => void }
176
- | { allowed: false; reason: 'per_tool' | 'session_rate' | 'concurrency' };
301
+ | { allowed: false; reason: 'per_tool' | 'session_rate' | 'concurrency' | 'per_client' };
177
302
 
178
303
  /**
179
304
  * Attempt to admit a tools/call. If allowed, returns a `release()` that
180
305
  * decrements in-flight; the caller MUST invoke it (even on tool failure)
181
306
  * via `try { ... } finally { release(); }`.
307
+ *
308
+ * `clientIdentity` (from `resolveClientIdentity`) engages the per-client
309
+ * bucket when the profile configures `perClientPerSecond` — the scope that
310
+ * survives session cycling (#1610). Absent identity or a 0 rate skips it.
182
311
  */
183
312
  export function tryAdmit(
184
313
  sessionId: string,
185
314
  toolName: string,
186
- profile: 'operations' | 'application'
315
+ profile: 'operations' | 'application',
316
+ clientIdentity?: string
187
317
  ): RateLimitDecision {
188
318
  const state = getOrCreate(sessionId, profile);
189
319
  if (state.inFlight >= state.config.sessionConcurrency) {
@@ -194,19 +324,27 @@ export function tryAdmit(
194
324
  toolBucket = new TokenBucket(state.config.perToolPerSecond, state.config.perToolBurst);
195
325
  state.perTool.set(toolName, toolBucket);
196
326
  }
197
- // Peek both buckets first. Consuming session-rate before checking per-tool
198
- // (or vice versa) silently drains the unrelated bucket on the denied path.
327
+ const clientState =
328
+ state.config.perClientPerSecond > 0 && clientIdentity
329
+ ? getOrCreateClient(clientIdentity, profile, state.config)
330
+ : undefined;
331
+ // Peek every bucket first. Consuming one before checking another silently
332
+ // drains the unrelated bucket on the denied path.
199
333
  if (!toolBucket.hasToken()) {
200
334
  return { allowed: false, reason: 'per_tool' };
201
335
  }
202
336
  if (!state.sessionRate.hasToken()) {
203
337
  return { allowed: false, reason: 'session_rate' };
204
338
  }
205
- // Both have capacity — actually deduct. The peeks above ran refill(), so
206
- // this immediate-follow-up tryConsume sees the same fresh state and is
207
- // guaranteed to succeed (refill() is a no-op for elapsedSec ≤ 0).
339
+ if (clientState && !clientState.rate.hasToken()) {
340
+ return { allowed: false, reason: 'per_client' };
341
+ }
342
+ // All have capacity — actually deduct. The peeks above ran refill(), so
343
+ // these immediate-follow-up tryConsume calls see the same fresh state and
344
+ // are guaranteed to succeed (refill() is a no-op for elapsedSec ≤ 0).
208
345
  toolBucket.tryConsume();
209
346
  state.sessionRate.tryConsume();
347
+ clientState?.rate.tryConsume();
210
348
  state.inFlight += 1;
211
349
  return {
212
350
  allowed: true,
@@ -393,8 +393,7 @@ export async function subscribeToResource(
393
393
  } else {
394
394
  const resourcesRegistry = getResources();
395
395
  const entry = resourcesRegistry.getMatch(path, 'mcp') as
396
- | { Resource: { subscribe?: (request: unknown, context: unknown) => unknown }; relativeURL?: string }
397
- | undefined;
396
+ { Resource: { subscribe?: (request: unknown, context: unknown) => unknown }; relativeURL?: string } | undefined;
398
397
  const ResourceClass = entry?.Resource;
399
398
  if (!entry || typeof ResourceClass?.subscribe !== 'function') return null;
400
399
  // `getMatch` matched the Resource and put the remaining path (the record key,
@@ -461,8 +460,7 @@ function enumerateParamRouteTemplates(prefix: string): ResourceTemplate[] {
461
460
  for (const route of getResources().paramRoutes ?? []) {
462
461
  if (!isMcpExposed(route.entry)) continue;
463
462
  const ResourceClass = route.entry.Resource as
464
- | { prototype?: unknown; description?: string; hidden?: boolean }
465
- | undefined;
463
+ { prototype?: unknown; description?: string; hidden?: boolean } | undefined;
466
464
  // @hidden suppresses the Resource from descriptive surfaces (MCP + OpenAPI).
467
465
  if (ResourceClass?.hidden === true) continue;
468
466
  if (!hasRestVerbs(ResourceClass?.prototype)) continue;