@bevel-software/platform-core-backend 0.14.0 → 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 +99 -8
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +18 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +96 -43
- package/dist/core/create-core-services.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/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/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/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/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 -92
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +6 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +18 -25
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +40 -2
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +227 -54
- 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.service.d.ts +52 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +242 -9
- 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 +9 -9
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +33 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +110 -16
- 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/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__/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 +123 -7
- package/src/core/create-core-services.ts +162 -41
- 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/plugin-principals.ts +137 -0
- package/src/modules/database/core-schema.ts +54 -0
- 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/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 +24 -1
- package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
- package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -88
- package/src/modules/tool-manuals/tool-manuals.service.ts +14 -23
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +2 -2
- 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.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 +239 -56
- package/src/modules/workflow/git/pull-request.service.ts +9 -0
- package/src/modules/workflow/workflow.service.ts +260 -9
- 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 +9 -17
- package/src/modules/workspace/workspace.service.ts +107 -16
- 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__/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/ttl-cache.ts +33 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
|
|
5
|
+
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
6
|
+
import type { IAccessControl } from '../../access/access-control.interface.js';
|
|
7
|
+
import type { ISkillService } from '../../skills/skills.contract.js';
|
|
8
|
+
import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
|
|
9
|
+
import type { PluginLinkIndex } from '../plugin-links.js';
|
|
10
|
+
import type { PluginSource } from '../discovery/plugin-source.js';
|
|
11
|
+
import { KbPluginSource } from '../discovery/kb-plugin-source.js';
|
|
12
|
+
import { compileMarketplace, type VirtualTree } from './compile-marketplace.js';
|
|
13
|
+
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
|
|
16
|
+
/** Whose view of the knowledge base to compile. */
|
|
17
|
+
export type CompileAudience = { userEmail: string } | { everyone: true };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The compiler wired to the live deployment: the default-branch checkout,
|
|
21
|
+
* the released catalog, the link index, and the access resolver as the read
|
|
22
|
+
* verdict. Every sink — the per-user git endpoint, a public mirror, a local
|
|
23
|
+
* export — asks this for a tree and writes it somewhere.
|
|
24
|
+
*
|
|
25
|
+
* Two audiences: a PERSON (their `canRead` per skill, exactly the catalog's
|
|
26
|
+
* filter) or EVERYONE (only what `read: everyone` reaches cleanly — the
|
|
27
|
+
* public subset a shared mirror may carry). Nothing in between: an audience
|
|
28
|
+
* that is "these three people" would need a union of verdicts nobody has
|
|
29
|
+
* asked for yet.
|
|
30
|
+
*/
|
|
31
|
+
export class MarketplaceCompilerService {
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly workspaceService: WorkspaceService,
|
|
34
|
+
private readonly accessControl: IAccessControl,
|
|
35
|
+
private readonly skillService: ISkillService,
|
|
36
|
+
private readonly links: PluginLinkIndex,
|
|
37
|
+
private readonly kbDirName: string,
|
|
38
|
+
private readonly marketplace: {
|
|
39
|
+
name: string;
|
|
40
|
+
owner: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
/** The hosted MCP endpoint to ship in the skills plugin (URL only). */
|
|
43
|
+
knowledgeBaseMcp?: { name: string; url: string };
|
|
44
|
+
},
|
|
45
|
+
private readonly source: PluginSource = new KbPluginSource(),
|
|
46
|
+
) {}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The default-branch commit the next compile would read. Throws when git
|
|
50
|
+
* cannot say: the repo service keys freshness on this value, and a stable
|
|
51
|
+
* placeholder would make every later compile look unnecessary — source
|
|
52
|
+
* changes and access revocations would never reach that caller again.
|
|
53
|
+
*/
|
|
54
|
+
async sourceCommit(): Promise<string> {
|
|
55
|
+
const { kbRoot } = await this.checkout();
|
|
56
|
+
const { stdout } = await execFileAsync('git', ['-C', kbRoot, 'rev-parse', 'HEAD']);
|
|
57
|
+
const sha = stdout.trim();
|
|
58
|
+
// SHA-1 or SHA-256 object format: any hex object id is a commit.
|
|
59
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(sha)) {
|
|
60
|
+
throw new Error(`could not read the default branch's commit (${sha || 'empty'})`);
|
|
61
|
+
}
|
|
62
|
+
return sha;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async compileFor(
|
|
66
|
+
audience: CompileAudience,
|
|
67
|
+
/** The commit to stamp the tree with, when the caller already read it (the freshness check does). */
|
|
68
|
+
knownCommit?: string,
|
|
69
|
+
): Promise<VirtualTree & { sourceCommit: string }> {
|
|
70
|
+
const { wsId, kbRoot } = await this.checkout();
|
|
71
|
+
// Strictness about the commit belongs to the FRESHNESS check (the repo
|
|
72
|
+
// service calls `sourceCommit()` itself and refuses to serve without
|
|
73
|
+
// one). A compile of a checkout git cannot describe still yields a
|
|
74
|
+
// correct tree; only its README and bundle version lose the sha.
|
|
75
|
+
const sourceCommit = knownCommit ?? (await this.sourceCommit().catch(() => 'unknown'));
|
|
76
|
+
// The tree is stamped with the checkout's commit, so every input must be
|
|
77
|
+
// read from that checkout NOW — not from a catalog scanned before the
|
|
78
|
+
// commit landed. The skill catalog, the link index and the access memo
|
|
79
|
+
// are TTL caches that nothing drops on an ordinary write (an MCP
|
|
80
|
+
// `write_files` moves HEAD and invalidates none of them), and the repo
|
|
81
|
+
// service keys freshness on the stamped commit: a tree compiled from a
|
|
82
|
+
// stale catalog under a fresh sha would be "current" until the NEXT
|
|
83
|
+
// commit, with the caller's marketplace frozen on content the knowledge
|
|
84
|
+
// base no longer has. Compiles run once per moved commit per caller, so
|
|
85
|
+
// the rescan is cheap where it matters.
|
|
86
|
+
this.skillService.invalidate();
|
|
87
|
+
this.links.invalidate();
|
|
88
|
+
this.accessControl.invalidate(wsId);
|
|
89
|
+
const skills = await this.skillService.listSkills(undefined);
|
|
90
|
+
const membership = await this.links.membership();
|
|
91
|
+
const { plugins, warnings: discoveryWarnings } = await this.source.discover(kbRoot);
|
|
92
|
+
const readable = await this.readPredicate(wsId, audience, skills.map((s) => `${s.path}/SKILL.md`));
|
|
93
|
+
const tree = await compileMarketplace({
|
|
94
|
+
kbRoot,
|
|
95
|
+
skills,
|
|
96
|
+
plugins,
|
|
97
|
+
membership,
|
|
98
|
+
readable,
|
|
99
|
+
options: { ...this.marketplace, sourceCommit },
|
|
100
|
+
});
|
|
101
|
+
// Discovery's warnings ride with the compile's: a plugin left out for a
|
|
102
|
+
// malformed manifest or an unresolved MCP profile is otherwise an
|
|
103
|
+
// omission nobody can diagnose from the tree alone.
|
|
104
|
+
return { ...tree, warnings: [...discoveryWarnings, ...tree.warnings], sourceCommit };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- internal --------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
private async checkout(): Promise<{ wsId: string; kbRoot: string }> {
|
|
110
|
+
const wsId = (await this.workspaceService.getOrCreateForBranch(DEFAULT_BRANCH)).id;
|
|
111
|
+
return { wsId, kbRoot: path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* One batch verdict per compile, then a lookup. Fail closed both ways: a
|
|
116
|
+
* path the batch skipped is unreadable, and for the everyone audience a
|
|
117
|
+
* skill is public only when the resolver says `read: everyone` applies
|
|
118
|
+
* cleanly (`restricted: false`).
|
|
119
|
+
*/
|
|
120
|
+
private async readPredicate(
|
|
121
|
+
wsId: string,
|
|
122
|
+
audience: CompileAudience,
|
|
123
|
+
docPaths: string[],
|
|
124
|
+
): Promise<(repoPath: string) => Promise<boolean>> {
|
|
125
|
+
if ('userEmail' in audience) {
|
|
126
|
+
const verdicts = docPaths.length
|
|
127
|
+
? await this.accessControl.canReadBatch(wsId, audience.userEmail, docPaths)
|
|
128
|
+
: new Map<string, boolean>();
|
|
129
|
+
return async (p) => verdicts.get(p) === true;
|
|
130
|
+
}
|
|
131
|
+
const cache = new Map<string, boolean>();
|
|
132
|
+
return async (p) => {
|
|
133
|
+
const hit = cache.get(p);
|
|
134
|
+
if (hit !== undefined) return hit;
|
|
135
|
+
let verdict = false;
|
|
136
|
+
try {
|
|
137
|
+
verdict = !(await this.accessControl.eligibleReaders(wsId, p)).restricted;
|
|
138
|
+
} catch {
|
|
139
|
+
verdict = false;
|
|
140
|
+
}
|
|
141
|
+
cache.set(p, verdict);
|
|
142
|
+
return verdict;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The default-branch workspace id compiles run against. */
|
|
148
|
+
export function compileWorkspaceId(): string {
|
|
149
|
+
return workspaceIdForBranch(DEFAULT_BRANCH);
|
|
150
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { DEFAULT_KB_LAYOUT, configureKbLayout } from '@bevel-software/platform-shared';
|
|
6
|
+
import { KbPluginSource } from '../kb-plugin-source.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The customer's tree, in their own root names (lowercase `plugins/` and
|
|
10
|
+
* `skills/`), with the four ownership scopes they use and a registry with an
|
|
11
|
+
* `extends` chain. The dialect must read it as plugins that link skills.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const REGISTRY = {
|
|
15
|
+
servers: [
|
|
16
|
+
{ id: 'jira', name: 'Jira', config: { command: 'npx', args: ['-y', 'jira-mcp'], env: { JIRA_URL: 'https://j' } } },
|
|
17
|
+
{ id: 'confluence', name: 'Confluence', config: { type: 'http', url: 'https://mcp.confluence.example' } },
|
|
18
|
+
{ id: 'flat-http', type: 'http', url: 'https://flat.example' },
|
|
19
|
+
],
|
|
20
|
+
profiles: [
|
|
21
|
+
{ id: 'base', servers: ['jira'] },
|
|
22
|
+
{ id: 'global', servers: ['confluence', 'ghost'], extends: 'base' },
|
|
23
|
+
{ id: 'loop-a', servers: ['flat-http'], extends: 'loop-b' },
|
|
24
|
+
{ id: 'loop-b', servers: [], extends: 'loop-a' },
|
|
25
|
+
{ id: 'empty', servers: [] },
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe('KbPluginSource — bundles', () => {
|
|
30
|
+
let kb: string;
|
|
31
|
+
const write = async (rel: string, text: string) => {
|
|
32
|
+
const abs = path.join(kb, rel);
|
|
33
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
34
|
+
await fs.writeFile(abs, text);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
beforeEach(async () => {
|
|
38
|
+
kb = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-bundle-'));
|
|
39
|
+
configureKbLayout({ knowledgeBaseDir: 'docs', skillsDir: 'skills', pluginsDir: 'plugins' });
|
|
40
|
+
await write('configs/mcp/registry.json', JSON.stringify(REGISTRY));
|
|
41
|
+
await write(
|
|
42
|
+
'plugins/functional/cluster-a/example-plugin/plugin.bundle.json',
|
|
43
|
+
JSON.stringify({
|
|
44
|
+
name: 'example-plugin',
|
|
45
|
+
version: '1.3.1',
|
|
46
|
+
description: 'What this plugin is for',
|
|
47
|
+
mcpProfile: 'global',
|
|
48
|
+
interface: { displayName: 'Example Plugin', category: 'Productivity' },
|
|
49
|
+
sourceSkillRoots: ['skills/departments/engineering/shared/cluster-a', 'skills/functional/cluster-a/one-skill'],
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
await write(
|
|
53
|
+
'plugins/departments/business/finance/close/plugin.bundle.json',
|
|
54
|
+
JSON.stringify({ name: 'close', version: '0.1.0', mcpProfile: 'empty', sourceSkillRoots: ['skills/departments/business/finance'] }),
|
|
55
|
+
);
|
|
56
|
+
await write('plugins/departments/engineering/shared/unnamed/plugin.bundle.json', JSON.stringify({ sourceSkillRoots: ['../escape', 'skills/x'] }));
|
|
57
|
+
// A bundle whose name is not its folder's, with no display name of its own.
|
|
58
|
+
await write('plugins/departments/business/finance/ledger-folder/plugin.bundle.json', JSON.stringify({ name: 'ledger' }));
|
|
59
|
+
await write('plugins/broken/plugin.bundle.json', '{ not json');
|
|
60
|
+
});
|
|
61
|
+
afterEach(async () => {
|
|
62
|
+
configureKbLayout({ ...DEFAULT_KB_LAYOUT });
|
|
63
|
+
await fs.rm(kb, { recursive: true, force: true });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('finds bundles at any depth and reads them as plugins that link skill roots', async () => {
|
|
67
|
+
const { plugins, warnings } = await new KbPluginSource().discover(kb);
|
|
68
|
+
const byName = new Map(plugins.map((p) => [p.name, p]));
|
|
69
|
+
expect([...byName.keys()].sort()).toEqual(['close', 'example-plugin', 'ledger', 'unnamed']);
|
|
70
|
+
// The shared contract: a declared display name, else the FOLDER — never the identity.
|
|
71
|
+
expect(byName.get('example-plugin')!.displayName).toBe('Example Plugin');
|
|
72
|
+
expect(byName.get('ledger')!.displayName).toBe('ledger-folder');
|
|
73
|
+
|
|
74
|
+
const example = byName.get('example-plugin')!;
|
|
75
|
+
expect(example.folder).toBe('plugins/functional/cluster-a/example-plugin');
|
|
76
|
+
expect(example.relFolder).toBe('functional/cluster-a/example-plugin');
|
|
77
|
+
expect(example.exists).toBe(true);
|
|
78
|
+
expect(example.linksAreManaged).toBe(false);
|
|
79
|
+
expect(example.linkedRoots).toEqual([
|
|
80
|
+
'skills/departments/engineering/shared/cluster-a',
|
|
81
|
+
'skills/functional/cluster-a/one-skill',
|
|
82
|
+
]);
|
|
83
|
+
expect(example.manifest).toEqual({
|
|
84
|
+
name: 'example-plugin',
|
|
85
|
+
version: '1.3.1',
|
|
86
|
+
description: 'What this plugin is for',
|
|
87
|
+
displayName: 'Example Plugin',
|
|
88
|
+
});
|
|
89
|
+
// An unnamed bundle takes its folder's name; a bad root is dropped with a warning.
|
|
90
|
+
expect(byName.get('unnamed')!.linkedRoots).toEqual(['skills/x']);
|
|
91
|
+
expect(warnings.some((w) => w.includes('../escape'))).toBe(true);
|
|
92
|
+
expect(warnings.some((w) => w.includes('plugins/broken'))).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('expands mcpProfile through the registry, following extends and reporting unknowns', async () => {
|
|
96
|
+
const { plugins, warnings } = await new KbPluginSource().discover(kb);
|
|
97
|
+
const example = plugins.find((p) => p.name === 'example-plugin')!;
|
|
98
|
+
expect(example.mcpServers).toEqual({
|
|
99
|
+
confluence: { type: 'streamable-http', url: 'https://mcp.confluence.example' },
|
|
100
|
+
jira: { type: 'stdio', command: 'npx', args: ['-y', 'jira-mcp'], env: { JIRA_URL: 'https://j' } },
|
|
101
|
+
});
|
|
102
|
+
expect(JSON.parse(example.mcpJsonText!)).toEqual({ mcpServers: example.mcpServers });
|
|
103
|
+
expect(warnings.some((w) => w.includes('unknown server "ghost"'))).toBe(true);
|
|
104
|
+
// An empty profile is valid and selects nothing.
|
|
105
|
+
expect(plugins.find((p) => p.name === 'close')!.mcpServers).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('skips a registry server that could not work, and the second of two declarations with one id', async () => {
|
|
109
|
+
await write(
|
|
110
|
+
'configs/mcp/registry.json',
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
servers: [
|
|
113
|
+
{ id: 'jira', config: { command: 'npx', args: ['-y', 'jira-mcp'] } },
|
|
114
|
+
{ id: 'jira', config: { command: 'something-else' } },
|
|
115
|
+
{ id: 'no-url', config: { type: 'http' } },
|
|
116
|
+
{ id: 'blank-url', config: { url: ' ' } },
|
|
117
|
+
{ id: 'odd', config: { type: 'grpc', url: 'https://x' } },
|
|
118
|
+
// A stray blank url beside a real command is still a stdio server.
|
|
119
|
+
{ id: 'launch', config: { type: 'stdio', command: 'run-it', url: '' } },
|
|
120
|
+
{ id: 'untyped-launch', config: { command: 'run-it', url: '' } },
|
|
121
|
+
// The client speaks no sse; shipping it would install a server nothing can use.
|
|
122
|
+
{ id: 'events', config: { type: 'sse', url: 'https://events.example' } },
|
|
123
|
+
// A name that cannot be a server name (the secret namespace, the route slug).
|
|
124
|
+
{ id: 'Bad Id', config: { command: 'x' } },
|
|
125
|
+
],
|
|
126
|
+
profiles: [
|
|
127
|
+
{ id: 'global', servers: ['jira', 'no-url', 'blank-url', 'odd', 'launch', 'untyped-launch', 'events', 'Bad Id'] },
|
|
128
|
+
],
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
const { plugins, warnings } = await new KbPluginSource().discover(kb);
|
|
132
|
+
const example = plugins.find((p) => p.name === 'example-plugin')!;
|
|
133
|
+
expect(example.mcpServers).toEqual({
|
|
134
|
+
jira: { type: 'stdio', command: 'npx', args: ['-y', 'jira-mcp'] },
|
|
135
|
+
launch: { type: 'stdio', command: 'run-it', args: [] },
|
|
136
|
+
'untyped-launch': { type: 'stdio', command: 'run-it', args: [] },
|
|
137
|
+
});
|
|
138
|
+
expect(warnings.some((w) => w.includes('"jira" is declared twice'))).toBe(true);
|
|
139
|
+
// The reasons are the shared judgement's — the same words an mcp.json entry gets.
|
|
140
|
+
expect(warnings.some((w) => w.includes('"no-url" no url'))).toBe(true);
|
|
141
|
+
expect(warnings.some((w) => w.includes('"blank-url" has neither a url nor a command'))).toBe(true);
|
|
142
|
+
expect(warnings.some((w) => w.includes('"odd" unknown type "grpc"'))).toBe(true);
|
|
143
|
+
expect(warnings.some((w) => w.includes('"events" the MCP client has no `sse` transport'))).toBe(true);
|
|
144
|
+
expect(warnings.some((w) => w.includes('"Bad Id" the name is the secret namespace'))).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('cuts an extends cycle instead of hanging, and keeps what it collected', async () => {
|
|
148
|
+
await write('plugins/loop/plugin.bundle.json', JSON.stringify({ name: 'loop', mcpProfile: 'loop-a' }));
|
|
149
|
+
const { plugins, warnings } = await new KbPluginSource().discover(kb);
|
|
150
|
+
const loop = plugins.find((p) => p.name === 'loop')!;
|
|
151
|
+
expect(loop.mcpServers).toEqual({ 'flat-http': { type: 'streamable-http', url: 'https://flat.example' } });
|
|
152
|
+
expect(warnings.some((w) => w.includes('cycle'))).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('a missing registry leaves the plugins standing, servers-less, with a warning per profile', async () => {
|
|
156
|
+
await fs.rm(path.join(kb, 'configs'), { recursive: true });
|
|
157
|
+
const { plugins, warnings } = await new KbPluginSource().discover(kb);
|
|
158
|
+
expect(plugins.find((p) => p.name === 'example-plugin')!.mcpServers).toBeNull();
|
|
159
|
+
expect(warnings.some((w) => w.includes('no registry'))).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('KbPluginSource — one walk, both shapes', () => {
|
|
164
|
+
let kb: string;
|
|
165
|
+
const write = async (rel: string, text: string) => {
|
|
166
|
+
const abs = path.join(kb, rel);
|
|
167
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
168
|
+
await fs.writeFile(abs, text);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
beforeEach(async () => {
|
|
172
|
+
kb = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-mixed-'));
|
|
173
|
+
});
|
|
174
|
+
afterEach(async () => {
|
|
175
|
+
configureKbLayout({ ...DEFAULT_KB_LAYOUT });
|
|
176
|
+
await fs.rm(kb, { recursive: true, force: true });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('reads native manifests and bundles side by side, at any depth, and stops at a plugin folder', async () => {
|
|
180
|
+
// A native plugin directly under the root, with a skill inside — the
|
|
181
|
+
// skill folder must not be mistaken for a nested plugin.
|
|
182
|
+
await write('Plugins/GTM/plugin.json', JSON.stringify({ name: 'gtm', extensions: { 'software.bevel.hexis': { skills: ['Skills/Eng'] } } }));
|
|
183
|
+
await write('Plugins/GTM/access.md', '---\n---\nread:\n - everyone\n');
|
|
184
|
+
await write('Plugins/GTM/skills/outreach/SKILL.md', '---\ndescription: x\n---\n');
|
|
185
|
+
await write('Plugins/GTM/skills/outreach/plugin.json', '{"name":"not-a-plugin"}');
|
|
186
|
+
// A pre-manifest folder (access.md only) directly under the root: NOT a
|
|
187
|
+
// plugin to discovery — the boot step gives it a manifest first.
|
|
188
|
+
await write('Plugins/Legacy/access.md', '---\n---\nread:\n - everyone\n');
|
|
189
|
+
// A bundle three folders down, and a native manifest two folders down.
|
|
190
|
+
await write('Plugins/functional/cluster/example/plugin.bundle.json', JSON.stringify({ name: 'example', sourceSkillRoots: ['Skills/x'] }));
|
|
191
|
+
await write('Plugins/teams/deep/plugin.json', JSON.stringify({ name: 'deep' }));
|
|
192
|
+
// A folder with BOTH files: the manifest wins.
|
|
193
|
+
await write('Plugins/Both/plugin.json', JSON.stringify({ name: 'both' }));
|
|
194
|
+
await write('Plugins/Both/plugin.bundle.json', JSON.stringify({ name: 'both-bundle', sourceSkillRoots: ['Skills/y'] }));
|
|
195
|
+
// A deeper folder that is only a container: walked through, never a plugin.
|
|
196
|
+
await write('Plugins/teams/empty-container/README.md', 'nothing here');
|
|
197
|
+
// A level-one scope that carries an access.md of its own AND plugins
|
|
198
|
+
// beneath: a container, not a plugin.
|
|
199
|
+
await write('Plugins/functional/access.md', '---\n---\nread:\n - everyone\n');
|
|
200
|
+
|
|
201
|
+
const { plugins, warnings } = await new KbPluginSource().discover(kb);
|
|
202
|
+
// Folders are visited in locale order (case-insensitive), depth first.
|
|
203
|
+
// Position means nothing: only the two files make a plugin.
|
|
204
|
+
expect(plugins.map((p) => [p.name, p.folder, p.linksAreManaged, p.exists])).toEqual([
|
|
205
|
+
['both', 'Plugins/Both', true, false],
|
|
206
|
+
['example', 'Plugins/functional/cluster/example', false, true],
|
|
207
|
+
['gtm', 'Plugins/GTM', true, true],
|
|
208
|
+
['deep', 'Plugins/teams/deep', true, false],
|
|
209
|
+
]);
|
|
210
|
+
expect(plugins.find((p) => p.name === 'gtm')?.linkedRoots).toEqual(['Skills/Eng']);
|
|
211
|
+
expect(plugins.find((p) => p.name === 'both')?.linkedRoots).toEqual([]);
|
|
212
|
+
expect(warnings).toEqual([]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('keeps the first of two plugins whose names fold to one slug, and says which was skipped', async () => {
|
|
216
|
+
await write('Plugins/a/GTM/plugin.json', '{}');
|
|
217
|
+
await write('Plugins/b/GTM/plugin.json', '{}');
|
|
218
|
+
// Different spelling, same manifest slug: still one plugin.
|
|
219
|
+
await write('Plugins/c/gtm/plugin.json', '{}');
|
|
220
|
+
const { plugins, warnings } = await new KbPluginSource().discover(kb);
|
|
221
|
+
expect(plugins.map((p) => p.folder)).toEqual(['Plugins/a/GTM']);
|
|
222
|
+
expect(warnings).toEqual([
|
|
223
|
+
'Plugins/b/GTM: plugin name "gtm" is already used by Plugins/a/GTM — plugin skipped',
|
|
224
|
+
'Plugins/c/gtm: plugin name "gtm" is already used by Plugins/a/GTM — plugin skipped',
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('the manifest name is the identity and the folder only the label — unless the name is no identifier', async () => {
|
|
229
|
+
await write('Plugins/GTM/plugin.json', JSON.stringify({ name: 'go-to-market', displayName: 'Go To Market' }));
|
|
230
|
+
await write('Plugins/Numeric/plugin.json', '{"name":42}');
|
|
231
|
+
// A name nested deeper than any stack would serialise: the warning must
|
|
232
|
+
// describe it, never try to print it.
|
|
233
|
+
await write('Plugins/Nested/plugin.json', `{"name":${'['.repeat(20000)}${']'.repeat(20000)}}`);
|
|
234
|
+
await write('Plugins/Ops/plugin.json', JSON.stringify({ name: 'Not An Identifier' }));
|
|
235
|
+
await write('Plugins/Plain/plugin.json', '{}');
|
|
236
|
+
const { plugins, warnings } = await new KbPluginSource().discover(kb);
|
|
237
|
+
expect(plugins.map((p) => [p.name, p.displayName, p.folder])).toEqual([
|
|
238
|
+
['go-to-market', 'Go To Market', 'Plugins/GTM'],
|
|
239
|
+
['nested', 'Nested', 'Plugins/Nested'],
|
|
240
|
+
['numeric', 'Numeric', 'Plugins/Numeric'],
|
|
241
|
+
['ops', 'Ops', 'Plugins/Ops'],
|
|
242
|
+
['plain', 'Plain', 'Plugins/Plain'],
|
|
243
|
+
]);
|
|
244
|
+
// Every PRESENT name that is no identifier is said out loud — whatever its type.
|
|
245
|
+
expect(warnings).toEqual([
|
|
246
|
+
'Plugins/Nested/plugin.json names a value of type array, which is not a plugin identifier (lowercase kebab-case) — the folder stands in as "nested"',
|
|
247
|
+
'Plugins/Numeric/plugin.json names a value of type number, which is not a plugin identifier (lowercase kebab-case) — the folder stands in as "numeric"',
|
|
248
|
+
'Plugins/Ops/plugin.json names "Not An Identifier", which is not a plugin identifier (lowercase kebab-case) — the folder stands in as "ops"',
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('counts what exists but could not be read — a folder, a manifest — so a writer can tell a hole from an absence', async () => {
|
|
253
|
+
await write('Plugins/GTM/plugin.json', '{"name":"gtm"}');
|
|
254
|
+
// An optional file that cannot be read carries no identity: a warning, never a hole.
|
|
255
|
+
await write('Plugins/GTM/mcp.json', '{"mcpServers":{}}');
|
|
256
|
+
await write('Plugins/Hidden/plugin.json', '{"name":"hidden-identity"}');
|
|
257
|
+
await write('Plugins/teams/Deep/plugin.json', '{"name":"deep"}');
|
|
258
|
+
const realReaddir = fs.readdir;
|
|
259
|
+
const realReadFile = fs.readFile;
|
|
260
|
+
const eacces = () => Promise.reject(Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }));
|
|
261
|
+
const spies = [
|
|
262
|
+
vi.spyOn(fs, 'readdir').mockImplementation(((dir: string, opts: unknown) =>
|
|
263
|
+
String(dir).endsWith('teams') ? eacces() : (realReaddir as (d: string, o: unknown) => Promise<unknown>).call(fs, dir, opts)) as never),
|
|
264
|
+
vi.spyOn(fs, 'readFile').mockImplementation(((file: string, opts: unknown) =>
|
|
265
|
+
(String(file).includes('Hidden') && String(file).endsWith('plugin.json')) || String(file).endsWith('mcp.json')
|
|
266
|
+
? eacces()
|
|
267
|
+
: (realReadFile as (f: string, o: unknown) => Promise<unknown>).call(fs, file, opts)) as never),
|
|
268
|
+
];
|
|
269
|
+
try {
|
|
270
|
+
const { plugins, warnings, unreadable } = await new KbPluginSource().discover(kb);
|
|
271
|
+
// The container could not be listed: nothing beneath it was seen. The
|
|
272
|
+
// manifest could not be read: an identity nobody could see, so the
|
|
273
|
+
// plugin is not listed under a guessed one. Both are holes, not absence.
|
|
274
|
+
expect(unreadable).toEqual(['Plugins/Hidden/plugin.json', 'Plugins/teams']);
|
|
275
|
+
expect(plugins.map((p) => p.name)).toEqual(['gtm']);
|
|
276
|
+
expect(warnings.some((w) => w.startsWith('Plugins/Hidden/plugin.json could not be read'))).toBe(true);
|
|
277
|
+
expect(warnings.some((w) => w.startsWith('Plugins/teams: could not be read'))).toBe(true);
|
|
278
|
+
// The unreadable mcp.json is said, and GTM stands without it.
|
|
279
|
+
expect(warnings.some((w) => w.startsWith('Plugins/GTM/mcp.json could not be read'))).toBe(true);
|
|
280
|
+
expect(plugins[0]!.mcpServers).toBeNull();
|
|
281
|
+
} finally {
|
|
282
|
+
for (const s of spies) s.mockRestore();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('a folder whose identity file could not be read is a hole that CLAIMS its subtree: nothing beneath surfaces as a plugin', async () => {
|
|
287
|
+
await write('Plugins/GTM/plugin.json', '{"name":"gtm"}');
|
|
288
|
+
await write('Plugins/Hidden/plugin.json', '{"name":"hidden"}');
|
|
289
|
+
// Beneath the hole: the walk still visits it (other listeners may need
|
|
290
|
+
// it), but to discovery it is inside a plugin folder — not a plugin.
|
|
291
|
+
await write('Plugins/Hidden/Sub/plugin.json', '{"name":"sub"}');
|
|
292
|
+
const realReadFile = fs.readFile;
|
|
293
|
+
const spy = vi.spyOn(fs, 'readFile').mockImplementation(((file: string, opts: unknown) =>
|
|
294
|
+
String(file).endsWith(path.join('Hidden', 'plugin.json'))
|
|
295
|
+
? Promise.reject(Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }))
|
|
296
|
+
: (realReadFile as (f: string, o: unknown) => Promise<unknown>).call(fs, file, opts)) as never);
|
|
297
|
+
try {
|
|
298
|
+
const { plugins, unreadable } = await new KbPluginSource().discover(kb);
|
|
299
|
+
expect(unreadable).toEqual(['Plugins/Hidden/plugin.json']);
|
|
300
|
+
expect(plugins.map((p) => p.name)).toEqual(['gtm']);
|
|
301
|
+
} finally {
|
|
302
|
+
spy.mockRestore();
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('a manifest that vanishes between the probe and the read is no plugin — never one under a guessed name', async () => {
|
|
307
|
+
await write('Plugins/GTM/plugin.json', '{"name":"gtm"}');
|
|
308
|
+
await write('Plugins/Gone/plugin.json', '{"name":"gone"}');
|
|
309
|
+
const realReadFile = fs.readFile;
|
|
310
|
+
const spy = vi.spyOn(fs, 'readFile').mockImplementation(((file: string, opts: unknown) =>
|
|
311
|
+
String(file).endsWith(path.join('Gone', 'plugin.json'))
|
|
312
|
+
? Promise.reject(Object.assign(new Error('ENOENT: no such file'), { code: 'ENOENT' }))
|
|
313
|
+
: (realReadFile as (f: string, o: unknown) => Promise<unknown>).call(fs, file, opts)) as never);
|
|
314
|
+
try {
|
|
315
|
+
const { plugins, unreadable } = await new KbPluginSource().discover(kb);
|
|
316
|
+
expect(plugins.map((p) => p.name)).toEqual(['gtm']);
|
|
317
|
+
expect(unreadable).toEqual([]);
|
|
318
|
+
} finally {
|
|
319
|
+
spy.mockRestore();
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('a knowledge base without a plugins root has no plugins and no complaint', async () => {
|
|
324
|
+
const { plugins, warnings, unreadable } = await new KbPluginSource().discover(kb);
|
|
325
|
+
expect(plugins).toEqual([]);
|
|
326
|
+
expect(warnings).toEqual([]);
|
|
327
|
+
expect(unreadable).toEqual([]);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isPersonalPluginDir, normalizeSkillRoot, pluginManifestName } from '@bevel-software/platform-shared';
|
|
4
|
+
import { isAbsence } from '../../../../shared/fs-errors.js';
|
|
5
|
+
import type { DiscoveredPlugin } from '../plugin-source.js';
|
|
6
|
+
import { expandProfile, parseRegistry, type McpRegistry } from './registry.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* THE DETACHABLE PART — a customer's plugin file format, read as-is:
|
|
10
|
+
*
|
|
11
|
+
* <pluginsRoot>/<scope…>/<plugin>/plugin.bundle.json
|
|
12
|
+
* {
|
|
13
|
+
* "name": "example-plugin", "version": "1.3.1", "description": "…",
|
|
14
|
+
* "mcpProfile": "global",
|
|
15
|
+
* "interface": { "displayName": "Example Plugin", "category": "…" },
|
|
16
|
+
* "sourceSkillRoots": [ "skills/departments/engineering/shared/<cluster>" ]
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* A bundle is a LIST OF SKILL PATHS plus a pointer into an MCP registry —
|
|
20
|
+
* which is exactly the native model's "a plugin links skills" with different
|
|
21
|
+
* field names. So the reader maps:
|
|
22
|
+
*
|
|
23
|
+
* sourceSkillRoots → linkedRoots (a root is a skill or a folder of skills)
|
|
24
|
+
* mcpProfile → mcpServers (expanded through the registry)
|
|
25
|
+
* name/version/desc → a synthesised manifest
|
|
26
|
+
* interface.displayName → the plugin's display name
|
|
27
|
+
*
|
|
28
|
+
* The walker (`kb-plugin-source.ts`) calls this for every folder holding a
|
|
29
|
+
* bundle file; this reads the file and decides nothing else. READ-ONLY by
|
|
30
|
+
* construction: nothing in this directory writes. A bundle's links are plain
|
|
31
|
+
* references (`linksAreManaged: false`): no principal grant is written or
|
|
32
|
+
* checked, the skills' own scopes decide readability.
|
|
33
|
+
*
|
|
34
|
+
* Deleting the dialect later = this directory, the one `else if` in the
|
|
35
|
+
* walker that calls it, and the walker's registry load.
|
|
36
|
+
*/
|
|
37
|
+
export const BUNDLE_FILE = 'plugin.bundle.json';
|
|
38
|
+
export const DEFAULT_REGISTRY_PATH = 'configs/mcp/registry.json';
|
|
39
|
+
|
|
40
|
+
/** The registry, when the repository has one; a missing file is null, a broken one warns. */
|
|
41
|
+
export async function loadRegistry(kbRoot: string, warnings: string[]): Promise<McpRegistry | null> {
|
|
42
|
+
let text: string;
|
|
43
|
+
try {
|
|
44
|
+
text = await fs.readFile(path.join(kbRoot, DEFAULT_REGISTRY_PATH), 'utf-8');
|
|
45
|
+
} catch (err) {
|
|
46
|
+
// No registry is a valid state: bundles without a profile are still
|
|
47
|
+
// plugins. A registry that exists but cannot be read is not.
|
|
48
|
+
if (!isAbsence(err)) {
|
|
49
|
+
const code = (err as { code?: unknown } | null)?.code;
|
|
50
|
+
warnings.push(`${DEFAULT_REGISTRY_PATH} could not be read (${String(code ?? err)}) — every mcpProfile is unresolved`);
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const registry = parseRegistry(text);
|
|
55
|
+
warnings.push(...registry.warnings);
|
|
56
|
+
return registry;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function readBundlePlugin(
|
|
60
|
+
dir: string,
|
|
61
|
+
folder: string,
|
|
62
|
+
relFolder: string,
|
|
63
|
+
registry: McpRegistry | null,
|
|
64
|
+
warnings: string[],
|
|
65
|
+
unreadable: string[],
|
|
66
|
+
): Promise<DiscoveredPlugin | null> {
|
|
67
|
+
// Reading and parsing are two failures with two meanings: a file that
|
|
68
|
+
// cannot be read is a plugin nobody could see (counted, so a writer can
|
|
69
|
+
// refuse); a file that is not a bundle is a plugin that is not there.
|
|
70
|
+
let text: string;
|
|
71
|
+
try {
|
|
72
|
+
text = await fs.readFile(path.join(dir, BUNDLE_FILE), 'utf-8');
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (!isAbsence(err)) {
|
|
75
|
+
warnings.push(`${folder}/${BUNDLE_FILE} could not be read — ${err instanceof Error ? err.message : String(err)}`);
|
|
76
|
+
unreadable.push(`${folder}/${BUNDLE_FILE}`);
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
let bundle: Record<string, unknown>;
|
|
81
|
+
try {
|
|
82
|
+
const parsed: unknown = JSON.parse(text);
|
|
83
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) throw new Error('not an object');
|
|
84
|
+
bundle = parsed as Record<string, unknown>;
|
|
85
|
+
} catch {
|
|
86
|
+
warnings.push(`${folder}/${BUNDLE_FILE} is not a JSON object — plugin skipped`);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const leaf = path.posix.basename(relFolder);
|
|
90
|
+
const name = typeof bundle.name === 'string' && bundle.name.trim() ? bundle.name.trim() : leaf;
|
|
91
|
+
|
|
92
|
+
const linkedRoots: string[] = [];
|
|
93
|
+
for (const raw of Array.isArray(bundle.sourceSkillRoots) ? bundle.sourceSkillRoots : []) {
|
|
94
|
+
const normalised = normalizeSkillRoot(typeof raw === 'string' ? raw : '');
|
|
95
|
+
if (normalised === null) {
|
|
96
|
+
warnings.push(`${folder}: sourceSkillRoots entry ${JSON.stringify(raw)} is not a folder path — ignored`);
|
|
97
|
+
} else if (!linkedRoots.includes(normalised)) {
|
|
98
|
+
linkedRoots.push(normalised);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let mcpServers: Record<string, unknown> | null = null;
|
|
103
|
+
if (typeof bundle.mcpProfile === 'string' && bundle.mcpProfile.trim()) {
|
|
104
|
+
if (!registry) {
|
|
105
|
+
warnings.push(`${folder}: mcpProfile "${bundle.mcpProfile}" named but no registry could be read`);
|
|
106
|
+
} else {
|
|
107
|
+
const expanded = expandProfile(registry, bundle.mcpProfile.trim());
|
|
108
|
+
warnings.push(...expanded.warnings.map((w) => `${folder}: ${w}`));
|
|
109
|
+
if (Object.keys(expanded.mcpServers).length > 0) mcpServers = expanded.mcpServers;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const ui =
|
|
114
|
+
typeof bundle.interface === 'object' && bundle.interface !== null
|
|
115
|
+
? (bundle.interface as Record<string, unknown>)
|
|
116
|
+
: {};
|
|
117
|
+
const manifest: Record<string, unknown> = { name: pluginManifestName(name) };
|
|
118
|
+
if (typeof bundle.version === 'string') manifest.version = bundle.version;
|
|
119
|
+
if (typeof bundle.description === 'string') manifest.description = bundle.description;
|
|
120
|
+
if (typeof ui.displayName === 'string') manifest.displayName = ui.displayName;
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
name,
|
|
124
|
+
// The shared contract (`DiscoveredPlugin.displayName`): the declared
|
|
125
|
+
// display name, else the FOLDER — never the identity.
|
|
126
|
+
displayName: typeof ui.displayName === 'string' && ui.displayName.trim() ? ui.displayName.trim() : leaf,
|
|
127
|
+
folder,
|
|
128
|
+
relFolder,
|
|
129
|
+
// The same rule as the native reader: a reserved personal folder directly
|
|
130
|
+
// under the root is a place, not a plugin, whatever file it carries.
|
|
131
|
+
personal: isPersonalPluginDir(folder),
|
|
132
|
+
exists: true,
|
|
133
|
+
manifest,
|
|
134
|
+
manifestText: null,
|
|
135
|
+
linkedRoots,
|
|
136
|
+
mcpServers,
|
|
137
|
+
mcpJsonText: mcpServers ? JSON.stringify({ mcpServers }) : null,
|
|
138
|
+
linksAreManaged: false,
|
|
139
|
+
};
|
|
140
|
+
}
|