@bevel-software/platform-core-backend 0.13.6 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +6 -6
- package/dist/core/catalog-cache-invalidation.d.ts +29 -0
- package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
- package/dist/core/catalog-cache-invalidation.js +71 -0
- package/dist/core/catalog-cache-invalidation.js.map +1 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +101 -9
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +20 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +105 -43
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +12 -5
- package/dist/core-config.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +18 -6
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +26 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +213 -71
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +59 -7
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +51 -1
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +95 -1
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/access-model/group-files.d.ts.map +1 -1
- package/dist/modules/access-model/group-files.js +4 -1
- package/dist/modules/access-model/group-files.js.map +1 -1
- package/dist/modules/access-model/kb-read-filter.d.ts +12 -0
- package/dist/modules/access-model/kb-read-filter.d.ts.map +1 -1
- package/dist/modules/access-model/kb-read-filter.js +15 -0
- package/dist/modules/access-model/kb-read-filter.js.map +1 -1
- package/dist/modules/access-model/plugin-principals.d.ts +49 -0
- package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
- package/dist/modules/access-model/plugin-principals.js +125 -0
- package/dist/modules/access-model/plugin-principals.js.map +1 -0
- package/dist/modules/connection-probe/connection-probe.contract.d.ts +43 -0
- package/dist/modules/connection-probe/connection-probe.contract.d.ts.map +1 -0
- package/dist/modules/connection-probe/connection-probe.contract.js +2 -0
- package/dist/modules/connection-probe/connection-probe.contract.js.map +1 -0
- package/dist/modules/connection-probe/connection-probe.service.d.ts +94 -0
- package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -0
- package/dist/modules/connection-probe/connection-probe.service.js +684 -0
- package/dist/modules/connection-probe/connection-probe.service.js.map +1 -0
- package/dist/modules/connection-probe/index.d.ts +3 -0
- package/dist/modules/connection-probe/index.d.ts.map +1 -0
- package/dist/modules/connection-probe/index.js +3 -0
- package/dist/modules/connection-probe/index.js.map +1 -0
- package/dist/modules/database/core-schema.d.ts +326 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +52 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/diff/diff.routes.d.ts.map +1 -1
- package/dist/modules/diff/diff.routes.js +3 -5
- package/dist/modules/diff/diff.routes.js.map +1 -1
- package/dist/modules/kb-fs/mutex.d.ts +37 -0
- package/dist/modules/kb-fs/mutex.d.ts.map +1 -1
- package/dist/modules/kb-fs/mutex.js +48 -5
- package/dist/modules/kb-fs/mutex.js.map +1 -1
- package/dist/modules/kb-sync/index.d.ts +6 -0
- package/dist/modules/kb-sync/index.d.ts.map +1 -0
- package/dist/modules/kb-sync/index.js +5 -0
- package/dist/modules/kb-sync/index.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
- package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.js +183 -0
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
- package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-auth.js +62 -0
- package/dist/modules/kb-sync/sync-auth.js.map +1 -0
- package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
- package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-payload.js +105 -0
- package/dist/modules/kb-sync/sync-payload.js.map +1 -0
- package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
- package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/git-http.routes.js +190 -0
- package/dist/modules/marketplace/git-http.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
- package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/index.js +6 -0
- package/dist/modules/marketplace/github-facade/index.js.map +1 -0
- package/dist/modules/marketplace/index.d.ts +4 -0
- package/dist/modules/marketplace/index.d.ts.map +1 -0
- package/dist/modules/marketplace/index.js +4 -0
- package/dist/modules/marketplace/index.js.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
- package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
- package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.js +3 -1
- package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
- package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
- package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
- package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
- package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
- package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
- package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/native.source.js +102 -0
- package/dist/modules/plugins/discovery/native.source.js.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.js +2 -0
- package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
- package/dist/modules/plugins/index.d.ts +10 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +8 -0
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-links.d.ts +66 -0
- package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.js +160 -0
- package/dist/modules/plugins/plugin-links.js.map +1 -0
- package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
- package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.service.js +304 -0
- package/dist/modules/plugins/plugin-links.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +152 -52
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
- package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-rename.service.js +245 -0
- package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +27 -1
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +7 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +120 -12
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/plugins/plugins.service.d.ts +50 -3
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.service.js +108 -47
- package/dist/modules/plugins/plugins.service.js.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.js +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +6 -0
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js +31 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +82 -2
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts +26 -0
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +9 -1
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/index.d.ts +1 -0
- package/dist/modules/skills/index.d.ts.map +1 -1
- package/dist/modules/skills/index.js +1 -0
- package/dist/modules/skills/index.js.map +1 -1
- package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
- package/dist/modules/skills/pending-skills.service.js +9 -4
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
- package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
- package/dist/modules/skills/skill-access-requests.routes.js +168 -0
- package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
- package/dist/modules/skills/skills.contract.d.ts +31 -0
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.d.ts +28 -3
- package/dist/modules/skills/skills.routes.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.js +42 -3
- package/dist/modules/skills/skills.routes.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +4 -0
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +83 -22
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
- package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +26 -9
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -83
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +10 -4
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +83 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +15 -2
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +199 -38
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +58 -3
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +325 -60
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +8 -0
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts +2 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +98 -9
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +133 -9
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +462 -52
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +11 -14
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +38 -1
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +131 -20
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +20 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +29 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/fs-errors.d.ts +8 -0
- package/dist/shared/fs-errors.d.ts.map +1 -0
- package/dist/shared/fs-errors.js +11 -0
- package/dist/shared/fs-errors.js.map +1 -0
- package/dist/shared/fs-walk.d.ts +20 -6
- package/dist/shared/fs-walk.d.ts.map +1 -1
- package/dist/shared/fs-walk.js +35 -27
- package/dist/shared/fs-walk.js.map +1 -1
- package/dist/shared/kb-walk.d.ts +52 -0
- package/dist/shared/kb-walk.d.ts.map +1 -0
- package/dist/shared/kb-walk.js +76 -0
- package/dist/shared/kb-walk.js.map +1 -0
- package/dist/shared/path-order.d.ts +18 -0
- package/dist/shared/path-order.d.ts.map +1 -0
- package/dist/shared/path-order.js +35 -0
- package/dist/shared/path-order.js.map +1 -0
- package/dist/shared/token-crypto.d.ts +17 -2
- package/dist/shared/token-crypto.d.ts.map +1 -1
- package/dist/shared/token-crypto.js +25 -8
- package/dist/shared/token-crypto.js.map +1 -1
- package/dist/shared/ttl-cache.d.ts +28 -1
- package/dist/shared/ttl-cache.d.ts.map +1 -1
- package/dist/shared/ttl-cache.js +33 -1
- package/dist/shared/ttl-cache.js.map +1 -1
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +95 -30
- package/kb-template/Skills/.gitkeep +0 -0
- package/migrations/0005_github_facade.sql +25 -0
- package/migrations/meta/0005_snapshot.json +1717 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/__tests__/core-config.admin.test.ts +21 -0
- package/src/__tests__/kb-layout-config.test.ts +110 -0
- package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
- package/src/core/catalog-cache-invalidation.ts +92 -0
- package/src/core/create-core-server.ts +126 -7
- package/src/core/create-core-services.ts +173 -41
- package/src/core-config.ts +14 -7
- package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
- package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
- package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
- package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
- package/src/modules/access/access-control.interface.ts +20 -9
- package/src/modules/access/access-control.service.ts +240 -78
- package/src/modules/access/access.routes.ts +96 -14
- package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
- package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
- package/src/modules/access-model/access-grammar.ts +114 -2
- package/src/modules/access-model/group-files.ts +4 -0
- package/src/modules/access-model/kb-read-filter.ts +22 -0
- package/src/modules/access-model/plugin-principals.ts +137 -0
- package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +685 -0
- package/src/modules/connection-probe/connection-probe.contract.ts +44 -0
- package/src/modules/connection-probe/connection-probe.service.ts +734 -0
- package/src/modules/connection-probe/index.ts +2 -0
- package/src/modules/database/core-schema.ts +54 -0
- package/src/modules/diff/diff.routes.ts +9 -5
- package/src/modules/kb-fs/__tests__/mutex.test.ts +107 -0
- package/src/modules/kb-fs/mutex.ts +50 -5
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
- package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
- package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
- package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
- package/src/modules/kb-sync/index.ts +19 -0
- package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
- package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
- package/src/modules/kb-sync/kb-sync.service.ts +201 -0
- package/src/modules/kb-sync/sync-auth.ts +118 -0
- package/src/modules/kb-sync/sync-payload.ts +143 -0
- package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
- package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
- package/src/modules/marketplace/git-http.routes.ts +193 -0
- package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
- package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
- package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
- package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
- package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
- package/src/modules/marketplace/github-facade/index.ts +25 -0
- package/src/modules/marketplace/index.ts +3 -0
- package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
- package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
- package/src/modules/mcp/oauth/oauth-state.ts +15 -3
- package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
- package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
- package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
- package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
- package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
- package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
- package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
- package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
- package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
- package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
- package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
- package/src/modules/plugins/discovery/native.source.ts +122 -0
- package/src/modules/plugins/discovery/plugin-source.ts +99 -0
- package/src/modules/plugins/index.ts +10 -0
- package/src/modules/plugins/plugin-links.service.ts +363 -0
- package/src/modules/plugins/plugin-links.ts +184 -0
- package/src/modules/plugins/plugin-provision.service.ts +160 -54
- package/src/modules/plugins/plugin-rename.service.ts +298 -0
- package/src/modules/plugins/plugins.contract.ts +28 -1
- package/src/modules/plugins/plugins.routes.ts +128 -11
- package/src/modules/plugins/plugins.service.ts +106 -53
- package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +12 -0
- package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +10 -3
- package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +26 -0
- package/src/modules/secrets-vault/db-secrets-vault.service.ts +1 -1
- package/src/modules/secrets-vault/secrets-vault.routes.ts +35 -1
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
- package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
- package/src/modules/settings/deployment-settings.service.ts +95 -2
- package/src/modules/settings/setup.routes.ts +24 -0
- package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
- package/src/modules/skills/index.ts +1 -0
- package/src/modules/skills/pending-skills.service.ts +7 -5
- package/src/modules/skills/skill-access-requests.routes.ts +199 -0
- package/src/modules/skills/skills.contract.ts +32 -0
- package/src/modules/skills/skills.routes.ts +59 -3
- package/src/modules/skills/skills.service.ts +84 -22
- package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
- package/src/modules/tool-auth/external-api-key.service.ts +31 -13
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +35 -1
- package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.health-check.test.ts +104 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +56 -0
- package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -76
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +13 -4
- package/src/modules/tool-manuals/tool-manuals.contract.ts +89 -0
- package/src/modules/tool-manuals/tool-manuals.service.ts +210 -37
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +139 -0
- package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +337 -0
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
- package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +7 -1
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +97 -10
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
- package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
- package/src/modules/workflow/git/__tests__/git.service.history-guards.test.ts +86 -0
- package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
- package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
- package/src/modules/workflow/git/git.service.ts +354 -63
- package/src/modules/workflow/git/pull-request.service.ts +9 -0
- package/src/modules/workflow/workflow.routes.ts +104 -4
- package/src/modules/workflow/workflow.service.ts +505 -64
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
- package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
- package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
- package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
- package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
- package/src/modules/workspace/workspace.routes.ts +17 -21
- package/src/modules/workspace/workspace.service.ts +128 -19
- package/src/shared/__tests__/filename.test.ts +0 -0
- package/src/shared/__tests__/fs-walk.test.ts +22 -1
- package/src/shared/__tests__/kb-walk.test.ts +107 -0
- package/src/shared/__tests__/path-order.test.ts +24 -0
- package/src/shared/__tests__/token-crypto.test.ts +34 -0
- package/src/shared/__tests__/ttl-cache.test.ts +55 -0
- package/src/shared/domain-errors.ts +29 -0
- package/src/shared/fs-errors.ts +10 -0
- package/src/shared/fs-walk.ts +38 -23
- package/src/shared/kb-walk.ts +95 -0
- package/src/shared/path-order.ts +33 -0
- package/src/shared/token-crypto.ts +28 -10
- package/src/shared/ttl-cache.ts +33 -1
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import { describe, expect, it, vi, afterEach } from 'vitest';
|
|
2
|
-
import { descriptorsFromMcpJson } from '../mcp-json-discovery.js';
|
|
2
|
+
import { descriptorsFromMcpJson, judgeMcpServerEntry } from '../mcp-json-discovery.js';
|
|
3
3
|
|
|
4
4
|
afterEach(() => vi.restoreAllMocks());
|
|
5
5
|
|
|
6
|
+
describe('judgeMcpServerEntry', () => {
|
|
7
|
+
it('keeps every accepted header and env key — one spelled __proto__ included', () => {
|
|
8
|
+
// Parsed from text, as a repository file is: JSON.parse makes `__proto__`
|
|
9
|
+
// an own key, and a plain-object copy would turn it into a prototype
|
|
10
|
+
// assignment and lose it.
|
|
11
|
+
const http = judgeMcpServerEntry(
|
|
12
|
+
'vendor',
|
|
13
|
+
JSON.parse('{"type":"streamable-http","url":"https://x.example","headers":{"__proto__":"p","X-A":"a"}}'),
|
|
14
|
+
);
|
|
15
|
+
expect(http.ok).toBe(true);
|
|
16
|
+
if (!http.ok || http.transport !== 'streamable-http') throw new Error('unreachable');
|
|
17
|
+
expect(Object.keys(http.entry.headers ?? {}).sort()).toEqual(['X-A', '__proto__']);
|
|
18
|
+
expect(http.entry.headers?.['__proto__']).toBe('p');
|
|
19
|
+
|
|
20
|
+
const stdio = judgeMcpServerEntry(
|
|
21
|
+
'launch',
|
|
22
|
+
JSON.parse('{"type":"stdio","command":"run","env":{"__proto__":"p"}}'),
|
|
23
|
+
);
|
|
24
|
+
if (!stdio.ok || stdio.transport !== 'stdio') throw new Error('unreachable');
|
|
25
|
+
expect(stdio.entry.env?.['__proto__']).toBe('p');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
6
29
|
const MANIFEST = JSON.stringify({
|
|
7
30
|
name: 'gtm',
|
|
8
31
|
extensions: {
|
|
@@ -37,6 +60,17 @@ describe('descriptorsFromMcpJson', () => {
|
|
|
37
60
|
]);
|
|
38
61
|
});
|
|
39
62
|
|
|
63
|
+
it('refuses an sse server rather than rebuilding it as a transport it is not', () => {
|
|
64
|
+
// The pinned MCP client has no sse transport; emitting `http` for an sse
|
|
65
|
+
// server configures a handshake the server does not speak.
|
|
66
|
+
const out = descriptorsFromMcpJson(
|
|
67
|
+
'GTM',
|
|
68
|
+
JSON.stringify({ mcpServers: { legacy: { type: 'sse', url: 'https://mcp.legacy.example/sse' } } }),
|
|
69
|
+
null,
|
|
70
|
+
);
|
|
71
|
+
expect(out).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
40
74
|
it('merges extension auth over mcp.json literals and carries variables + description', () => {
|
|
41
75
|
const out = descriptorsFromMcpJson(
|
|
42
76
|
'GTM',
|
|
@@ -192,6 +192,8 @@ describe('manual namespaces are unique, not merely manual names', () => {
|
|
|
192
192
|
*/
|
|
193
193
|
async function writeMcpServer(folder: string, serverName: string): Promise<void> {
|
|
194
194
|
await mkdir(join(pluginsDir(), folder), { recursive: true });
|
|
195
|
+
// A plugin is a folder with a manifest; the servers ride in its mcp.json.
|
|
196
|
+
await writeFile(join(pluginsDir(), folder, 'plugin.json'), `{"name":"${folder.toLowerCase()}"}`);
|
|
195
197
|
await writeFile(
|
|
196
198
|
join(pluginsDir(), folder, 'mcp.json'),
|
|
197
199
|
JSON.stringify({ mcpServers: { [serverName]: { type: 'streamable-http', url: 'https://v.example/mcp' } } }),
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { normalizeToolManual } from '../tool-manuals.service.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The `healthCheck:` block is a URL this SERVER will fetch with the caller's
|
|
6
|
+
* credential attached, unattended, on every save and re-check. That makes it a
|
|
7
|
+
* second fetch target on the same footing as the manual's own `url`, so it is
|
|
8
|
+
* policed by the same SSRF rule — and held to one extra rule of its own: it may
|
|
9
|
+
* not mutate.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const toolFile = (body: string) => `---
|
|
13
|
+
name: acme
|
|
14
|
+
type: http
|
|
15
|
+
url: https://api.acme.test/utcp
|
|
16
|
+
headers:
|
|
17
|
+
Authorization: Bearer \${API_KEY}
|
|
18
|
+
${body}---
|
|
19
|
+
notes
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const parse = (body: string) => normalizeToolManual('acme', 'Plugins/acme.tool', toolFile(body));
|
|
23
|
+
|
|
24
|
+
describe('`.tool` healthCheck', () => {
|
|
25
|
+
it('is absent when nothing is declared — the tool is simply unverifiable', () => {
|
|
26
|
+
expect(parse('').healthCheck).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('inherits the manual\'s headers, so a one-line declaration still authenticates', () => {
|
|
30
|
+
// This is what makes the common case cheap: the credential already lives in
|
|
31
|
+
// `headers`, and re-typing it on the probe would be a chance to get it wrong.
|
|
32
|
+
expect(parse('healthCheck:\n url: https://api.acme.test/me\n').healthCheck).toEqual({
|
|
33
|
+
url: 'https://api.acme.test/me',
|
|
34
|
+
headers: { Authorization: 'Bearer ${API_KEY}' },
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('keeps its own headers when it declares them', () => {
|
|
39
|
+
const hc = parse('healthCheck:\n url: https://api.acme.test/me\n headers:\n X-Key: ${API_KEY}\n').healthCheck;
|
|
40
|
+
expect(hc?.headers).toEqual({ 'X-Key': '${API_KEY}' });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("an INLINE manual's probe inherits NO top-level headers — execution never sends them", () => {
|
|
44
|
+
// An inline manual's real calls go through each embedded tool's own call
|
|
45
|
+
// template; nothing ever sends the manual's top-level `headers:`. A probe
|
|
46
|
+
// that inherited them proved a request no call makes — `Connected` about
|
|
47
|
+
// the wrong request. An inline probe declares its headers on the
|
|
48
|
+
// healthCheck itself, or sends none.
|
|
49
|
+
const inline = normalizeToolManual(
|
|
50
|
+
'acme',
|
|
51
|
+
'Plugins/acme.tool',
|
|
52
|
+
`---
|
|
53
|
+
name: acme
|
|
54
|
+
type: inline
|
|
55
|
+
headers:
|
|
56
|
+
Authorization: Bearer \${API_KEY}
|
|
57
|
+
healthCheck:
|
|
58
|
+
url: https://api.acme.test/me
|
|
59
|
+
tools: []
|
|
60
|
+
---
|
|
61
|
+
`,
|
|
62
|
+
);
|
|
63
|
+
expect(inline.healthCheck?.headers).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('refuses a method that could mutate', () => {
|
|
67
|
+
// Silently downgrading POST to GET would leave the author believing they had
|
|
68
|
+
// declared something we are not doing.
|
|
69
|
+
expect(() => parse('healthCheck:\n url: https://api.acme.test/me\n method: POST\n')).toThrow(
|
|
70
|
+
/may not mutate/,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('refuses a probe pointed at an internal host', () => {
|
|
75
|
+
expect(() => parse('healthCheck:\n url: http://169.254.169.254/latest/meta-data\n')).toThrow(
|
|
76
|
+
/not allowed/,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('refuses a declaration with no url', () => {
|
|
81
|
+
expect(() => parse('healthCheck:\n method: GET\n')).toThrow(/must have a `url`/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A local-only `.tool` is never fetched by this server, so its probe is exempt
|
|
86
|
+
* from the guard for exactly the same reason its `url` is.
|
|
87
|
+
*/
|
|
88
|
+
it('exempts a local-only tool, as it does for the manual url', () => {
|
|
89
|
+
const local = normalizeToolManual(
|
|
90
|
+
'acme',
|
|
91
|
+
'Plugins/acme.tool',
|
|
92
|
+
`---
|
|
93
|
+
name: acme
|
|
94
|
+
type: http
|
|
95
|
+
remote: false
|
|
96
|
+
url: http://localhost:9000/utcp
|
|
97
|
+
healthCheck:
|
|
98
|
+
url: http://localhost:9000/me
|
|
99
|
+
---
|
|
100
|
+
`,
|
|
101
|
+
);
|
|
102
|
+
expect(local.healthCheck?.url).toBe('http://localhost:9000/me');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -82,6 +82,62 @@ describe('ToolManualService', () => {
|
|
|
82
82
|
expect(list.find((m) => m.name === 'billing')!.type).toBe('http');
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
test('surfaces a variable referenced ONLY by a health check, and keeps the probe off the summary', async () => {
|
|
86
|
+
// Two halves of the same contract. The probe's `${VAR}` has to reach the
|
|
87
|
+
// secrets UI or nobody can ever fill it in and the tool reports
|
|
88
|
+
// `unverifiable` forever — but the probe's HEADERS must not ride the
|
|
89
|
+
// browser-facing summary, since a `.tool` may write a literal token there.
|
|
90
|
+
const tools = join(root, wsId, KB_DIR, 'Plugins');
|
|
91
|
+
await writeFile(
|
|
92
|
+
join(tools, 'probe.tool'),
|
|
93
|
+
JSON.stringify({
|
|
94
|
+
name: 'probe',
|
|
95
|
+
type: 'http',
|
|
96
|
+
url: 'https://api.example.com/utcp',
|
|
97
|
+
healthCheck: { url: 'https://api.example.com/me', headers: { 'X-Key': '${PROBE_ONLY_KEY}' } },
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const summary = (await svc().listAccessible('user@x.eu')).find((m) => m.name === 'probe')!;
|
|
102
|
+
expect(summary.variables?.map((v) => v.name)).toContain('PROBE_ONLY_KEY');
|
|
103
|
+
expect(summary).not.toHaveProperty('healthCheck');
|
|
104
|
+
|
|
105
|
+
// The server still reaches it, through the accessor that never serializes.
|
|
106
|
+
const target = await svc().probeTargetFor('user@x.eu', 'probe');
|
|
107
|
+
expect(target?.healthCheck?.headers).toEqual({ 'X-Key': '${PROBE_ONLY_KEY}' });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('builds a call template only for the probe that actually dials one', async () => {
|
|
111
|
+
// The template is dialled by exactly one probe: an `mcp` manual, reachable
|
|
112
|
+
// from this process, that declared no health check. Building it for the
|
|
113
|
+
// others costs a validation pass per probe and warn-logs about a value
|
|
114
|
+
// nothing was going to use.
|
|
115
|
+
const tools = join(root, wsId, KB_DIR, 'Plugins');
|
|
116
|
+
await writeFile(
|
|
117
|
+
join(tools, 'chat.tool'),
|
|
118
|
+
JSON.stringify({ name: 'chat', type: 'mcp', url: 'https://mcp.example.com' }),
|
|
119
|
+
);
|
|
120
|
+
await writeFile(
|
|
121
|
+
join(tools, 'checked.tool'),
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
name: 'checked',
|
|
124
|
+
type: 'mcp',
|
|
125
|
+
url: 'https://mcp.example.com',
|
|
126
|
+
healthCheck: { url: 'https://api.example.com/me' },
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const service = svc();
|
|
131
|
+
// Asserted through the template's own type, not `not.toBeNull()`: the
|
|
132
|
+
// optional chain yields `undefined` when the lookup itself fails, and
|
|
133
|
+
// `undefined` is not null — so the weaker form passes on no target at all.
|
|
134
|
+
expect((await service.probeTargetFor('user@x.eu', 'chat'))?.callTemplate?.call_template_type).toBe('mcp');
|
|
135
|
+
// A declared health check wins for every type, so this one never dials it.
|
|
136
|
+
expect((await service.probeTargetFor('user@x.eu', 'checked'))?.callTemplate).toBeNull();
|
|
137
|
+
// An http manual is probed only by what it declares, never by a handshake.
|
|
138
|
+
expect((await service.probeTargetFor('user@x.eu', 'billing'))?.callTemplate).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
|
|
85
141
|
test('ACL filters out manuals the user cannot read', async () => {
|
|
86
142
|
const list = await svc(denyBilling).listAccessible('user@x.eu');
|
|
87
143
|
expect(list.map((m) => m.name)).toEqual(['weather']);
|
|
@@ -195,6 +195,121 @@ export function validatedVariables(raw: unknown): ToolVariable[] | null {
|
|
|
195
195
|
* materialization is a later phase — so a stdio server relying on them will
|
|
196
196
|
* fail to spawn until then; bare-command servers (`npx …`) work today.
|
|
197
197
|
*/
|
|
198
|
+
/**
|
|
199
|
+
* The ONE judgement of an `mcp.json` server entry — its name and its
|
|
200
|
+
* transport — shared by the manual scanner (which layers the platform's
|
|
201
|
+
* extension data and its reachability gate on top), the bundle dialect's
|
|
202
|
+
* registry (whose configs become entries) and the marketplace compiler
|
|
203
|
+
* (which ships only what a client could run). An entry that fails here fails
|
|
204
|
+
* everywhere, for one reason; an entry that passes is returned NORMALISED to
|
|
205
|
+
* the portable shape every consumer emits.
|
|
206
|
+
*/
|
|
207
|
+
export type McpEntryVerdict =
|
|
208
|
+
| { ok: true; transport: 'stdio'; entry: PortableStdioEntry }
|
|
209
|
+
| { ok: true; transport: 'streamable-http'; entry: PortableHttpEntry }
|
|
210
|
+
| { ok: false; reason: string };
|
|
211
|
+
|
|
212
|
+
/** A stdio server as a client launches it — every field already the type the launcher needs. */
|
|
213
|
+
export interface PortableStdioEntry {
|
|
214
|
+
type: 'stdio';
|
|
215
|
+
command: string;
|
|
216
|
+
args: string[];
|
|
217
|
+
env?: Record<string, string>;
|
|
218
|
+
cwd?: string;
|
|
219
|
+
}
|
|
220
|
+
/** An http server as a client dials it — an endpoint, never a credential. */
|
|
221
|
+
export interface PortableHttpEntry {
|
|
222
|
+
type: 'streamable-http';
|
|
223
|
+
url: string;
|
|
224
|
+
headers?: Record<string, string>;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* A record whose every value is a string, or null — no coercion, no cast.
|
|
229
|
+
* Built without a prototype: the keys are HTTP header and environment
|
|
230
|
+
* variable names, and a plain object would silently swallow one spelled
|
|
231
|
+
* `__proto__` (a prototype assignment, not a property) — every accepted key
|
|
232
|
+
* must come out the other side.
|
|
233
|
+
*/
|
|
234
|
+
function stringMap(v: unknown): Record<string, string> | null {
|
|
235
|
+
if (!isRecord(v)) return null;
|
|
236
|
+
const out: Record<string, string> = Object.create(null) as Record<string, string>;
|
|
237
|
+
for (const [k, val] of Object.entries(v)) {
|
|
238
|
+
if (typeof val !== 'string') return null;
|
|
239
|
+
out[k] = val;
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function judgeMcpServerEntry(name: string, raw: unknown): McpEntryVerdict {
|
|
245
|
+
// The name rule (lowercase, alphanumeric first) also keeps every object
|
|
246
|
+
// prototype key — `__proto__`, `constructor` — out of the plain-object
|
|
247
|
+
// maps consumers build from these names.
|
|
248
|
+
if (!SERVER_NAME_RE.test(name)) {
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
reason: 'the name is the secret namespace and route slug, so it must be lowercase alphanumeric with `_`/`-`',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (!isRecord(raw) || typeof raw.type !== 'string') return { ok: false, reason: 'no transport type' };
|
|
255
|
+
if (raw.type === 'stdio') {
|
|
256
|
+
if (typeof raw.command !== 'string' || raw.command.length === 0) return { ok: false, reason: 'no command' };
|
|
257
|
+
const entry: PortableStdioEntry = { type: 'stdio', command: raw.command, args: [] };
|
|
258
|
+
if (raw.args !== undefined) {
|
|
259
|
+
// A launcher gets the strings it was given, never a stringified object.
|
|
260
|
+
if (!Array.isArray(raw.args) || !raw.args.every((a) => typeof a === 'string')) {
|
|
261
|
+
return { ok: false, reason: 'args must be a list of strings' };
|
|
262
|
+
}
|
|
263
|
+
entry.args = raw.args;
|
|
264
|
+
}
|
|
265
|
+
if (raw.env !== undefined) {
|
|
266
|
+
const env = stringMap(raw.env);
|
|
267
|
+
if (env === null) return { ok: false, reason: 'env must be a map of strings' };
|
|
268
|
+
entry.env = env;
|
|
269
|
+
}
|
|
270
|
+
if (raw.cwd !== undefined) {
|
|
271
|
+
if (typeof raw.cwd !== 'string') return { ok: false, reason: 'cwd must be a string' };
|
|
272
|
+
entry.cwd = raw.cwd;
|
|
273
|
+
}
|
|
274
|
+
return { ok: true, transport: 'stdio', entry };
|
|
275
|
+
}
|
|
276
|
+
if (raw.type === 'sse') {
|
|
277
|
+
// The pinned `@utcp/mcp` speaks `stdio` and streamable `http` — there
|
|
278
|
+
// is no sse transport in its schema, so a declaration claiming one either
|
|
279
|
+
// fails validation or, worse, dials a handshake the server does not
|
|
280
|
+
// speak. Refusing names the fix; silently rebuilding as http used to
|
|
281
|
+
// configure exactly that wrong handshake.
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
reason: 'the MCP client has no `sse` transport — declare the server as `streamable-http` if it supports it',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (raw.type === 'streamable-http') {
|
|
288
|
+
if (typeof raw.url !== 'string' || raw.url.length === 0) return { ok: false, reason: 'no url' };
|
|
289
|
+
let parsed: URL;
|
|
290
|
+
try {
|
|
291
|
+
parsed = new URL(raw.url);
|
|
292
|
+
} catch {
|
|
293
|
+
return { ok: false, reason: 'url must be http(s)' };
|
|
294
|
+
}
|
|
295
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return { ok: false, reason: 'url must be http(s)' };
|
|
296
|
+
// A portable entry is an endpoint. A credential belongs in the vault and
|
|
297
|
+
// reaches a client through the platform, never verbatim in a config that
|
|
298
|
+
// is compiled into a marketplace and cloned onto every member's machine.
|
|
299
|
+
if (parsed.username || parsed.password) return { ok: false, reason: 'url must not carry credentials' };
|
|
300
|
+
const entry: PortableHttpEntry = { type: 'streamable-http', url: raw.url };
|
|
301
|
+
if (raw.headers !== undefined) {
|
|
302
|
+
const headers = stringMap(raw.headers);
|
|
303
|
+
if (headers === null) return { ok: false, reason: 'headers must be a map of strings' };
|
|
304
|
+
entry.headers = headers;
|
|
305
|
+
}
|
|
306
|
+
return { ok: true, transport: 'streamable-http', entry };
|
|
307
|
+
}
|
|
308
|
+
// Unknown transport: the spec says an unknown `type` invalidates the
|
|
309
|
+
// ENTRY, not the file — the caller skips it and keeps its siblings.
|
|
310
|
+
return { ok: false, reason: `unknown type "${raw.type}"` };
|
|
311
|
+
}
|
|
312
|
+
|
|
198
313
|
export function descriptorsFromMcpJson(
|
|
199
314
|
pluginFolder: string,
|
|
200
315
|
mcpJsonText: string,
|
|
@@ -227,18 +342,14 @@ export function descriptorsFromMcpJson(
|
|
|
227
342
|
const mcpJsonPath = `${PLUGINS_DIR}/${pluginFolder}/${PLUGIN_MCP_FILE}`;
|
|
228
343
|
|
|
229
344
|
const out: ToolManualDescriptor[] = [];
|
|
230
|
-
for (const [name,
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
'namespace and route slug, so it must be lowercase alphanumeric with `_`/`-`.',
|
|
235
|
-
);
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
if (!isRecord(raw) || typeof raw.type !== 'string') {
|
|
239
|
-
console.warn(`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: no transport type.`);
|
|
345
|
+
for (const [name, rawEntry] of Object.entries(mcp.mcpServers)) {
|
|
346
|
+
const verdict = judgeMcpServerEntry(name, rawEntry);
|
|
347
|
+
if (!verdict.ok) {
|
|
348
|
+
console.warn(`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: ${verdict.reason}.`);
|
|
240
349
|
continue;
|
|
241
350
|
}
|
|
351
|
+
// Narrowed by the verdict: an entry that passed is a record.
|
|
352
|
+
const raw = rawEntry as Record<string, unknown>;
|
|
242
353
|
// The extension entry is knowledge-base content too — same zero-trust
|
|
243
354
|
// parse as the rest: a non-object entry reads as "no extension data".
|
|
244
355
|
const ext: HexisMcpServerExtension = isRecord(extensions[name])
|
|
@@ -274,79 +385,46 @@ export function descriptorsFromMcpJson(
|
|
|
274
385
|
...(variables.length > 0 ? { variables } : {}),
|
|
275
386
|
};
|
|
276
387
|
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
out.push({
|
|
283
|
-
...shared,
|
|
284
|
-
remote: false,
|
|
285
|
-
stdio: {
|
|
286
|
-
command: raw.command,
|
|
287
|
-
args: Array.isArray(raw.args) ? raw.args.map(String) : [],
|
|
288
|
-
env: isRecord(raw.env) ? (raw.env as Record<string, string>) : undefined,
|
|
289
|
-
cwd: typeof raw.cwd === 'string' ? raw.cwd : undefined,
|
|
290
|
-
},
|
|
291
|
-
});
|
|
388
|
+
if (verdict.transport === 'stdio') {
|
|
389
|
+
const { command, args, env, cwd } = verdict.entry;
|
|
390
|
+
out.push({ ...shared, remote: false, stdio: { command, args, env, cwd } });
|
|
292
391
|
continue;
|
|
293
392
|
}
|
|
294
393
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
// user's own machine ever dials it.
|
|
305
|
-
// Scheme first, for EVERYONE: `local: true` exempts a server from the
|
|
306
|
-
// private-network reachability policy, not from being http(s) at all.
|
|
307
|
-
let schemeOk = false;
|
|
394
|
+
const url = verdict.entry.url;
|
|
395
|
+
// Remote-capable servers get the same SSRF gate `.tool` urls pass —
|
|
396
|
+
// otherwise mcp.json becomes the way to point the backend at loopback,
|
|
397
|
+
// private ranges, or the cloud metadata endpoint. A `local: true` entry
|
|
398
|
+
// is exempt because loopback is exactly what local MEANS, and only the
|
|
399
|
+
// user's own machine ever dials it. (The scheme itself was judged for
|
|
400
|
+
// everyone above: `local: true` exempts a server from the reachability
|
|
401
|
+
// policy, not from being http(s) at all.)
|
|
402
|
+
if (ext.local !== true) {
|
|
308
403
|
try {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
console.warn(`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: url must be http(s).`);
|
|
404
|
+
assertSafeFetchUrl(url, { label: `mcp server "${name}" url` });
|
|
405
|
+
} catch (err) {
|
|
406
|
+
console.warn(
|
|
407
|
+
`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: ` +
|
|
408
|
+
`${err instanceof Error ? err.message : String(err)} (declare it \`local: true\` if it is deliberately private).`,
|
|
409
|
+
);
|
|
316
410
|
continue;
|
|
317
411
|
}
|
|
318
|
-
if (ext.local !== true) {
|
|
319
|
-
try {
|
|
320
|
-
assertSafeFetchUrl(raw.url, { label: `mcp server "${name}" url` });
|
|
321
|
-
} catch (err) {
|
|
322
|
-
console.warn(
|
|
323
|
-
`[tool-manuals] skipping mcp server "${name}" in ${mcpJsonPath}: ` +
|
|
324
|
-
`${err instanceof Error ? err.message : String(err)} (declare it \`local: true\` if it is deliberately private).`,
|
|
325
|
-
);
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
// Extension headers (auth, `${VAR}` refs) win over mcp.json's literal
|
|
330
|
-
// ones on a key collision: the portable file cannot carry a credential,
|
|
331
|
-
// so when both name the same header the extension is the operative one.
|
|
332
|
-
// Both sides pass the isRecord gate — spreading a malformed non-object
|
|
333
|
-
// value (a string, say) would scatter its indices into header keys.
|
|
334
|
-
const headers = {
|
|
335
|
-
...(isRecord(raw.headers) ? (raw.headers as Record<string, string>) : {}),
|
|
336
|
-
...(isRecord(ext.headers) ? (ext.headers as Record<string, string>) : {}),
|
|
337
|
-
};
|
|
338
|
-
out.push({
|
|
339
|
-
...shared,
|
|
340
|
-
url: raw.url,
|
|
341
|
-
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
342
|
-
...(ext.local === true ? { remote: false } : {}),
|
|
343
|
-
});
|
|
344
|
-
continue;
|
|
345
412
|
}
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
413
|
+
// Extension headers (auth, `${VAR}` refs) win over mcp.json's literal
|
|
414
|
+
// ones on a key collision: the portable file cannot carry a credential,
|
|
415
|
+
// so when both name the same header the extension is the operative one.
|
|
416
|
+
// Both sides pass the isRecord gate — spreading a malformed non-object
|
|
417
|
+
// value (a string, say) would scatter its indices into header keys.
|
|
418
|
+
const headers = {
|
|
419
|
+
...(verdict.entry.headers ?? {}),
|
|
420
|
+
...(isRecord(ext.headers) ? (ext.headers as Record<string, string>) : {}),
|
|
421
|
+
};
|
|
422
|
+
out.push({
|
|
423
|
+
...shared,
|
|
424
|
+
url,
|
|
425
|
+
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
426
|
+
...(ext.local === true ? { remote: false } : {}),
|
|
427
|
+
});
|
|
350
428
|
}
|
|
351
429
|
return out;
|
|
352
430
|
}
|
|
@@ -174,10 +174,19 @@ export class McpServerEditService {
|
|
|
174
174
|
const servers = mcp.mcpServers as Record<string, unknown>;
|
|
175
175
|
if (!(name in servers)) throw new McpServerEditError('No such server.', 404);
|
|
176
176
|
|
|
177
|
-
if (write.transport !== 'streamable-http' && write.transport !== '
|
|
178
|
-
// Persisting
|
|
179
|
-
//
|
|
180
|
-
|
|
177
|
+
if (write.transport !== 'streamable-http' && write.transport !== 'stdio') {
|
|
178
|
+
// Persisting a transport discovery refuses would save an entry that
|
|
179
|
+
// vanishes from the catalog the moment it is written — an unusable
|
|
180
|
+
// server with no error at the moment it was made. `sse` is refused BY
|
|
181
|
+
// NAME with the fix spelled out, because the pinned MCP client has no
|
|
182
|
+
// sse transport; an EXISTING sse entry stays readable here, so it can
|
|
183
|
+
// be edited to `streamable-http` rather than being stranded.
|
|
184
|
+
throw new McpServerEditError(
|
|
185
|
+
write.transport === 'sse'
|
|
186
|
+
? 'The MCP client has no `sse` transport — declare the server as `streamable-http` if it supports it.'
|
|
187
|
+
: `Unknown transport "${String(write.transport)}".`,
|
|
188
|
+
422,
|
|
189
|
+
);
|
|
181
190
|
}
|
|
182
191
|
// The EFFECTIVE value of every field, PATCH-over-stored (see
|
|
183
192
|
// McpServerWrite): `undefined` keeps what the files already say, a
|
|
@@ -88,6 +88,57 @@ export interface ToolVariable {
|
|
|
88
88
|
oauth?: ToolVariableOAuth;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* An optional cheap, read-only call that proves a credential actually WORKS.
|
|
93
|
+
*
|
|
94
|
+
* A `type: mcp` manual needs none — its handshake is authenticated, so merely
|
|
95
|
+
* connecting already tests the token. `http` and `inline` manuals have no such
|
|
96
|
+
* moment: registering an `http` manual fetches its DESCRIPTION, which providers
|
|
97
|
+
* usually serve publicly, so a wrong key sails through; an `inline` manual's
|
|
98
|
+
* discovery template points at Bevel's own API and never touches the provider
|
|
99
|
+
* at all. Without a declaration there is genuinely nothing to call, and the
|
|
100
|
+
* platform reports the connection as unverified rather than guessing.
|
|
101
|
+
*
|
|
102
|
+
* NOT inferred from the manual's own tools. Picking one automatically would
|
|
103
|
+
* mean calling an arbitrary tool with invented arguments — a probe that could
|
|
104
|
+
* send a message or create a record is worse than no probe. The author names a
|
|
105
|
+
* safe endpoint or the tool stays unverified.
|
|
106
|
+
*
|
|
107
|
+
* `${VAR}` refs resolve from the caller's vault exactly as they do in `url` and
|
|
108
|
+
* `headers`, which is the whole point: the probe must carry the same credential
|
|
109
|
+
* the real calls carry. Any 2xx is a pass; 401/403 is a rejection; anything
|
|
110
|
+
* else is treated as the provider being unwell rather than the key being wrong.
|
|
111
|
+
*/
|
|
112
|
+
export interface ToolHealthCheck {
|
|
113
|
+
/** Absolute URL to call. May contain `${VAR}` refs; SSRF-checked like `url`. */
|
|
114
|
+
url: string;
|
|
115
|
+
/** Default `GET`. A health check may not mutate, so nothing else is allowed. */
|
|
116
|
+
method?: 'GET';
|
|
117
|
+
/** Defaults to the manual's own `headers` — usually where the credential is. */
|
|
118
|
+
headers?: Record<string, string>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* One manual, reduced to exactly what testing its credential requires.
|
|
123
|
+
*
|
|
124
|
+
* Server-internal, like {@link ToolHealthCheck} and for the same reason: both
|
|
125
|
+
* `healthCheck.headers` and `callTemplate` can carry a literal token.
|
|
126
|
+
*/
|
|
127
|
+
export interface ToolProbeTarget {
|
|
128
|
+
name: string;
|
|
129
|
+
type: ToolManualType;
|
|
130
|
+
/** `false` for a local-only tool this server is not the one that can reach it. */
|
|
131
|
+
remote?: boolean;
|
|
132
|
+
/** The probe the `.tool` declares, if any. */
|
|
133
|
+
healthCheck?: ToolHealthCheck;
|
|
134
|
+
/**
|
|
135
|
+
* The validated UTCP call template, or `null` when the manual doesn't produce
|
|
136
|
+
* a valid one. Only the `mcp` probe uses it — its handshake IS the test — but
|
|
137
|
+
* it is built here so the probe never has to walk the catalog again.
|
|
138
|
+
*/
|
|
139
|
+
callTemplate: CallTemplate | null;
|
|
140
|
+
}
|
|
141
|
+
|
|
91
142
|
/**
|
|
92
143
|
* For a `type: mcp` tool, what an admin must do to make the remote server
|
|
93
144
|
* reachable — derived from OAuth auto-discovery, which a non-mcp tool has no
|
|
@@ -140,6 +191,15 @@ export interface ToolManualDescriptorBase {
|
|
|
140
191
|
variables?: ToolVariable[];
|
|
141
192
|
/** For `type: mcp`: the admin-facing setup requirement from auto-discovery. */
|
|
142
193
|
setup?: ToolManualSetup;
|
|
194
|
+
/**
|
|
195
|
+
* Optional credential probe — see {@link ToolHealthCheck}. Declared on
|
|
196
|
+
* `http`/`inline` manuals, and honoured on `type: mcp` too, where it
|
|
197
|
+
* OVERRIDES the default handshake probe (a server whose health endpoint is
|
|
198
|
+
* cheaper or more truthful than a full MCP handshake says so here).
|
|
199
|
+
* INTERNAL: lives on the descriptor, never on {@link ToolManualSummary},
|
|
200
|
+
* because it carries `headers`.
|
|
201
|
+
*/
|
|
202
|
+
healthCheck?: ToolHealthCheck;
|
|
143
203
|
}
|
|
144
204
|
|
|
145
205
|
/** The spawn spec of a stdio-declared MCP server (a plugin `mcp.json` entry). */
|
|
@@ -197,6 +257,10 @@ export interface ToolManualSummary {
|
|
|
197
257
|
remote?: boolean;
|
|
198
258
|
/** For `type: mcp`: the admin-facing setup requirement from auto-discovery. */
|
|
199
259
|
setup?: ToolManualSetup;
|
|
260
|
+
// NOTE: no `healthCheck` here, deliberately. This type is serialized straight
|
|
261
|
+
// to the browser by the tool endpoints, and a probe carries `headers` — which
|
|
262
|
+
// a `.tool` author may write as a literal token rather than a `${VAR}` ref.
|
|
263
|
+
// The server reads probe config through `IToolManualService.probeTargetFor`.
|
|
200
264
|
}
|
|
201
265
|
|
|
202
266
|
/** One thing an `inline` manual's embedded tool list says the assistant can do. */
|
|
@@ -269,6 +333,31 @@ export interface IToolManualService {
|
|
|
269
333
|
* a `resolve` reads the shared row or the caller's row.
|
|
270
334
|
*/
|
|
271
335
|
scopeOfVariable(effectiveKey: string): Promise<ToolVariableScope>;
|
|
336
|
+
/**
|
|
337
|
+
* Everything the credential probe needs about one readable manual, resolved
|
|
338
|
+
* in a SINGLE catalog + ACL pass.
|
|
339
|
+
*
|
|
340
|
+
* One accessor rather than three because the probe needs three facts that all
|
|
341
|
+
* come from the same file — is it local-only, does it declare a health check,
|
|
342
|
+
* and what call template would reach it — and asking for them separately made
|
|
343
|
+
* one probe walk the catalog four times, building call templates for every
|
|
344
|
+
* manual in the workspace to use exactly one of them.
|
|
345
|
+
*
|
|
346
|
+
* Deliberately NOT reachable through {@link ToolManualSummary}: that type is
|
|
347
|
+
* serialized straight to the browser by the tool endpoints, and a probe
|
|
348
|
+
* carries `headers` — which a `.tool` author may write as a literal token
|
|
349
|
+
* rather than a `${VAR}` ref. Probe config is internal to the server, so it
|
|
350
|
+
* travels by its own accessor and never rides a public DTO.
|
|
351
|
+
*
|
|
352
|
+
* Addressed by SLUG, which is what the route has: resolving the slug here
|
|
353
|
+
* rather than making the caller map it to a name first is what reduces a
|
|
354
|
+
* probe to one pass. `ToolProbeTarget.name` carries the UTCP namespace back
|
|
355
|
+
* out, since that is what the probe's `${VAR}` lookups are keyed by.
|
|
356
|
+
*
|
|
357
|
+
* `null` when no such manual exists OR the caller can't read it — the two are
|
|
358
|
+
* indistinguishable on purpose, as everywhere else in this contract.
|
|
359
|
+
*/
|
|
360
|
+
probeTargetFor(userEmail: string, slug: string): Promise<ToolProbeTarget | null>;
|
|
272
361
|
/**
|
|
273
362
|
* The per-user (`user`-scoped) variables a manual declares, each with the vault
|
|
274
363
|
* key (`<manual>_<VAR>`) the caller's value is stored under, its bare name, and
|