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.
- package/agent/operations.ts +13 -16
- package/components/Application.ts +188 -7
- package/components/ApplicationScope.ts +3 -0
- package/components/Scope.ts +12 -0
- package/components/anthropic/index.ts +9 -2
- package/components/bedrock/index.ts +25 -2
- package/components/componentLoader.ts +56 -7
- package/components/componentSecrets.ts +451 -0
- package/components/deploymentRecorder.ts +7 -1
- package/components/mcp/adapters/fastify.ts +3 -0
- package/components/mcp/adapters/harperHttp.ts +3 -0
- package/components/mcp/audit.ts +21 -6
- package/components/mcp/customResourceRegistry.ts +17 -38
- package/components/mcp/listChanged.ts +37 -20
- package/components/mcp/quota.ts +145 -0
- package/components/mcp/rateLimit.ts +152 -14
- package/components/mcp/resources.ts +2 -4
- package/components/mcp/toolRegistry.ts +73 -4
- package/components/mcp/tools/application.ts +77 -8
- package/components/mcp/tools/operations.ts +106 -36
- package/components/mcp/tools/schemas/operationDescriptions.ts +4 -4
- package/components/mcp/tools/schemas/operations.ts +5 -0
- package/components/mcp/transport.ts +55 -4
- package/components/ollama/index.ts +6 -2
- package/components/openai/index.ts +9 -2
- package/components/operations.js +204 -10
- package/components/operationsValidation.js +173 -0
- package/components/secretOperations.ts +498 -0
- package/config/configUtils.ts +25 -1
- package/config-app.schema.json +81 -0
- package/config-root.schema.json +8 -1
- package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -3
- package/dataLayer/readAuditLog.ts +14 -0
- package/dataLayer/schemaDescribe.ts +1 -0
- package/dist/agent/operations.d.ts +7 -4
- package/dist/agent/operations.js +13 -15
- package/dist/agent/operations.js.map +1 -1
- package/dist/components/Application.d.ts +25 -2
- package/dist/components/Application.js +166 -7
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +2 -0
- package/dist/components/ApplicationScope.js +3 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/Scope.d.ts +8 -0
- package/dist/components/Scope.js +11 -0
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/anthropic/index.d.ts +4 -1
- package/dist/components/anthropic/index.js +4 -2
- package/dist/components/anthropic/index.js.map +1 -1
- package/dist/components/bedrock/index.d.ts +4 -1
- package/dist/components/bedrock/index.js +22 -2
- package/dist/components/bedrock/index.js.map +1 -1
- package/dist/components/componentLoader.d.ts +2 -0
- package/dist/components/componentLoader.js +52 -10
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/componentSecrets.d.ts +63 -0
- package/dist/components/componentSecrets.js +405 -0
- package/dist/components/componentSecrets.js.map +1 -0
- package/dist/components/deploymentRecorder.d.ts +3 -0
- package/dist/components/deploymentRecorder.js +4 -3
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/mcp/adapters/fastify.d.ts +2 -0
- package/dist/components/mcp/adapters/fastify.js +1 -0
- package/dist/components/mcp/adapters/fastify.js.map +1 -1
- package/dist/components/mcp/adapters/harperHttp.d.ts +2 -0
- package/dist/components/mcp/adapters/harperHttp.js +1 -0
- package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
- package/dist/components/mcp/audit.d.ts +2 -2
- package/dist/components/mcp/audit.js +20 -5
- package/dist/components/mcp/audit.js.map +1 -1
- package/dist/components/mcp/customResourceRegistry.d.ts +5 -12
- package/dist/components/mcp/customResourceRegistry.js +15 -30
- package/dist/components/mcp/customResourceRegistry.js.map +1 -1
- package/dist/components/mcp/listChanged.d.ts +7 -2
- package/dist/components/mcp/listChanged.js +28 -0
- package/dist/components/mcp/listChanged.js.map +1 -1
- package/dist/components/mcp/quota.d.ts +31 -0
- package/dist/components/mcp/quota.js +155 -0
- package/dist/components/mcp/quota.js.map +1 -0
- package/dist/components/mcp/rateLimit.d.ts +19 -3
- package/dist/components/mcp/rateLimit.js +129 -13
- package/dist/components/mcp/rateLimit.js.map +1 -1
- package/dist/components/mcp/resources.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +39 -1
- package/dist/components/mcp/toolRegistry.js +60 -5
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/application.d.ts +8 -1
- package/dist/components/mcp/tools/application.js +65 -5
- package/dist/components/mcp/tools/application.js.map +1 -1
- package/dist/components/mcp/tools/operations.d.ts +15 -3
- package/dist/components/mcp/tools/operations.js +99 -38
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operationDescriptions.js +4 -4
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operations.js +4 -0
- package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
- package/dist/components/mcp/transport.d.ts +6 -0
- package/dist/components/mcp/transport.js +47 -3
- package/dist/components/mcp/transport.js.map +1 -1
- package/dist/components/ollama/index.d.ts +4 -1
- package/dist/components/ollama/index.js +4 -2
- package/dist/components/ollama/index.js.map +1 -1
- package/dist/components/openai/index.d.ts +4 -1
- package/dist/components/openai/index.js +4 -2
- package/dist/components/openai/index.js.map +1 -1
- package/dist/components/operations.d.ts +39 -0
- package/dist/components/operations.js +194 -2
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.d.ts +38 -0
- package/dist/components/operationsValidation.js +155 -0
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/secretOperations.d.ts +113 -0
- package/dist/components/secretOperations.js +475 -0
- package/dist/components/secretOperations.js.map +1 -0
- package/dist/config/configUtils.d.ts +4 -1
- package/dist/config/configUtils.js +18 -1
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -3
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
- package/dist/dataLayer/readAuditLog.js +11 -0
- package/dist/dataLayer/readAuditLog.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +2 -0
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/globals.d.ts +1 -1
- package/dist/globals.js +1 -0
- package/dist/globals.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +38 -0
- package/dist/resources/DatabaseTransaction.d.ts +31 -3
- package/dist/resources/DatabaseTransaction.js +190 -29
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +1 -1
- package/dist/resources/LMDBTransaction.js +43 -5
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.d.ts +51 -0
- package/dist/resources/PrimaryRocksDatabase.js +194 -0
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -0
- package/dist/resources/RecordEncoder.d.ts +1 -0
- package/dist/resources/RecordEncoder.js +81 -75
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/RequestTarget.d.ts +19 -0
- package/dist/resources/RequestTarget.js.map +1 -1
- package/dist/resources/Resource.d.ts +0 -1
- package/dist/resources/Resource.js +103 -16
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +6 -2
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/Resources.js.map +1 -1
- package/dist/resources/Table.d.ts +6 -4
- package/dist/resources/Table.js +247 -24
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/read.js +25 -28
- package/dist/resources/analytics/read.js.map +1 -1
- package/dist/resources/blob.js +165 -22
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/crdt.d.ts +2 -0
- package/dist/resources/crdt.js +45 -36
- package/dist/resources/crdt.js.map +1 -1
- package/dist/resources/databases.d.ts +1 -0
- package/dist/resources/databases.js +134 -7
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/graphql.js +8 -0
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +35 -5
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +239 -14
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/jsResource.js +6 -0
- package/dist/resources/jsResource.js.map +1 -1
- package/dist/resources/loadEnv.js +30 -3
- package/dist/resources/loadEnv.js.map +1 -1
- package/dist/resources/login.js +2 -2
- package/dist/resources/login.js.map +1 -1
- package/dist/resources/models/Models.js +14 -3
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/embedHook.js +10 -2
- package/dist/resources/models/embedHook.js.map +1 -1
- package/dist/resources/models/openaiStream.d.ts +56 -0
- package/dist/resources/models/openaiStream.js +94 -0
- package/dist/resources/models/openaiStream.js.map +1 -0
- package/dist/resources/replayLogs.js +4 -3
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/search.d.ts +1 -1
- package/dist/resources/search.js +104 -11
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/secretDecryptor.d.ts +54 -0
- package/dist/resources/secretDecryptor.js +131 -0
- package/dist/resources/secretDecryptor.js.map +1 -0
- package/dist/resources/tracked.js +15 -5
- package/dist/resources/tracked.js.map +1 -1
- package/dist/security/auth.js +71 -8
- package/dist/security/auth.js.map +1 -1
- package/dist/security/jsLoader.js +11 -0
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/tokenAuthentication.d.ts +9 -1
- package/dist/security/tokenAuthentication.js +31 -1
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/server/DurableSubscriptionsSession.d.ts +1 -1
- package/dist/server/DurableSubscriptionsSession.js +8 -4
- package/dist/server/DurableSubscriptionsSession.js.map +1 -1
- package/dist/server/REST.js +28 -9
- package/dist/server/REST.js.map +1 -1
- package/dist/server/fastifyRoutes.js +11 -1
- package/dist/server/fastifyRoutes.js.map +1 -1
- package/dist/server/graphqlQuerying.js +4 -3
- package/dist/server/graphqlQuerying.js.map +1 -1
- package/dist/server/http.d.ts +40 -2
- package/dist/server/http.js +560 -77
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +64 -0
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/liveSubscriptionAuth.d.ts +12 -0
- package/dist/server/liveSubscriptionAuth.js +140 -0
- package/dist/server/liveSubscriptionAuth.js.map +1 -0
- package/dist/server/loadRootComponents.js +2 -1
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/middlewareChain.d.ts +44 -1
- package/dist/server/middlewareChain.js +86 -10
- package/dist/server/middlewareChain.js.map +1 -1
- package/dist/server/mqtt.js +2 -1
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/operationsServer.js +1 -1
- package/dist/server/operationsServer.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +9 -0
- package/dist/server/serverHelpers/Headers.js +22 -0
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/JSONStream.js +3 -3
- package/dist/server/serverHelpers/JSONStream.js.map +1 -1
- package/dist/server/serverHelpers/Request.d.ts +52 -1
- package/dist/server/serverHelpers/Request.js +122 -1
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +3 -8
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/progressEmitter.d.ts +19 -0
- package/dist/server/serverHelpers/progressEmitter.js +52 -4
- package/dist/server/serverHelpers/progressEmitter.js.map +1 -1
- package/dist/server/serverHelpers/registeredOperations.d.ts +40 -0
- package/dist/server/serverHelpers/registeredOperations.js +286 -0
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -0
- package/dist/server/serverHelpers/serverHandlers.js +11 -5
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +1 -0
- package/dist/server/serverHelpers/serverUtilities.js +67 -5
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.d.ts +80 -0
- package/dist/server/serverHelpers/uwsServer.js +433 -0
- package/dist/server/serverHelpers/uwsServer.js.map +1 -0
- package/dist/server/static.d.ts +0 -15
- package/dist/server/static.js +161 -2
- package/dist/server/static.js.map +1 -1
- package/dist/server/status/index.d.ts +4 -1
- package/dist/server/status/index.js +64 -4
- package/dist/server/status/index.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +10 -0
- package/dist/server/threads/manageThreads.js +107 -4
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.js +5 -1
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +92 -14
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/upgrade/directives/5-2-0.d.ts +7 -0
- package/dist/upgrade/directives/5-2-0.js +108 -0
- package/dist/upgrade/directives/5-2-0.js.map +1 -0
- package/dist/upgrade/directives/directivesController.js +2 -1
- package/dist/upgrade/directives/directivesController.js.map +1 -1
- package/dist/utility/common_utils.js +5 -0
- package/dist/utility/common_utils.js.map +1 -1
- package/dist/utility/envFile.d.ts +41 -0
- package/dist/utility/envFile.js +221 -0
- package/dist/utility/envFile.js.map +1 -0
- package/dist/utility/globalSchema.d.ts +9 -0
- package/dist/utility/hdbTerms.d.ts +25 -0
- package/dist/utility/hdbTerms.js +31 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +21 -0
- package/dist/utility/logging/harper_logger.js +160 -8
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logRotator.js +29 -17
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/logging/readLog.d.ts +1 -1
- package/dist/utility/logging/readLog.js +305 -2
- package/dist/utility/logging/readLog.js.map +1 -1
- package/dist/utility/operationPermissions.d.ts +18 -0
- package/dist/utility/operationPermissions.js +35 -1
- package/dist/utility/operationPermissions.js.map +1 -1
- package/dist/utility/operation_authorization.d.ts +17 -0
- package/dist/utility/operation_authorization.js +38 -0
- package/dist/utility/operation_authorization.js.map +1 -1
- package/dist/utility/secretEnvelope.d.ts +30 -0
- package/dist/utility/secretEnvelope.js +94 -0
- package/dist/utility/secretEnvelope.js.map +1 -0
- package/dist/utility/signalling.d.ts +7 -0
- package/dist/utility/signalling.js +16 -0
- package/dist/utility/signalling.js.map +1 -1
- package/dist/validation/configValidator.js +70 -24
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +4 -0
- package/json/systemSchema.json +38 -0
- package/npm-shrinkwrap.json +15066 -9347
- package/package.json +13 -8
- package/resources/DESIGN.md +15 -12
- package/resources/DatabaseTransaction.ts +206 -37
- package/resources/LMDBTransaction.ts +43 -9
- package/resources/PrimaryRocksDatabase.ts +201 -0
- package/resources/RecordEncoder.ts +81 -69
- package/resources/RequestTarget.ts +21 -0
- package/resources/Resource.ts +109 -25
- package/resources/ResourceInterface.ts +6 -2
- package/resources/Resources.ts +1 -3
- package/resources/Table.ts +238 -29
- package/resources/analytics/read.ts +25 -30
- package/resources/blob.ts +159 -23
- package/resources/crdt.ts +37 -31
- package/resources/databases.ts +143 -6
- package/resources/graphql.ts +8 -0
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +251 -17
- package/resources/jsResource.ts +6 -0
- package/resources/loadEnv.ts +34 -3
- package/resources/login.ts +2 -2
- package/resources/models/Models.ts +19 -3
- package/resources/models/embedHook.ts +13 -2
- package/resources/models/openaiStream.ts +133 -0
- package/resources/replayLogs.ts +4 -3
- package/resources/search.ts +113 -11
- package/resources/secretDecryptor.ts +159 -0
- package/resources/tracked.ts +24 -17
- package/security/auth.ts +70 -10
- package/security/jsLoader.ts +12 -0
- package/security/tokenAuthentication.ts +42 -1
- package/server/DESIGN.md +10 -0
- package/server/DurableSubscriptionsSession.ts +8 -4
- package/server/REST.ts +26 -9
- package/server/fastifyRoutes.ts +10 -1
- package/server/graphqlQuerying.ts +4 -3
- package/server/http.ts +563 -77
- package/server/itc/serverHandlers.js +70 -0
- package/server/liveSubscriptionAuth.ts +152 -0
- package/server/loadRootComponents.js +2 -1
- package/server/middlewareChain.ts +107 -17
- package/server/mqtt.ts +2 -1
- package/server/operationsServer.ts +2 -2
- package/server/serverHelpers/Headers.ts +24 -0
- package/server/serverHelpers/JSONStream.ts +3 -3
- package/server/serverHelpers/Request.ts +127 -0
- package/server/serverHelpers/contentTypes.ts +3 -8
- package/server/serverHelpers/progressEmitter.ts +55 -5
- package/server/serverHelpers/registeredOperations.ts +259 -0
- package/server/serverHelpers/serverHandlers.js +11 -5
- package/server/serverHelpers/serverUtilities.ts +88 -5
- package/server/serverHelpers/uwsServer.ts +488 -0
- package/server/static.ts +180 -2
- package/server/status/index.ts +72 -4
- package/server/threads/manageThreads.js +102 -4
- package/server/threads/socketRouter.ts +5 -1
- package/server/threads/threadServer.js +93 -10
- package/studio/web/assets/{Chat-SdD1EZca.js → Chat-Cv_2paZE.js} +2 -2
- package/studio/web/assets/{Chat-SdD1EZca.js.map → Chat-Cv_2paZE.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-GMq6tHK9.js → FloatingChat-CPyPIcVR.js} +4 -4
- package/studio/web/assets/{FloatingChat-GMq6tHK9.js.map → FloatingChat-CPyPIcVR.js.map} +1 -1
- package/studio/web/assets/{applications-B5hhv7uA.js → applications-C0jT2vdZ.js} +2 -2
- package/studio/web/assets/{applications-B5hhv7uA.js.map → applications-C0jT2vdZ.js.map} +1 -1
- package/studio/web/assets/{index-VFcpNWgq.js → index-D_GKOkhn.js} +6 -6
- package/studio/web/assets/index-D_GKOkhn.js.map +1 -0
- package/studio/web/assets/{index.lazy-DgcJojIA.js → index.lazy-CAmCIA65.js} +4 -4
- package/studio/web/assets/{index.lazy-DgcJojIA.js.map → index.lazy-CAmCIA65.js.map} +1 -1
- package/studio/web/assets/{profile-BqN-8vzm.js → profile-CaF-4aZe.js} +2 -2
- package/studio/web/assets/{profile-BqN-8vzm.js.map → profile-CaF-4aZe.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-CHFvFq9u.js → setComponentFile-DLW1DHCt.js} +2 -2
- package/studio/web/assets/{setComponentFile-CHFvFq9u.js.map → setComponentFile-DLW1DHCt.js.map} +1 -1
- package/studio/web/assets/{setup-CQ3vWJSr.js → setup-TGCErIhq.js} +2 -2
- package/studio/web/assets/{setup-CQ3vWJSr.js.map → setup-TGCErIhq.js.map} +1 -1
- package/studio/web/assets/{status-BsW5fjuh.js → status-DG0Maoao.js} +2 -2
- package/studio/web/assets/{status-BsW5fjuh.js.map → status-DG0Maoao.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-DITdeB9-.js → swagger-ui-react-8T4SgQ1m.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-DITdeB9-.js.map → swagger-ui-react-8T4SgQ1m.js.map} +1 -1
- package/studio/web/assets/{tsMode-Dyph-OUs.js → tsMode-DJ6Sl24Q.js} +2 -2
- package/studio/web/assets/{tsMode-Dyph-OUs.js.map → tsMode-DJ6Sl24Q.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-D0QTUqex.js → useEntityRestURL-JO2mfWTQ.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-D0QTUqex.js.map → useEntityRestURL-JO2mfWTQ.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-2-0.ts +82 -0
- package/upgrade/directives/directivesController.ts +2 -1
- package/utility/common_utils.ts +5 -0
- package/utility/envFile.ts +218 -0
- package/utility/hdbTerms.ts +31 -0
- package/utility/logging/harper_logger.ts +156 -8
- package/utility/logging/logRotator.ts +24 -14
- package/utility/logging/readLog.ts +323 -2
- package/utility/operationPermissions.ts +35 -1
- package/utility/operation_authorization.ts +74 -1
- package/utility/secretEnvelope.ts +113 -0
- package/utility/signalling.ts +15 -0
- package/validation/configValidator.ts +78 -25
- 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
|
-
|
|
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 (`[^/]+`)
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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;
|
|
81
|
-
const
|
|
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 (
|
|
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
|
-
|
|
96
|
+
paramNames.push(name);
|
|
104
97
|
pattern += reserved ? '(.+)' : '([^/]+)';
|
|
105
98
|
index = close + 1;
|
|
106
99
|
}
|
|
107
|
-
if (
|
|
108
|
-
return { regex: new RegExp(`^${pattern}$`),
|
|
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,
|
|
112
|
+
const { regex, paramNames } = compileUriTemplate(def.uriTemplate);
|
|
127
113
|
compiled.regex = regex;
|
|
128
|
-
compiled.
|
|
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,
|
|
199
|
-
if (!regex || !
|
|
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
|
|
204
|
-
|
|
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
|
-
*
|
|
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: {
|
|
31
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|