@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
|
@@ -13,6 +13,8 @@ import { WorkflowDomainError } from '../../shared/domain-errors.js';
|
|
|
13
13
|
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
14
14
|
import { pluginsWorkspaceId } from './plugins.service.js';
|
|
15
15
|
import { PluginProvisionError, type PluginProvisionService } from './plugin-provision.service.js';
|
|
16
|
+
import { PluginLinkError, type PluginLinksService } from './plugin-links.service.js';
|
|
17
|
+
import { PluginRenameError, type PluginRenameService } from './plugin-rename.service.js';
|
|
16
18
|
import type { JoinRequestsService } from './join-requests.service.js';
|
|
17
19
|
import type {
|
|
18
20
|
PluginCatalogEntry,
|
|
@@ -61,13 +63,121 @@ export function createPluginsRoutes(
|
|
|
61
63
|
provision: PluginProvisionService,
|
|
62
64
|
kbDirName: string,
|
|
63
65
|
resolveUser: (req: express.Request) => Promise<AuthUser | null>,
|
|
66
|
+
/** Optional: a host without the link machinery simply has no link routes. */
|
|
67
|
+
links?: PluginLinksService,
|
|
68
|
+
/** Optional: a host without it has no rename route. */
|
|
69
|
+
rename?: PluginRenameService,
|
|
64
70
|
): express.Router {
|
|
65
71
|
const router = express.Router();
|
|
66
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Rename a plugin — its identifier, its display name, or both. The
|
|
75
|
+
* MANAGER's verb (write on the folder's access.md, the gate linking uses).
|
|
76
|
+
* An identifier change rewrites every grant naming the old identifier in
|
|
77
|
+
* the same commit; see `PluginRenameService`.
|
|
78
|
+
*
|
|
79
|
+
* PATCH /api/plugins/:name { name?, displayName? } → { name, displayName, rewritten }
|
|
80
|
+
*/
|
|
81
|
+
if (rename) {
|
|
82
|
+
router.patch('/plugins/:name', async (req, res) => {
|
|
83
|
+
if (!req.userEmail) {
|
|
84
|
+
res.status(401).json({ error: 'Unauthenticated' });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const user = await resolveUser(req);
|
|
88
|
+
if (!user) {
|
|
89
|
+
res.status(401).json({ error: 'Unauthenticated' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const body = (req.body ?? {}) as { name?: unknown; displayName?: unknown };
|
|
93
|
+
try {
|
|
94
|
+
res.json(await rename.rename(user, String(req.params.name), { name: body.name, displayName: body.displayName }));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err instanceof PluginRenameError) {
|
|
97
|
+
res.status(err.status).json({ error: err.message, ...err.payload });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (err instanceof WorkflowDomainError) {
|
|
101
|
+
res.status(err.status).json({ error: err.message, ...(err.payload ?? {}) });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
console.error('[plugins] rename failed:', err);
|
|
105
|
+
res.status(500).json({ error: 'Failed to rename the plugin' });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Linking shared skills — see `PluginLinksService` for the two-sided write.
|
|
112
|
+
*
|
|
113
|
+
* POST /api/plugins/:name/links { skillPath } → { root, skills }
|
|
114
|
+
* DELETE /api/plugins/:name/links?skillPath= → { root, revoked }
|
|
115
|
+
* POST /api/plugins/:name/links/repair { skillPath } → { root }
|
|
116
|
+
*
|
|
117
|
+
* Refusals carry a `kind` the UI branches on — `needs-skill-write` (409) is
|
|
118
|
+
* the one that becomes "request write access".
|
|
119
|
+
*/
|
|
120
|
+
if (links) {
|
|
121
|
+
const linkOp = async (
|
|
122
|
+
req: express.Request,
|
|
123
|
+
res: express.Response,
|
|
124
|
+
op: (user: AuthUser, plugin: string, skillPath: string) => Promise<unknown>,
|
|
125
|
+
skillPathOf: (req: express.Request) => unknown,
|
|
126
|
+
) => {
|
|
127
|
+
if (!req.userEmail) {
|
|
128
|
+
res.status(401).json({ error: 'Unauthenticated' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const user = await resolveUser(req);
|
|
132
|
+
if (!user) {
|
|
133
|
+
res.status(401).json({ error: 'Unauthenticated' });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const skillPath = skillPathOf(req);
|
|
137
|
+
if (typeof skillPath !== 'string' || !skillPath.trim()) {
|
|
138
|
+
res.status(400).json({ error: 'skillPath is required' });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
res.json(await op(user, String(req.params.name), skillPath));
|
|
143
|
+
} catch (err) {
|
|
144
|
+
if (err instanceof PluginLinkError) {
|
|
145
|
+
res.status(err.status).json({ error: err.message, ...err.payload });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (err instanceof WorkflowDomainError) {
|
|
149
|
+
res.status(err.status).json({ error: err.message, ...(err.payload ?? {}) });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
console.error('[plugins] link operation failed:', err);
|
|
153
|
+
res.status(500).json({ error: 'Failed to update the plugin\'s links' });
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const bodyPath = (req: express.Request) => ((req.body ?? {}) as { skillPath?: unknown }).skillPath;
|
|
157
|
+
router.post('/plugins/:name/links', (req, res) => linkOp(req, res, (u, p, s) => links.link(u, p, s), bodyPath));
|
|
158
|
+
router.post('/plugins/:name/links/repair', (req, res) =>
|
|
159
|
+
linkOp(req, res, (u, p, s) => links.repair(u, p, s), bodyPath),
|
|
160
|
+
);
|
|
161
|
+
router.delete('/plugins/:name/links', (req, res) =>
|
|
162
|
+
linkOp(req, res, (u, p, s) => links.unlink(u, p, s), (r) => r.query.skillPath),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
67
166
|
/** The folder-chain probe for MEMBERSHIP — the folder itself. */
|
|
68
167
|
const memberProbe = (folder: string) => folder;
|
|
69
168
|
/** The FILE probe for discovery/management — the folder's access.md. */
|
|
70
169
|
const accessMdOf = (folder: string) => `${folder}/access.md`;
|
|
170
|
+
/** A plugin folder's path BELOW the plugins root: `GTM`, or `teams/deep`. */
|
|
171
|
+
const folderBelowRoot = (folder: string) => folder.slice(folder.indexOf('/') + 1);
|
|
172
|
+
/**
|
|
173
|
+
* What a join request is keyed by: the plugin's primary FOLDER path below
|
|
174
|
+
* the root, not its identity. The request writes into that folder's rules;
|
|
175
|
+
* a top-level folder keys exactly as every join branch already on a remote
|
|
176
|
+
* was cut (its name), a nested one by its whole path, so two plugins whose
|
|
177
|
+
* folders share a basename never share a branch; and a rename of the
|
|
178
|
+
* identity moves no folder, so it orphans no open request.
|
|
179
|
+
*/
|
|
180
|
+
const joinKeyOf = (g: PluginCatalogEntry) => folderBelowRoot(g.folders[0]);
|
|
71
181
|
|
|
72
182
|
const probesFor = (plugins: PluginCatalogEntry[]): string[] => [
|
|
73
183
|
...new Set(plugins.flatMap((g) => g.folders.flatMap((f) => [memberProbe(f), accessMdOf(f)]))),
|
|
@@ -122,15 +232,18 @@ export function createPluginsRoutes(
|
|
|
122
232
|
const owner = any(owned, g, memberProbe);
|
|
123
233
|
const discoverable = member || any(readable, g, accessMdOf);
|
|
124
234
|
if (!member && !manager && !discoverable) continue; // absent — fail closed
|
|
125
|
-
const joinCr = member ? null : openJoinCr(mine, email, g
|
|
235
|
+
const joinCr = member ? null : openJoinCr(mine, email, joinKeyOf(g));
|
|
126
236
|
plugins.push({
|
|
127
237
|
name: g.name,
|
|
238
|
+
displayName: g.displayName,
|
|
128
239
|
folders: g.folders,
|
|
129
240
|
canRead: member,
|
|
130
241
|
canWrite: manager,
|
|
131
242
|
isOwner: owner,
|
|
243
|
+
linksAreManaged: g.linksAreManaged,
|
|
132
244
|
skillCount: g.skillCount,
|
|
133
245
|
toolCount: g.toolCount,
|
|
246
|
+
brokenLinks: g.brokenLinks,
|
|
134
247
|
owners: g.owners,
|
|
135
248
|
writers: g.writers,
|
|
136
249
|
readers: g.readers,
|
|
@@ -230,7 +343,7 @@ export function createPluginsRoutes(
|
|
|
230
343
|
res.status(401).json({ error: 'Unauthenticated' });
|
|
231
344
|
return;
|
|
232
345
|
}
|
|
233
|
-
//
|
|
346
|
+
// By identity — the manifest name the catalog keys on.
|
|
234
347
|
const plugin = (await pluginIndex.catalog()).find((g) => g.name === req.params.name);
|
|
235
348
|
const wsId = pluginsWorkspaceId();
|
|
236
349
|
const ownerVerdicts = plugin
|
|
@@ -242,7 +355,10 @@ export function createPluginsRoutes(
|
|
|
242
355
|
res.status(404).json({ error: 'Unknown plugin', kind: 'unknown-plugin' });
|
|
243
356
|
return;
|
|
244
357
|
}
|
|
245
|
-
|
|
358
|
+
// Provisioning works on FOLDERS (it created one); the identity only
|
|
359
|
+
// found the plugin. The whole path below the root, so a nested plugin
|
|
360
|
+
// is deleted where it is.
|
|
361
|
+
await provision.deletePlugin(user, folderBelowRoot(plugin.folders[0]));
|
|
246
362
|
pluginIndex.invalidate();
|
|
247
363
|
res.json({ ok: true });
|
|
248
364
|
} catch (err) {
|
|
@@ -268,7 +384,7 @@ export function createPluginsRoutes(
|
|
|
268
384
|
return;
|
|
269
385
|
}
|
|
270
386
|
const catalog = await pluginIndex.catalog();
|
|
271
|
-
//
|
|
387
|
+
// By identity — the manifest name the catalog keys on.
|
|
272
388
|
const plugin = catalog.find((g) => g.name === req.params.name);
|
|
273
389
|
const wsId = pluginsWorkspaceId();
|
|
274
390
|
// Same ANY-folder shape `GET /plugins` resolves with, so a plugin can
|
|
@@ -298,8 +414,8 @@ export function createPluginsRoutes(
|
|
|
298
414
|
// expects.
|
|
299
415
|
const folder = plugin.folders[0];
|
|
300
416
|
|
|
301
|
-
const branch = joinBranchFor(email, plugin
|
|
302
|
-
const existing = openJoinCr(await workflow.listChangeRequestsAuthoredBy(email), email, plugin
|
|
417
|
+
const branch = joinBranchFor(email, joinKeyOf(plugin));
|
|
418
|
+
const existing = openJoinCr(await workflow.listChangeRequestsAuthoredBy(email), email, joinKeyOf(plugin));
|
|
303
419
|
if (existing) {
|
|
304
420
|
res.json({ ok: true, number: existing.number });
|
|
305
421
|
return;
|
|
@@ -323,14 +439,15 @@ export function createPluginsRoutes(
|
|
|
323
439
|
);
|
|
324
440
|
if (spliced.changed) {
|
|
325
441
|
await workspaceService.writeFile(ws.id, accessPath, spliced.text);
|
|
326
|
-
await workflow.commitChanges(ws.id, user, `Request access to ${plugin.
|
|
442
|
+
await workflow.commitChanges(ws.id, user, `Request access to ${plugin.displayName}`);
|
|
327
443
|
}
|
|
328
444
|
const detail = await workflow.openChangeRequest(ws.id, user, {
|
|
329
445
|
sourceBranch: branch,
|
|
330
446
|
targetBranch: DEFAULT_BRANCH,
|
|
331
|
-
|
|
447
|
+
// People read these: the display name, not the identifier.
|
|
448
|
+
title: `Join request: ${plugin.displayName}`,
|
|
332
449
|
description:
|
|
333
|
-
`${user.name} asked to join ${plugin.
|
|
450
|
+
`${user.name} asked to join ${plugin.displayName}. A manager of the plugin accepts by ` +
|
|
334
451
|
`granting the access this branch proposes; the request closes itself once ` +
|
|
335
452
|
`every proposal has landed.`,
|
|
336
453
|
});
|
|
@@ -392,7 +509,7 @@ export function createPluginsRoutes(
|
|
|
392
509
|
if (!ctx) return;
|
|
393
510
|
const crs = await workflow.listChangeRequests();
|
|
394
511
|
res.json({
|
|
395
|
-
requests: await joinRequests.list(ctx.plugin
|
|
512
|
+
requests: await joinRequests.list(joinKeyOf(ctx.plugin), ctx.folder, crs, ctx.user),
|
|
396
513
|
});
|
|
397
514
|
} catch (err) {
|
|
398
515
|
console.error('[plugins] failed to list join requests:', err);
|
|
@@ -422,7 +539,7 @@ export function createPluginsRoutes(
|
|
|
422
539
|
res.status(404).json({ error: 'Not found' });
|
|
423
540
|
return;
|
|
424
541
|
}
|
|
425
|
-
res.json({ closed: await joinRequests.reconcile(ctx.plugin
|
|
542
|
+
res.json({ closed: await joinRequests.reconcile(joinKeyOf(ctx.plugin), ctx.folder, cr, ctx.user) });
|
|
426
543
|
} catch (err) {
|
|
427
544
|
console.error('[plugins] failed to reconcile a join request:', err);
|
|
428
545
|
res.status(500).json({ error: 'Failed to update the request' });
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import type { Dirent } from 'node:fs';
|
|
4
2
|
import {
|
|
5
3
|
DEFAULT_BRANCH,
|
|
6
|
-
PLUGINS_DIR,
|
|
7
|
-
pluginOfPath,
|
|
8
|
-
isPersonalPluginFolder,
|
|
9
4
|
} from '@bevel-software/platform-shared';
|
|
10
5
|
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
11
6
|
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
@@ -14,6 +9,9 @@ import type { ISkillService } from '../skills/skills.contract.js';
|
|
|
14
9
|
import type { IToolManualService } from '../tool-manuals/tool-manuals.contract.js';
|
|
15
10
|
import { TtlCache } from '../../shared/ttl-cache.js';
|
|
16
11
|
import type { PluginCatalogEntry, IPluginIndexService } from './plugins.contract.js';
|
|
12
|
+
import type { PluginLinkIndex } from './plugin-links.js';
|
|
13
|
+
import type { PluginSource } from './discovery/plugin-source.js';
|
|
14
|
+
import { KbPluginSource } from './discovery/kb-plugin-source.js';
|
|
17
15
|
|
|
18
16
|
const CACHE_TTL_MS = 60_000;
|
|
19
17
|
|
|
@@ -52,6 +50,14 @@ export class PluginIndexService implements IPluginIndexService {
|
|
|
52
50
|
private readonly toolManualService: IToolManualService,
|
|
53
51
|
private readonly kbDirName: string,
|
|
54
52
|
now: () => number = Date.now,
|
|
53
|
+
/**
|
|
54
|
+
* The link index, when the deployment has one: a plugin's skill count is
|
|
55
|
+
* then inline PLUS linked. Optional so hosts composing their own service
|
|
56
|
+
* set (and older tests) keep the inline-only count.
|
|
57
|
+
*/
|
|
58
|
+
private readonly links?: PluginLinkIndex,
|
|
59
|
+
/** Where plugins come from — native manifests unless a dialect is configured. */
|
|
60
|
+
private readonly source: PluginSource = new KbPluginSource(),
|
|
55
61
|
) {
|
|
56
62
|
this.cache = new TtlCache(CACHE_TTL_MS, now);
|
|
57
63
|
}
|
|
@@ -63,6 +69,9 @@ export class PluginIndexService implements IPluginIndexService {
|
|
|
63
69
|
async catalog(): Promise<PluginCatalogEntry[]> {
|
|
64
70
|
const cached = this.cache.get();
|
|
65
71
|
if (cached) return cached;
|
|
72
|
+
// See `TtlCache.begin`: taken before the read so an `invalidate()` that
|
|
73
|
+
// lands mid-build discards this result instead of being overwritten by it.
|
|
74
|
+
const token = this.cache.begin();
|
|
66
75
|
const entries = await this.build();
|
|
67
76
|
// A failed scan and a KB with genuinely no plugin folders both serve `[]`,
|
|
68
77
|
// but only the second is a fact worth holding for the TTL. Caching the
|
|
@@ -72,7 +81,7 @@ export class PluginIndexService implements IPluginIndexService {
|
|
|
72
81
|
// never arrive in that window. So a degraded read is served, not stored,
|
|
73
82
|
// and the next caller retries.
|
|
74
83
|
if (entries === null) return [];
|
|
75
|
-
this.cache.set(entries);
|
|
84
|
+
this.cache.set(entries, token);
|
|
76
85
|
return entries;
|
|
77
86
|
}
|
|
78
87
|
|
|
@@ -94,12 +103,13 @@ export class PluginIndexService implements IPluginIndexService {
|
|
|
94
103
|
const wsId = (await this.workspaceService.getOrCreateForBranch(DEFAULT_BRANCH)).id;
|
|
95
104
|
const kbRoot = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName);
|
|
96
105
|
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
106
|
+
const scanned = await this.scanFolders(kbRoot);
|
|
107
|
+
if (scanned.size === 0) return [];
|
|
108
|
+
const folders = new Map([...scanned].map(([name, p]) => [name, p.folders]));
|
|
99
109
|
|
|
100
|
-
const [skillCounts, toolCounts] = await Promise.all([
|
|
101
|
-
this.
|
|
102
|
-
this.countTools(),
|
|
110
|
+
const [{ skillCounts, brokenLinkCounts }, toolCounts] = await Promise.all([
|
|
111
|
+
this.countThroughLinks(folders),
|
|
112
|
+
this.countTools(folders),
|
|
103
113
|
]);
|
|
104
114
|
|
|
105
115
|
const entries: PluginCatalogEntry[] = [];
|
|
@@ -113,9 +123,12 @@ export class PluginIndexService implements IPluginIndexService {
|
|
|
113
123
|
]);
|
|
114
124
|
entries.push({
|
|
115
125
|
name,
|
|
126
|
+
displayName: scanned.get(name)?.displayName ?? name,
|
|
116
127
|
folders: pluginFolders,
|
|
128
|
+
linksAreManaged: scanned.get(name)?.linksAreManaged ?? false,
|
|
117
129
|
skillCount: skillCounts.get(name) ?? 0,
|
|
118
130
|
toolCount: toolCounts.get(name) ?? 0,
|
|
131
|
+
brokenLinks: brokenLinkCounts.get(name) ?? 0,
|
|
119
132
|
owners,
|
|
120
133
|
writers,
|
|
121
134
|
readers,
|
|
@@ -130,59 +143,99 @@ export class PluginIndexService implements IPluginIndexService {
|
|
|
130
143
|
}
|
|
131
144
|
}
|
|
132
145
|
|
|
133
|
-
/**
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
146
|
+
/**
|
|
147
|
+
* name → repo-relative plugin folder (always a single-element list), from
|
|
148
|
+
* the configured source. Personal folders live under Plugins/ but are not
|
|
149
|
+
* plugins: one exists per person, private by construction, and listing them
|
|
150
|
+
* would put a locked row per employee in everyone's index. `exists` is the
|
|
151
|
+
* source's existence rule (see `DiscoveredPlugin`) — for native plugins,
|
|
152
|
+
* the `access.md` the class doc describes.
|
|
153
|
+
*/
|
|
154
|
+
private async scanFolders(
|
|
155
|
+
kbRoot: string,
|
|
156
|
+
): Promise<Map<string, { folders: string[]; linksAreManaged: boolean; displayName: string }>> {
|
|
157
|
+
const byName = new Map<string, { folders: string[]; linksAreManaged: boolean; displayName: string }>();
|
|
158
|
+
const discovered = await this.source.discover(kbRoot);
|
|
159
|
+
for (const w of discovered.warnings) console.warn(`[plugins] ${w}`);
|
|
160
|
+
for (const plugin of discovered.plugins) {
|
|
161
|
+
if (plugin.personal || !plugin.exists) continue;
|
|
162
|
+
byName.set(plugin.name, {
|
|
163
|
+
folders: [plugin.folder],
|
|
164
|
+
linksAreManaged: plugin.linksAreManaged,
|
|
165
|
+
displayName: plugin.displayName,
|
|
166
|
+
});
|
|
141
167
|
}
|
|
142
|
-
const candidates = children.filter(
|
|
143
|
-
(child) =>
|
|
144
|
-
child.isDirectory() &&
|
|
145
|
-
!child.name.startsWith('.') &&
|
|
146
|
-
// Personal folders live under Plugins/ but are not plugins: one exists
|
|
147
|
-
// per person, private by construction, and listing them would put a
|
|
148
|
-
// locked row per employee in everyone's index.
|
|
149
|
-
!isPersonalPluginFolder(child.name),
|
|
150
|
-
);
|
|
151
|
-
// A plugin exists exactly when its folder carries an `access.md` (see the
|
|
152
|
-
// class doc) — stat that file, don't trust the directory.
|
|
153
|
-
const verdicts = await Promise.all(
|
|
154
|
-
candidates.map(async (child) => {
|
|
155
|
-
try {
|
|
156
|
-
return (await fs.stat(path.join(kbRoot, PLUGINS_DIR, child.name, 'access.md'))).isFile();
|
|
157
|
-
} catch {
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
}),
|
|
161
|
-
);
|
|
162
|
-
candidates.forEach((child, i) => {
|
|
163
|
-
if (verdicts[i]) byName.set(child.name, [`${PLUGINS_DIR}/${child.name}`]);
|
|
164
|
-
});
|
|
165
168
|
return byName;
|
|
166
169
|
}
|
|
167
170
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Two counts from ONE read of the link index — the membership is built
|
|
173
|
+
* once per cold catalog, not once per count: `TtlCache` has no
|
|
174
|
+
* single-flight, so two concurrent readers of an empty cache would each
|
|
175
|
+
* discover the tree and resolve every linked skill's access.
|
|
176
|
+
*
|
|
177
|
+
* - `skillCounts`: with links, a skill counts for EVERY plugin that holds
|
|
178
|
+
* it — inline in its folder, or linked from a manifest. Personal folders
|
|
179
|
+
* are already absent from the membership (they are places, not plugins).
|
|
180
|
+
* - `brokenLinkCounts`: how many linked skills each plugin's members
|
|
181
|
+
* cannot read — memberships reported as linked but not granted. From the
|
|
182
|
+
* UNFILTERED index, so the count reaches the plugin's managers even when
|
|
183
|
+
* the missing grant locks them out of the skill too.
|
|
184
|
+
*
|
|
185
|
+
* Without a link index: skills bucket by folder, and no link can be broken.
|
|
186
|
+
*/
|
|
187
|
+
private async countThroughLinks(
|
|
188
|
+
folders: Map<string, string[]>,
|
|
189
|
+
): Promise<{ skillCounts: Map<string, number>; brokenLinkCounts: Map<string, number> }> {
|
|
190
|
+
const skillCounts = new Map<string, number>();
|
|
191
|
+
const brokenLinkCounts = new Map<string, number>();
|
|
192
|
+
if (!this.links) {
|
|
193
|
+
// `undefined` is the documented GLOBAL, unfiltered mode — counts are a
|
|
194
|
+
// property of the plugin, not of who is asking.
|
|
195
|
+
return {
|
|
196
|
+
skillCounts: bucketByFolder(await this.skillService.listSkills(undefined), folders),
|
|
197
|
+
brokenLinkCounts,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
for (const memberships of (await this.links.membership()).bySkill.values()) {
|
|
201
|
+
for (const m of memberships) {
|
|
202
|
+
skillCounts.set(m.name, (skillCounts.get(m.name) ?? 0) + 1);
|
|
203
|
+
if (m.linked && !m.granted) brokenLinkCounts.set(m.name, (brokenLinkCounts.get(m.name) ?? 0) + 1);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { skillCounts, brokenLinkCounts };
|
|
172
207
|
}
|
|
173
208
|
|
|
174
|
-
|
|
175
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Tools belong to the plugin whose FOLDER holds them — matched by path
|
|
211
|
+
* prefix against the discovered folders, not by `pluginOfPath`'s
|
|
212
|
+
* second-segment rule, so a dialect plugin nested three folders deep still
|
|
213
|
+
* counts the servers its bundle expands.
|
|
214
|
+
*/
|
|
215
|
+
private async countTools(folders: Map<string, string[]>): Promise<Map<string, number>> {
|
|
216
|
+
const counts = new Map<string, number>();
|
|
217
|
+
const byFolder = [...folders.entries()].map(([name, [folder]]) => ({ name, prefix: `${folder}/` }));
|
|
218
|
+
for (const tool of await this.toolManualService.listAllSummaries()) {
|
|
219
|
+
const owner = byFolder.find((f) => tool.path.startsWith(f.prefix));
|
|
220
|
+
if (!owner) continue;
|
|
221
|
+
counts.set(owner.name, (counts.get(owner.name) ?? 0) + 1);
|
|
222
|
+
}
|
|
223
|
+
return counts;
|
|
176
224
|
}
|
|
177
225
|
}
|
|
178
226
|
|
|
179
|
-
/**
|
|
180
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Count items per plugin by FOLDER PREFIX against the discovered folders —
|
|
229
|
+
* not by `pluginOfPath`'s second-segment rule, which would file a nested
|
|
230
|
+
* plugin's skills under its grouping folder. Items in no plugin count nowhere.
|
|
231
|
+
*/
|
|
232
|
+
function bucketByFolder(items: { path: string }[], folders: Map<string, string[]>): Map<string, number> {
|
|
181
233
|
const counts = new Map<string, number>();
|
|
234
|
+
const byFolder = [...folders.entries()].map(([name, [folder]]) => ({ name, prefix: `${folder}/` }));
|
|
182
235
|
for (const item of items) {
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
185
|
-
counts.set(
|
|
236
|
+
const owner = byFolder.find((f) => item.path.startsWith(f.prefix));
|
|
237
|
+
if (!owner) continue;
|
|
238
|
+
counts.set(owner.name, (counts.get(owner.name) ?? 0) + 1);
|
|
186
239
|
}
|
|
187
240
|
return counts;
|
|
188
241
|
}
|
|
@@ -150,6 +150,63 @@ describe('DeploymentSettingsService — secrets', () => {
|
|
|
150
150
|
});
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
+
describe('DeploymentSettingsService — KB layout', () => {
|
|
154
|
+
it('resolves the three roots to their defaults when nothing names them', () => {
|
|
155
|
+
const { db } = makeDb();
|
|
156
|
+
const settings = new DeploymentSettingsService(db, ENC_KEY);
|
|
157
|
+
expect(settings.resolveKbLayout()).toEqual({
|
|
158
|
+
knowledgeBaseDir: 'KnowledgeBase',
|
|
159
|
+
skillsDir: 'Skills',
|
|
160
|
+
pluginsDir: 'Plugins',
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('takes a renamed root from the environment first, then the store', async () => {
|
|
165
|
+
const { db } = makeDb();
|
|
166
|
+
const settings = new DeploymentSettingsService(db, ENC_KEY);
|
|
167
|
+
await settings.save({ skillsDir: 'skills', pluginsDir: 'plugins' }, null);
|
|
168
|
+
expect(settings.resolveKbLayout()).toMatchObject({ skillsDir: 'skills', pluginsDir: 'plugins' });
|
|
169
|
+
process.env.KB_SKILLS_DIR = 'capabilities';
|
|
170
|
+
expect(settings.resolveKbLayout().skillsDir).toBe('capabilities');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The trio rule is judged on the layout the save WOULD produce: a plugins
|
|
175
|
+
* folder renamed to collide with the skills folder already in effect is a
|
|
176
|
+
* collision even though the batch names only one of them.
|
|
177
|
+
*/
|
|
178
|
+
it('refuses two roots that share a name, case-insensitively', async () => {
|
|
179
|
+
const { db } = makeDb();
|
|
180
|
+
const settings = new DeploymentSettingsService(db, ENC_KEY);
|
|
181
|
+
await expect(settings.save({ pluginsDir: 'skills' }, null)).rejects.toBeInstanceOf(
|
|
182
|
+
SettingsValidationError,
|
|
183
|
+
);
|
|
184
|
+
await expect(settings.save({ pluginsDir: 'SKILLS' }, null)).rejects.toBeInstanceOf(
|
|
185
|
+
SettingsValidationError,
|
|
186
|
+
);
|
|
187
|
+
await expect(
|
|
188
|
+
settings.save({ knowledgeBaseDir: 'Content', skillsDir: 'content' }, null),
|
|
189
|
+
).rejects.toBeInstanceOf(SettingsValidationError);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('refuses a root that is not a single plain folder name', async () => {
|
|
193
|
+
const { db } = makeDb();
|
|
194
|
+
const settings = new DeploymentSettingsService(db, ENC_KEY);
|
|
195
|
+
for (const bad of ['a/b', '..', '.hidden', 'x\\y']) {
|
|
196
|
+
await expect(settings.save({ skillsDir: bad }, null)).rejects.toBeInstanceOf(
|
|
197
|
+
SettingsValidationError,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('marks a layout change as restart-to-apply', async () => {
|
|
203
|
+
const { db } = makeDb();
|
|
204
|
+
const settings = new DeploymentSettingsService(db, ENC_KEY);
|
|
205
|
+
const { restartRequired } = await settings.save({ pluginsDir: 'Bundles' }, null);
|
|
206
|
+
expect(restartRequired).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
153
210
|
describe('DeploymentSettingsService — validation', () => {
|
|
154
211
|
it('rejects the whole batch when one field is wrong', async () => {
|
|
155
212
|
const { db, rows } = makeDb();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Server as HttpServer } from 'node:http';
|
|
2
|
+
import type { AddressInfo } from 'node:net';
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { createSetupRoutes, type LastSyncStatus } from '../setup.routes.js';
|
|
7
|
+
import { DeploymentSettingsService } from '../deployment-settings.service.js';
|
|
8
|
+
import type { IAdminAccessService } from '../../admin/admin.interface.js';
|
|
9
|
+
import type { Database } from '../../database/connection.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The remote-sync facts on `GET /setup/status`: the address a hook calls and
|
|
13
|
+
* what the last sync did. Admin-only like the rest of the settings — a
|
|
14
|
+
* non-admin learns nothing about the deployment's endpoints.
|
|
15
|
+
*/
|
|
16
|
+
const ENC_KEY = randomBytes(32).toString('base64');
|
|
17
|
+
|
|
18
|
+
let server: HttpServer | null = null;
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
server?.close();
|
|
21
|
+
server = null;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function listen(isAdmin: boolean, last: LastSyncStatus | null) {
|
|
25
|
+
const db = {
|
|
26
|
+
select: () => ({ from: () => Promise.resolve([]) }),
|
|
27
|
+
insert: () => ({ values: () => ({ onConflictDoUpdate: () => Promise.resolve() }) }),
|
|
28
|
+
delete: () => ({ where: () => Promise.resolve() }),
|
|
29
|
+
} as unknown as Database;
|
|
30
|
+
const settings = new DeploymentSettingsService(db, ENC_KEY);
|
|
31
|
+
const app = express();
|
|
32
|
+
app.use(express.json());
|
|
33
|
+
app.use((req, _res, next) => {
|
|
34
|
+
req.userEmail = 'root@example.com';
|
|
35
|
+
req.userId = 'user-1';
|
|
36
|
+
next();
|
|
37
|
+
});
|
|
38
|
+
app.use(
|
|
39
|
+
'/api',
|
|
40
|
+
createSetupRoutes(
|
|
41
|
+
settings,
|
|
42
|
+
{ isAdmin: async () => isAdmin } as IAdminAccessService,
|
|
43
|
+
{ runAll: async () => {} },
|
|
44
|
+
{ url: 'https://hexis.example.test/api/sync', lastSync: () => last },
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
server = app.listen(0);
|
|
48
|
+
const { port } = server.address() as AddressInfo;
|
|
49
|
+
return `http://127.0.0.1:${port}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('GET /setup/status — remote sync', () => {
|
|
53
|
+
it('tells an admin the address and the last sync', async () => {
|
|
54
|
+
const last: LastSyncStatus = {
|
|
55
|
+
at: 1_700_000_000_000,
|
|
56
|
+
by: 'bearer',
|
|
57
|
+
status: 'synced',
|
|
58
|
+
results: [{ branch: 'main', outcome: 'updated' }],
|
|
59
|
+
};
|
|
60
|
+
const res = await fetch(`${listen(true, last)}/api/setup/status`);
|
|
61
|
+
const body = (await res.json()) as { sync?: unknown };
|
|
62
|
+
expect(body.sync).toEqual({ url: 'https://hexis.example.test/api/sync', last });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('says null before the first sync', async () => {
|
|
66
|
+
const res = await fetch(`${listen(true, null)}/api/setup/status`);
|
|
67
|
+
const body = (await res.json()) as { sync?: { last: unknown } };
|
|
68
|
+
expect(body.sync?.last).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('tells a non-admin nothing', async () => {
|
|
72
|
+
const res = await fetch(`${listen(false, null)}/api/setup/status`);
|
|
73
|
+
const body = (await res.json()) as Record<string, unknown>;
|
|
74
|
+
expect(body.sync).toBeUndefined();
|
|
75
|
+
expect(body.settings).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
});
|