@bevel-software/platform-core-backend 0.13.6 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +6 -6
- package/dist/core/catalog-cache-invalidation.d.ts +29 -0
- package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
- package/dist/core/catalog-cache-invalidation.js +71 -0
- package/dist/core/catalog-cache-invalidation.js.map +1 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +101 -9
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +20 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +105 -43
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +12 -5
- package/dist/core-config.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +18 -6
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +26 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +213 -71
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +59 -7
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +51 -1
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +95 -1
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/access-model/group-files.d.ts.map +1 -1
- package/dist/modules/access-model/group-files.js +4 -1
- package/dist/modules/access-model/group-files.js.map +1 -1
- package/dist/modules/access-model/kb-read-filter.d.ts +12 -0
- package/dist/modules/access-model/kb-read-filter.d.ts.map +1 -1
- package/dist/modules/access-model/kb-read-filter.js +15 -0
- package/dist/modules/access-model/kb-read-filter.js.map +1 -1
- package/dist/modules/access-model/plugin-principals.d.ts +49 -0
- package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
- package/dist/modules/access-model/plugin-principals.js +125 -0
- package/dist/modules/access-model/plugin-principals.js.map +1 -0
- package/dist/modules/connection-probe/connection-probe.contract.d.ts +43 -0
- package/dist/modules/connection-probe/connection-probe.contract.d.ts.map +1 -0
- package/dist/modules/connection-probe/connection-probe.contract.js +2 -0
- package/dist/modules/connection-probe/connection-probe.contract.js.map +1 -0
- package/dist/modules/connection-probe/connection-probe.service.d.ts +94 -0
- package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -0
- package/dist/modules/connection-probe/connection-probe.service.js +684 -0
- package/dist/modules/connection-probe/connection-probe.service.js.map +1 -0
- package/dist/modules/connection-probe/index.d.ts +3 -0
- package/dist/modules/connection-probe/index.d.ts.map +1 -0
- package/dist/modules/connection-probe/index.js +3 -0
- package/dist/modules/connection-probe/index.js.map +1 -0
- package/dist/modules/database/core-schema.d.ts +326 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +52 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/diff/diff.routes.d.ts.map +1 -1
- package/dist/modules/diff/diff.routes.js +3 -5
- package/dist/modules/diff/diff.routes.js.map +1 -1
- package/dist/modules/kb-fs/mutex.d.ts +37 -0
- package/dist/modules/kb-fs/mutex.d.ts.map +1 -1
- package/dist/modules/kb-fs/mutex.js +48 -5
- package/dist/modules/kb-fs/mutex.js.map +1 -1
- package/dist/modules/kb-sync/index.d.ts +6 -0
- package/dist/modules/kb-sync/index.d.ts.map +1 -0
- package/dist/modules/kb-sync/index.js +5 -0
- package/dist/modules/kb-sync/index.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
- package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.js +183 -0
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
- package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-auth.js +62 -0
- package/dist/modules/kb-sync/sync-auth.js.map +1 -0
- package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
- package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-payload.js +105 -0
- package/dist/modules/kb-sync/sync-payload.js.map +1 -0
- package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
- package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/git-http.routes.js +190 -0
- package/dist/modules/marketplace/git-http.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
- package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/index.js +6 -0
- package/dist/modules/marketplace/github-facade/index.js.map +1 -0
- package/dist/modules/marketplace/index.d.ts +4 -0
- package/dist/modules/marketplace/index.d.ts.map +1 -0
- package/dist/modules/marketplace/index.js +4 -0
- package/dist/modules/marketplace/index.js.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
- package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
- package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.js +3 -1
- package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
- package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
- package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
- package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
- package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
- package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
- package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/native.source.js +102 -0
- package/dist/modules/plugins/discovery/native.source.js.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.js +2 -0
- package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
- package/dist/modules/plugins/index.d.ts +10 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +8 -0
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-links.d.ts +66 -0
- package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.js +160 -0
- package/dist/modules/plugins/plugin-links.js.map +1 -0
- package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
- package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.service.js +304 -0
- package/dist/modules/plugins/plugin-links.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +152 -52
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
- package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-rename.service.js +245 -0
- package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +27 -1
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +7 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +120 -12
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/plugins/plugins.service.d.ts +50 -3
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.service.js +108 -47
- package/dist/modules/plugins/plugins.service.js.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.js +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +6 -0
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js +31 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +82 -2
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts +26 -0
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +9 -1
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/index.d.ts +1 -0
- package/dist/modules/skills/index.d.ts.map +1 -1
- package/dist/modules/skills/index.js +1 -0
- package/dist/modules/skills/index.js.map +1 -1
- package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
- package/dist/modules/skills/pending-skills.service.js +9 -4
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
- package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
- package/dist/modules/skills/skill-access-requests.routes.js +168 -0
- package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
- package/dist/modules/skills/skills.contract.d.ts +31 -0
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.d.ts +28 -3
- package/dist/modules/skills/skills.routes.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.js +42 -3
- package/dist/modules/skills/skills.routes.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +4 -0
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +83 -22
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
- package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +26 -9
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -83
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +10 -4
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +83 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +15 -2
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +199 -38
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +58 -3
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +325 -60
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +8 -0
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts +2 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +98 -9
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +133 -9
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +462 -52
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +11 -14
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +38 -1
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +131 -20
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +20 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +29 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/fs-errors.d.ts +8 -0
- package/dist/shared/fs-errors.d.ts.map +1 -0
- package/dist/shared/fs-errors.js +11 -0
- package/dist/shared/fs-errors.js.map +1 -0
- package/dist/shared/fs-walk.d.ts +20 -6
- package/dist/shared/fs-walk.d.ts.map +1 -1
- package/dist/shared/fs-walk.js +35 -27
- package/dist/shared/fs-walk.js.map +1 -1
- package/dist/shared/kb-walk.d.ts +52 -0
- package/dist/shared/kb-walk.d.ts.map +1 -0
- package/dist/shared/kb-walk.js +76 -0
- package/dist/shared/kb-walk.js.map +1 -0
- package/dist/shared/path-order.d.ts +18 -0
- package/dist/shared/path-order.d.ts.map +1 -0
- package/dist/shared/path-order.js +35 -0
- package/dist/shared/path-order.js.map +1 -0
- package/dist/shared/token-crypto.d.ts +17 -2
- package/dist/shared/token-crypto.d.ts.map +1 -1
- package/dist/shared/token-crypto.js +25 -8
- package/dist/shared/token-crypto.js.map +1 -1
- package/dist/shared/ttl-cache.d.ts +28 -1
- package/dist/shared/ttl-cache.d.ts.map +1 -1
- package/dist/shared/ttl-cache.js +33 -1
- package/dist/shared/ttl-cache.js.map +1 -1
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +95 -30
- package/kb-template/Skills/.gitkeep +0 -0
- package/migrations/0005_github_facade.sql +25 -0
- package/migrations/meta/0005_snapshot.json +1717 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/__tests__/core-config.admin.test.ts +21 -0
- package/src/__tests__/kb-layout-config.test.ts +110 -0
- package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
- package/src/core/catalog-cache-invalidation.ts +92 -0
- package/src/core/create-core-server.ts +126 -7
- package/src/core/create-core-services.ts +173 -41
- package/src/core-config.ts +14 -7
- package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
- package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
- package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
- package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
- package/src/modules/access/access-control.interface.ts +20 -9
- package/src/modules/access/access-control.service.ts +240 -78
- package/src/modules/access/access.routes.ts +96 -14
- package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
- package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
- package/src/modules/access-model/access-grammar.ts +114 -2
- package/src/modules/access-model/group-files.ts +4 -0
- package/src/modules/access-model/kb-read-filter.ts +22 -0
- package/src/modules/access-model/plugin-principals.ts +137 -0
- package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +685 -0
- package/src/modules/connection-probe/connection-probe.contract.ts +44 -0
- package/src/modules/connection-probe/connection-probe.service.ts +734 -0
- package/src/modules/connection-probe/index.ts +2 -0
- package/src/modules/database/core-schema.ts +54 -0
- package/src/modules/diff/diff.routes.ts +9 -5
- package/src/modules/kb-fs/__tests__/mutex.test.ts +107 -0
- package/src/modules/kb-fs/mutex.ts +50 -5
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
- package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
- package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
- package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
- package/src/modules/kb-sync/index.ts +19 -0
- package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
- package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
- package/src/modules/kb-sync/kb-sync.service.ts +201 -0
- package/src/modules/kb-sync/sync-auth.ts +118 -0
- package/src/modules/kb-sync/sync-payload.ts +143 -0
- package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
- package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
- package/src/modules/marketplace/git-http.routes.ts +193 -0
- package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
- package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
- package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
- package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
- package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
- package/src/modules/marketplace/github-facade/index.ts +25 -0
- package/src/modules/marketplace/index.ts +3 -0
- package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
- package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
- package/src/modules/mcp/oauth/oauth-state.ts +15 -3
- package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
- package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
- package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
- package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
- package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
- package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
- package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
- package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
- package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
- package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
- package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
- package/src/modules/plugins/discovery/native.source.ts +122 -0
- package/src/modules/plugins/discovery/plugin-source.ts +99 -0
- package/src/modules/plugins/index.ts +10 -0
- package/src/modules/plugins/plugin-links.service.ts +363 -0
- package/src/modules/plugins/plugin-links.ts +184 -0
- package/src/modules/plugins/plugin-provision.service.ts +160 -54
- package/src/modules/plugins/plugin-rename.service.ts +298 -0
- package/src/modules/plugins/plugins.contract.ts +28 -1
- package/src/modules/plugins/plugins.routes.ts +128 -11
- package/src/modules/plugins/plugins.service.ts +106 -53
- package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +12 -0
- package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +10 -3
- package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +26 -0
- package/src/modules/secrets-vault/db-secrets-vault.service.ts +1 -1
- package/src/modules/secrets-vault/secrets-vault.routes.ts +35 -1
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
- package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
- package/src/modules/settings/deployment-settings.service.ts +95 -2
- package/src/modules/settings/setup.routes.ts +24 -0
- package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
- package/src/modules/skills/index.ts +1 -0
- package/src/modules/skills/pending-skills.service.ts +7 -5
- package/src/modules/skills/skill-access-requests.routes.ts +199 -0
- package/src/modules/skills/skills.contract.ts +32 -0
- package/src/modules/skills/skills.routes.ts +59 -3
- package/src/modules/skills/skills.service.ts +84 -22
- package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
- package/src/modules/tool-auth/external-api-key.service.ts +31 -13
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +35 -1
- package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.health-check.test.ts +104 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +56 -0
- package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -76
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +13 -4
- package/src/modules/tool-manuals/tool-manuals.contract.ts +89 -0
- package/src/modules/tool-manuals/tool-manuals.service.ts +210 -37
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +139 -0
- package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +337 -0
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
- package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +7 -1
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +97 -10
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
- package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
- package/src/modules/workflow/git/__tests__/git.service.history-guards.test.ts +86 -0
- package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
- package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
- package/src/modules/workflow/git/git.service.ts +354 -63
- package/src/modules/workflow/git/pull-request.service.ts +9 -0
- package/src/modules/workflow/workflow.routes.ts +104 -4
- package/src/modules/workflow/workflow.service.ts +505 -64
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
- package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
- package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
- package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
- package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
- package/src/modules/workspace/workspace.routes.ts +17 -21
- package/src/modules/workspace/workspace.service.ts +128 -19
- package/src/shared/__tests__/filename.test.ts +0 -0
- package/src/shared/__tests__/fs-walk.test.ts +22 -1
- package/src/shared/__tests__/kb-walk.test.ts +107 -0
- package/src/shared/__tests__/path-order.test.ts +24 -0
- package/src/shared/__tests__/token-crypto.test.ts +34 -0
- package/src/shared/__tests__/ttl-cache.test.ts +55 -0
- package/src/shared/domain-errors.ts +29 -0
- package/src/shared/fs-errors.ts +10 -0
- package/src/shared/fs-walk.ts +38 -23
- package/src/shared/kb-walk.ts +95 -0
- package/src/shared/path-order.ts +33 -0
- package/src/shared/token-crypto.ts +28 -10
- package/src/shared/ttl-cache.ts +33 -1
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ISecretsVaultService,
|
|
11
11
|
} from './secrets-vault.contract.js';
|
|
12
12
|
import type { IToolManualService, ToolManualSummary, ToolVariable } from '../tool-manuals/tool-manuals.contract.js';
|
|
13
|
+
import type { IConnectionProbeService } from '../connection-probe/connection-probe.contract.js';
|
|
13
14
|
import { utcpNamespacedKey } from '../../shared/utcp-namespace.js';
|
|
14
15
|
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
15
16
|
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
@@ -21,6 +22,11 @@ export interface SecretsVaultRoutesDeps {
|
|
|
21
22
|
toolManualService: IToolManualService;
|
|
22
23
|
/** Gates who may set a tool's ADMIN (shared) secrets — writers of the `.tool` file. */
|
|
23
24
|
accessControl: IAccessControl;
|
|
25
|
+
/**
|
|
26
|
+
* Verdicts of the last credential PROBE per tool — what makes the difference
|
|
27
|
+
* between "a key is stored" and "the key works" visible to the UI.
|
|
28
|
+
*/
|
|
29
|
+
connectionProbe: IConnectionProbeService;
|
|
24
30
|
/** HMAC secret for signing the OAuth `state` (reuse the connector state secret). */
|
|
25
31
|
stateSecret: string;
|
|
26
32
|
/** Public base URL of THIS backend — builds the OAuth redirect URI. */
|
|
@@ -69,7 +75,7 @@ export function isSafeReturnPath(returnTo: unknown): returnTo is string {
|
|
|
69
75
|
* `req.userId` — secrets are private per user. Mounted behind the JWT middleware.
|
|
70
76
|
*/
|
|
71
77
|
export function createSecretsVaultRoutes(deps: SecretsVaultRoutesDeps): express.Router {
|
|
72
|
-
const { secretsVault, toolManualService, accessControl } = deps;
|
|
78
|
+
const { secretsVault, toolManualService, accessControl, connectionProbe } = deps;
|
|
73
79
|
const router = express.Router();
|
|
74
80
|
// A FUNCTION, not a constant. Routers are constructed at boot, and on a
|
|
75
81
|
// deployment configured through the setup screen the branch model does not
|
|
@@ -361,6 +367,10 @@ export function createSecretsVaultRoutes(deps: SecretsVaultRoutesDeps): express.
|
|
|
361
367
|
value: body.value,
|
|
362
368
|
label: body.label,
|
|
363
369
|
});
|
|
370
|
+
// No probe here, and nothing to invalidate: a verdict is never stored, so
|
|
371
|
+
// saving a key cannot leave a stale one behind. The caller follows this
|
|
372
|
+
// with an explicit check, which keeps the save fast and gives the UI a
|
|
373
|
+
// "Testing…" state to show instead of a frozen dialog.
|
|
364
374
|
res.status(201).json({ secret });
|
|
365
375
|
} catch (err) {
|
|
366
376
|
mapError(err, res, 'set user var');
|
|
@@ -504,6 +514,30 @@ export function createSecretsVaultRoutes(deps: SecretsVaultRoutesDeps): express.
|
|
|
504
514
|
}
|
|
505
515
|
});
|
|
506
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Probe one tool's credential NOW and return the verdict.
|
|
519
|
+
*
|
|
520
|
+
* A POST because it has an effect on the world — it makes a real
|
|
521
|
+
* authenticated call to the provider — and because browsers and proxies are
|
|
522
|
+
* free to cache a GET, which for a freshness check would defeat the point.
|
|
523
|
+
*
|
|
524
|
+
* A probe outcome is never an HTTP error: a rejected credential is a
|
|
525
|
+
* successful check that found a problem, so it comes back 200 with
|
|
526
|
+
* `status: 'failed'`. Only a tool that can't be found or read 404s.
|
|
527
|
+
*/
|
|
528
|
+
router.post('/secrets/tools/:slug/check', async (req, res) => {
|
|
529
|
+
const userId = req.userId;
|
|
530
|
+
const email = req.userEmail;
|
|
531
|
+
if (!userId || !email) return void res.status(401).json({ error: 'Not authenticated' });
|
|
532
|
+
try {
|
|
533
|
+
const verdict = await connectionProbe.probe(userId, email, req.params.slug);
|
|
534
|
+
if (!verdict) return void res.status(404).json({ error: 'Tool not found' });
|
|
535
|
+
res.json({ verdict });
|
|
536
|
+
} catch (err) {
|
|
537
|
+
mapError(err, res, 'check tool connection');
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
507
541
|
return router;
|
|
508
542
|
}
|
|
509
543
|
|
|
@@ -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
|
+
});
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { eq, inArray } from 'drizzle-orm';
|
|
2
2
|
import type { Database } from '../database/connection.js';
|
|
3
3
|
import { deploymentSettings } from '../database/core-schema.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_KB_LAYOUT,
|
|
6
|
+
validateBranchModel,
|
|
7
|
+
validateKbLayout,
|
|
8
|
+
validateKbRootName,
|
|
9
|
+
} from '@bevel-software/platform-shared';
|
|
5
10
|
import { TokenCrypto } from '../../shared/token-crypto.js';
|
|
6
11
|
|
|
7
12
|
/**
|
|
@@ -85,6 +90,38 @@ export const CORE_SETTINGS: SettingDef[] = [
|
|
|
85
90
|
restartToApply: true,
|
|
86
91
|
},
|
|
87
92
|
|
|
93
|
+
/**
|
|
94
|
+
* The KB layout: the three root folders a deployment may rename so hexis can
|
|
95
|
+
* read a repository laid out by someone else (`skills/` and `plugins/` in
|
|
96
|
+
* lowercase, say). Restart-to-apply like the branch model — the names are
|
|
97
|
+
* applied once at boot through `configureKbLayout` and served to the browser
|
|
98
|
+
* once by `/api/config`. Each has a default, so an unset field means the
|
|
99
|
+
* default, not an unconfigured deployment. Checked as a trio in `save`: the
|
|
100
|
+
* three must differ, and one field alone cannot see the other two.
|
|
101
|
+
*/
|
|
102
|
+
{
|
|
103
|
+
key: 'knowledgeBaseDir',
|
|
104
|
+
envVar: 'KB_KNOWLEDGE_BASE_DIR',
|
|
105
|
+
section: 'knowledge-base',
|
|
106
|
+
validate: validateKbRootName,
|
|
107
|
+
restartToApply: true,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
key: 'skillsDir',
|
|
111
|
+
envVar: 'KB_SKILLS_DIR',
|
|
112
|
+
section: 'knowledge-base',
|
|
113
|
+
validate: validateKbRootName,
|
|
114
|
+
restartToApply: true,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
key: 'pluginsDir',
|
|
118
|
+
envVar: 'KB_PLUGINS_DIR',
|
|
119
|
+
section: 'knowledge-base',
|
|
120
|
+
validate: validateKbRootName,
|
|
121
|
+
restartToApply: true,
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
|
|
88
125
|
/**
|
|
89
126
|
* The branch model. Both are restart-to-apply and could not be otherwise:
|
|
90
127
|
* the backend hands `DEFAULT_BRANCH` to services at construction, and the
|
|
@@ -109,6 +146,25 @@ export const CORE_SETTINGS: SettingDef[] = [
|
|
|
109
146
|
restartToApply: true,
|
|
110
147
|
},
|
|
111
148
|
|
|
149
|
+
{
|
|
150
|
+
/**
|
|
151
|
+
* The credential a git host's webhook or a pipeline presents to
|
|
152
|
+
* `POST /api/sync` (see `modules/kb-sync/`). Deployment-level on purpose:
|
|
153
|
+
* a person's connection key would stop the pipeline the day they leave.
|
|
154
|
+
* Optional — without it the endpoint only admits an admin's own session.
|
|
155
|
+
* Read per request, so it applies without a restart.
|
|
156
|
+
*/
|
|
157
|
+
key: 'kbSyncSecret',
|
|
158
|
+
envVar: 'KB_SYNC_SECRET',
|
|
159
|
+
section: 'knowledge-base',
|
|
160
|
+
secret: true,
|
|
161
|
+
// The same floor `sync-auth.ts` enforces at request time (which is what
|
|
162
|
+
// covers a secret set through the environment); this one just says so
|
|
163
|
+
// before the save.
|
|
164
|
+
validate: (v) =>
|
|
165
|
+
v.trim().length >= 16 ? null : 'Use at least 16 characters — a random string is best.',
|
|
166
|
+
},
|
|
167
|
+
|
|
112
168
|
/**
|
|
113
169
|
* Single sign-on. Restart-to-apply because the provider is built once at boot
|
|
114
170
|
* and pushed into the auth plugin array the server mounts from.
|
|
@@ -266,6 +322,18 @@ export class DeploymentSettingsService {
|
|
|
266
322
|
return (this.stored.get(key) ?? '').trim();
|
|
267
323
|
}
|
|
268
324
|
|
|
325
|
+
/**
|
|
326
|
+
* The KB layout in effect: each root from the environment, else the stored
|
|
327
|
+
* row, else its default. The composition root applies this once at boot.
|
|
328
|
+
*/
|
|
329
|
+
resolveKbLayout(): { knowledgeBaseDir: string; skillsDir: string; pluginsDir: string } {
|
|
330
|
+
return {
|
|
331
|
+
knowledgeBaseDir: this.resolve('knowledgeBaseDir') || DEFAULT_KB_LAYOUT.knowledgeBaseDir,
|
|
332
|
+
skillsDir: this.resolve('skillsDir') || DEFAULT_KB_LAYOUT.skillsDir,
|
|
333
|
+
pluginsDir: this.resolve('pluginsDir') || DEFAULT_KB_LAYOUT.pluginsDir,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
269
337
|
/** Where {@link resolve} got its answer — what the setup screen labels the field with. */
|
|
270
338
|
sourceOf(key: string): SettingSource {
|
|
271
339
|
const def = this.defs.get(key);
|
|
@@ -356,11 +424,36 @@ export class DeploymentSettingsService {
|
|
|
356
424
|
if (problem) problems.protectedBranches = problem;
|
|
357
425
|
}
|
|
358
426
|
|
|
427
|
+
// The layout trio is the other cross-field rule: three names that must
|
|
428
|
+
// differ. Judged on the layout this save WOULD produce, with the default
|
|
429
|
+
// standing in for anything neither written nor stored.
|
|
430
|
+
const layoutKeys = ['knowledgeBaseDir', 'skillsDir', 'pluginsDir'] as const;
|
|
431
|
+
if (toWrite.some((w) => (layoutKeys as readonly string[]).includes(w.key))) {
|
|
432
|
+
const effective = (key: (typeof layoutKeys)[number]) =>
|
|
433
|
+
toWrite.find((w) => w.key === key)?.value || this.resolve(key) || DEFAULT_KB_LAYOUT[key];
|
|
434
|
+
const problem = validateKbLayout({
|
|
435
|
+
knowledgeBaseDir: effective('knowledgeBaseDir'),
|
|
436
|
+
skillsDir: effective('skillsDir'),
|
|
437
|
+
pluginsDir: effective('pluginsDir'),
|
|
438
|
+
});
|
|
439
|
+
// Against the field being written — the first one in the batch — since
|
|
440
|
+
// any of the three could be the one that collides.
|
|
441
|
+
if (problem) {
|
|
442
|
+
const first = toWrite.find((w) => (layoutKeys as readonly string[]).includes(w.key))!;
|
|
443
|
+
problems[first.key] = problem;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
359
447
|
if (Object.keys(problems).length > 0) throw new SettingsValidationError(problems);
|
|
360
448
|
|
|
361
449
|
let restartRequired = false;
|
|
362
450
|
for (const { key, value, def } of toWrite) {
|
|
363
|
-
|
|
451
|
+
// Compared against the EFFECTIVE value: a layout root that was unset
|
|
452
|
+
// was already running on its default, so saving that default changes
|
|
453
|
+
// nothing a restart would pick up.
|
|
454
|
+
const effective =
|
|
455
|
+
this.resolve(key) || (DEFAULT_KB_LAYOUT as Record<string, string | undefined>)[key] || '';
|
|
456
|
+
if (def.restartToApply && effective !== value) restartRequired = true;
|
|
364
457
|
const encrypted = def.secret === true;
|
|
365
458
|
const stored = encrypted ? this.crypto!.encrypt(value) : value;
|
|
366
459
|
await this.db
|
|
@@ -16,6 +16,18 @@ import '../auth/auth.middleware.js'; // Express Request augmentation
|
|
|
16
16
|
|
|
17
17
|
const execFileAsync = promisify(execFile);
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The slice of a sync record the status endpoint publishes. Declared here
|
|
21
|
+
* rather than imported from `kb-sync` so this module stays free of that one;
|
|
22
|
+
* the composition root hands in something that satisfies it.
|
|
23
|
+
*/
|
|
24
|
+
export interface LastSyncStatus {
|
|
25
|
+
at: number;
|
|
26
|
+
by: string;
|
|
27
|
+
status: 'synced' | 'partial';
|
|
28
|
+
results: Array<{ branch: string; outcome: string; error?: string }>;
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
/**
|
|
20
32
|
* First-run setup — the deployment's own configuration, for the things that
|
|
21
33
|
* used to be environment-only.
|
|
@@ -37,6 +49,17 @@ export function createSetupRoutes(
|
|
|
37
49
|
* would race.
|
|
38
50
|
*/
|
|
39
51
|
kbStartupRunner: { runAll(): Promise<void> },
|
|
52
|
+
/**
|
|
53
|
+
* The remote-sync facts the Deployment page shows beside the sync secret:
|
|
54
|
+
* the address a webhook or pipeline must call, and what the last call did.
|
|
55
|
+
* Optional so a minimal mount (tests, a distribution without the module)
|
|
56
|
+
* simply omits them from the status.
|
|
57
|
+
*/
|
|
58
|
+
sync?: {
|
|
59
|
+
/** `<PUBLIC_BACKEND_URL>/api/sync` — the reader appends `/<branch>`. */
|
|
60
|
+
url: string;
|
|
61
|
+
lastSync(): LastSyncStatus | null;
|
|
62
|
+
},
|
|
40
63
|
): express.Router {
|
|
41
64
|
const router = express.Router();
|
|
42
65
|
|
|
@@ -101,6 +124,7 @@ export function createSetupRoutes(
|
|
|
101
124
|
isAdmin: true,
|
|
102
125
|
settings: settings.describe(),
|
|
103
126
|
...(kbInitFailed ? { kbInitError } : {}),
|
|
127
|
+
...(sync ? { sync: { url: sync.url, last: sync.lastSync() } } : {}),
|
|
104
128
|
});
|
|
105
129
|
});
|
|
106
130
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { Server } from 'node:http';
|
|
2
|
+
import type { AddressInfo } from 'node:net';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
5
|
+
import { DEFAULT_BRANCH, joinBranchFor } from '@bevel-software/platform-shared';
|
|
6
|
+
import type { IWorkflowService } from '@bevel-software/platform-shared';
|
|
7
|
+
|
|
8
|
+
import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
|
|
9
|
+
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
10
|
+
import type { IAccessControl } from '../../access/access-control.interface.js';
|
|
11
|
+
import type { JoinRequestsService } from '../../plugins/join-requests.service.js';
|
|
12
|
+
import type { ISkillService } from '../skills.contract.js';
|
|
13
|
+
import { createSkillAccessRequestRoutes } from '../skill-access-requests.routes.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The branch probe behind an access request: the listing is used to PROVE a
|
|
17
|
+
* branch absent before one is cut, so it must be the strict kind — a stale
|
|
18
|
+
* list (a fetch that failed) proves nothing, and the route must stop rather
|
|
19
|
+
* than write a proposal onto a branch it never confirmed.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const KB = 'knowledge-base';
|
|
23
|
+
const wsId = workspaceIdForBranch(DEFAULT_BRANCH);
|
|
24
|
+
const MIA = { id: 'u-mia', email: 'mia@x.io', name: 'Mia' };
|
|
25
|
+
const FOLDER = 'Skills/Eng/deploy';
|
|
26
|
+
|
|
27
|
+
async function makeHarness(listBranches: IWorkflowService['listBranches']) {
|
|
28
|
+
const workflow = {
|
|
29
|
+
listChangeRequestsAuthoredBy: vi.fn(async () => []),
|
|
30
|
+
listBranches: vi.fn(listBranches),
|
|
31
|
+
createBranch: vi.fn(async () => ({ name: 'x', isDefault: false, isProtected: false })),
|
|
32
|
+
commitChanges: vi.fn(async () => null),
|
|
33
|
+
openChangeRequest: vi.fn(async () => ({ number: 42 })),
|
|
34
|
+
} as unknown as IWorkflowService;
|
|
35
|
+
const workspaceService = {
|
|
36
|
+
getOrCreateForBranch: vi.fn(async (branch: string) => ({ id: workspaceIdForBranch(branch) })),
|
|
37
|
+
readFile: vi.fn(async () => '---\n---\nwrite:\n - Eve <eve@x.io>\n'),
|
|
38
|
+
writeFile: vi.fn(async () => undefined),
|
|
39
|
+
} as unknown as WorkspaceService;
|
|
40
|
+
const accessControl = { canWrite: vi.fn(async () => false) } as unknown as IAccessControl;
|
|
41
|
+
const skillService = {
|
|
42
|
+
listSkills: vi.fn(async () => [{ name: 'deploy', description: '', path: FOLDER }]),
|
|
43
|
+
} as unknown as ISkillService;
|
|
44
|
+
|
|
45
|
+
const app = express();
|
|
46
|
+
app.use(express.json());
|
|
47
|
+
app.use(
|
|
48
|
+
'/api',
|
|
49
|
+
createSkillAccessRequestRoutes({
|
|
50
|
+
skillService,
|
|
51
|
+
accessControl,
|
|
52
|
+
workflow,
|
|
53
|
+
workspaceService,
|
|
54
|
+
joinRequests: { list: vi.fn(), reconcile: vi.fn() } as unknown as JoinRequestsService,
|
|
55
|
+
kbDirName: KB,
|
|
56
|
+
resolveUser: async () => MIA,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
const server = await new Promise<Server>((resolve) => {
|
|
60
|
+
const s = app.listen(0, () => resolve(s));
|
|
61
|
+
});
|
|
62
|
+
const { port } = server.address() as AddressInfo;
|
|
63
|
+
return { server, url: `http://127.0.0.1:${port}/api/skills/deploy/access-request`, workflow, workspaceService };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('POST /skills/:name/access-request — the branch probe', () => {
|
|
67
|
+
let server: Server | null = null;
|
|
68
|
+
afterEach(async () => {
|
|
69
|
+
// Released here, not after the assertions: a failing assertion must not
|
|
70
|
+
// leave the console muted for every test that follows.
|
|
71
|
+
vi.restoreAllMocks();
|
|
72
|
+
if (server) await new Promise<void>((r) => server!.close(() => r()));
|
|
73
|
+
server = null;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('proves absence with a STRICT fresh listing, then cuts the branch and opens the request', async () => {
|
|
77
|
+
const h = await makeHarness(async () => []);
|
|
78
|
+
server = h.server;
|
|
79
|
+
const res = await fetch(h.url, { method: 'POST' });
|
|
80
|
+
expect(res.status).toBe(200);
|
|
81
|
+
expect(await res.json()).toEqual({ ok: true, number: 42 });
|
|
82
|
+
expect(h.workflow.listBranches).toHaveBeenCalledWith(wsId, { freshFetch: true, strictFetch: true });
|
|
83
|
+
expect(h.workflow.createBranch).toHaveBeenCalledWith(wsId, joinBranchFor(MIA.email, FOLDER), DEFAULT_BRANCH);
|
|
84
|
+
// The whole chain the probe exists to enable: the grant is written AND committed.
|
|
85
|
+
expect(h.workspaceService.writeFile).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(h.workflow.commitChanges).toHaveBeenCalledTimes(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('a create that loses the race proceeds once the fresh re-probe shows the branch', async () => {
|
|
90
|
+
const branch = joinBranchFor(MIA.email, FOLDER);
|
|
91
|
+
// Absent on the first probe, present on the second: someone else cut it in between.
|
|
92
|
+
let probes = 0;
|
|
93
|
+
const h = await makeHarness(async () => (probes++ === 0 ? [] : [{ name: branch, isDefault: false, isProtected: false }]));
|
|
94
|
+
(h.workflow.createBranch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('already exists'));
|
|
95
|
+
server = h.server;
|
|
96
|
+
const res = await fetch(h.url, { method: 'POST' });
|
|
97
|
+
expect(res.status).toBe(200);
|
|
98
|
+
expect(await res.json()).toEqual({ ok: true, number: 42 });
|
|
99
|
+
expect(h.workflow.listBranches).toHaveBeenCalledTimes(2);
|
|
100
|
+
expect(h.workspaceService.writeFile).toHaveBeenCalledTimes(1);
|
|
101
|
+
expect(h.workflow.commitChanges).toHaveBeenCalledTimes(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('a create that fails with no branch to show for it stops the request — nothing is written on a branch that was not made', async () => {
|
|
105
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
106
|
+
const h = await makeHarness(async () => []);
|
|
107
|
+
(h.workflow.createBranch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('origin refused'));
|
|
108
|
+
server = h.server;
|
|
109
|
+
const res = await fetch(h.url, { method: 'POST' });
|
|
110
|
+
expect(res.status).toBe(500);
|
|
111
|
+
expect(h.workflow.listBranches).toHaveBeenCalledTimes(2);
|
|
112
|
+
expect(h.workspaceService.writeFile).not.toHaveBeenCalled();
|
|
113
|
+
expect(h.workflow.openChangeRequest).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('a listing that could not prove anything stops the request before a branch or a byte is written', async () => {
|
|
117
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
118
|
+
const h = await makeHarness(async () => {
|
|
119
|
+
throw new Error('the fetch from origin failed, so the branch list cannot prove absence');
|
|
120
|
+
});
|
|
121
|
+
server = h.server;
|
|
122
|
+
const res = await fetch(h.url, { method: 'POST' });
|
|
123
|
+
expect(res.status).toBe(500);
|
|
124
|
+
expect(h.workflow.createBranch).not.toHaveBeenCalled();
|
|
125
|
+
expect(h.workspaceService.writeFile).not.toHaveBeenCalled();
|
|
126
|
+
expect(h.workflow.openChangeRequest).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -67,6 +67,62 @@ describe('SkillService', () => {
|
|
|
67
67
|
});
|
|
68
68
|
afterEach(() => rm(root, { recursive: true, force: true }));
|
|
69
69
|
|
|
70
|
+
test('lists shared skills under the Skills root alongside plugin skills', async () => {
|
|
71
|
+
const shared = join(root, wsId, KB_DIR, 'Skills', 'Engineering', 'deploy');
|
|
72
|
+
await mkdir(shared, { recursive: true });
|
|
73
|
+
await writeFile(join(shared, 'SKILL.md'), '---\ndescription: Ship it.\n---\n\n# Deploy\n');
|
|
74
|
+
// A skill directly under the root, no scope folder — also a skill.
|
|
75
|
+
const bare = join(root, wsId, KB_DIR, 'Skills', 'triage');
|
|
76
|
+
await mkdir(bare, { recursive: true });
|
|
77
|
+
await writeFile(join(bare, 'SKILL.md'), '---\ndescription: Triage.\n---\n');
|
|
78
|
+
|
|
79
|
+
const list = await svc().listSkills();
|
|
80
|
+
expect(list.find((s) => s.name === 'deploy')?.path).toBe('Skills/Engineering/deploy');
|
|
81
|
+
expect(list.find((s) => s.name === 'triage')?.path).toBe('Skills/triage');
|
|
82
|
+
// The plugin tree is still scanned: the union is the catalog.
|
|
83
|
+
expect(list.find((s) => s.name === 'rfi')?.path).toBe('Plugins/rfi');
|
|
84
|
+
|
|
85
|
+
const res = await svc().getSkill('u@x.io', 'deploy');
|
|
86
|
+
expect(res.ok && res.kind === 'skill' && res.skill.body).toContain('# Deploy');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('honours a .bevelignore inside a root, so a build output never shadows the source', async () => {
|
|
90
|
+
// A repository that commits a compiled copy of every skill beside the
|
|
91
|
+
// source: without the ignore, `dist/…/rfi` would collide with `Plugins/rfi`.
|
|
92
|
+
const dist = join(root, wsId, KB_DIR, 'Skills', 'dist', 'rfi');
|
|
93
|
+
await mkdir(dist, { recursive: true });
|
|
94
|
+
await writeFile(join(dist, 'SKILL.md'), '---\ndescription: compiled copy\n---\n');
|
|
95
|
+
await writeFile(join(root, wsId, KB_DIR, 'Skills', '.bevelignore'), 'dist/\n');
|
|
96
|
+
|
|
97
|
+
const list = await svc().listSkills();
|
|
98
|
+
expect(list.filter((s) => s.name === 'rfi').map((s) => s.path)).toEqual(['Plugins/rfi']);
|
|
99
|
+
expect(list.some((s) => s.path.includes('/dist/'))).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('ignores the repo-root .bevelignore: hiding a root from the file tree must not empty the catalog', async () => {
|
|
103
|
+
// The seeded template hides `Plugins/` (and `Skills/`) from the Knowledge
|
|
104
|
+
// tree with exactly this file. The catalog is what those roots exist for.
|
|
105
|
+
await writeFile(join(root, wsId, KB_DIR, '.bevelignore'), 'Plugins/\nSkills/\n');
|
|
106
|
+
const shared = join(root, wsId, KB_DIR, 'Skills', 'deploy');
|
|
107
|
+
await mkdir(shared, { recursive: true });
|
|
108
|
+
await writeFile(join(shared, 'SKILL.md'), '---\ndescription: Ship it.\n---\n');
|
|
109
|
+
|
|
110
|
+
const list = await svc().listSkills();
|
|
111
|
+
expect(list.map((s) => s.name).sort()).toEqual(['coding-guidelines', 'deploy', 'rfi']);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a nested .bevelignore applies beneath the folder that carries it', async () => {
|
|
115
|
+
const scope = join(root, wsId, KB_DIR, 'Skills', 'Sales');
|
|
116
|
+
await mkdir(join(scope, 'drafts', 'pitch'), { recursive: true });
|
|
117
|
+
await mkdir(join(scope, 'pitch'), { recursive: true });
|
|
118
|
+
await writeFile(join(scope, 'drafts', 'pitch', 'SKILL.md'), '---\ndescription: draft\n---\n');
|
|
119
|
+
await writeFile(join(scope, 'pitch', 'SKILL.md'), '---\ndescription: released\n---\n');
|
|
120
|
+
await writeFile(join(scope, '.bevelignore'), 'drafts/\n');
|
|
121
|
+
|
|
122
|
+
const list = await svc().listSkills();
|
|
123
|
+
expect(list.filter((s) => s.name === 'pitch').map((s) => s.path)).toEqual(['Skills/Sales/pitch']);
|
|
124
|
+
});
|
|
125
|
+
|
|
70
126
|
test('lists skills with a block-scalar description parsed correctly', async () => {
|
|
71
127
|
const skills = await svc().listSkills();
|
|
72
128
|
expect(skills).toHaveLength(2);
|
|
@@ -145,6 +201,35 @@ describe('SkillService', () => {
|
|
|
145
201
|
expect(trav).toEqual({ ok: false, error: 'invalid_file' });
|
|
146
202
|
});
|
|
147
203
|
|
|
204
|
+
test('an ignored SKILL.md suppresses the skill and NEVER promotes its assets to skills', async () => {
|
|
205
|
+
const rfi = join(root, wsId, KB_DIR, 'Plugins', 'rfi');
|
|
206
|
+
await writeFile(join(rfi, '.bevelignore'), 'SKILL.md\n');
|
|
207
|
+
await mkdir(join(rfi, 'examples'), { recursive: true });
|
|
208
|
+
await writeFile(join(rfi, 'examples', 'SKILL.md'), '---\ndescription: An example, not a skill.\n---\n');
|
|
209
|
+
const names = (await svc().listSkills()).map((s) => s.name);
|
|
210
|
+
expect(names).not.toContain('rfi');
|
|
211
|
+
expect(names).not.toContain('examples');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('ignore rules reach a skill\'s bundled files: hidden from the listing and refused when asked for', async () => {
|
|
215
|
+
const rfi = join(root, wsId, KB_DIR, 'Plugins', 'rfi');
|
|
216
|
+
await writeFile(join(rfi, '.bevelignore'), 'scripts/\n');
|
|
217
|
+
const res = await svc().getSkill('user@x.eu', 'rfi');
|
|
218
|
+
expect(res.ok && res.kind === 'skill' ? res.skill.files : null).toEqual([]);
|
|
219
|
+
expect(await svc().getSkill('user@x.eu', 'rfi', 'scripts/build_xlsx.py')).toEqual({
|
|
220
|
+
ok: false,
|
|
221
|
+
error: 'not_found',
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('a skill that exists inline AND under the shared root resolves to the shared copy', async () => {
|
|
226
|
+
const shared = join(root, wsId, KB_DIR, 'Skills', 'Ops', 'rfi');
|
|
227
|
+
await mkdir(shared, { recursive: true });
|
|
228
|
+
await writeFile(join(shared, 'SKILL.md'), '---\ndescription: The canonical one.\n---\n');
|
|
229
|
+
const rfi = (await svc().listSkills()).find((s) => s.name === 'rfi');
|
|
230
|
+
expect(rfi?.path).toBe('Skills/Ops/rfi');
|
|
231
|
+
});
|
|
232
|
+
|
|
148
233
|
test('listSkills filters by canRead for a given user', async () => {
|
|
149
234
|
const denyRfi: IAccessControl = {
|
|
150
235
|
canRead: async () => false,
|
|
@@ -2,6 +2,7 @@ export { SkillService } from './skills.service.js';
|
|
|
2
2
|
export { PendingSkillsService } from './pending-skills.service.js';
|
|
3
3
|
export { registerSkillsTools } from './skills.tools.js';
|
|
4
4
|
export { createSkillsRoutes } from './skills.routes.js';
|
|
5
|
+
export { createSkillAccessRequestRoutes } from './skill-access-requests.routes.js';
|
|
5
6
|
export type {
|
|
6
7
|
ISkillService,
|
|
7
8
|
IPendingSkillService,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_BRANCH,
|
|
3
3
|
PLUGINS_DIR,
|
|
4
|
+
SKILLS_DIR,
|
|
4
5
|
type ChangeRequest,
|
|
5
6
|
type IWorkflowService,
|
|
6
7
|
} from '@bevel-software/platform-shared';
|
|
@@ -118,6 +119,8 @@ export class PendingSkillsService implements IPendingSkillService {
|
|
|
118
119
|
name: isSafeSkillName(declared) ? declared : folderName,
|
|
119
120
|
description: fm.description,
|
|
120
121
|
version: fm.version,
|
|
122
|
+
owner: fm.owner,
|
|
123
|
+
lifecycle: fm.lifecycle,
|
|
121
124
|
path: folder,
|
|
122
125
|
changeRequestNumber: cr.number,
|
|
123
126
|
branch: cr.branch,
|
|
@@ -156,11 +159,10 @@ export class PendingSkillsService implements IPendingSkillService {
|
|
|
156
159
|
*/
|
|
157
160
|
function isSkillDoc(repoRelPath: string): boolean {
|
|
158
161
|
const segments = repoRelPath.split('/');
|
|
159
|
-
return
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
);
|
|
162
|
+
if (segments[segments.length - 1] !== SKILL_DOC) return false;
|
|
163
|
+
// Under `Skills/` a skill may sit directly below the root: `Skills/<skill>/SKILL.md`.
|
|
164
|
+
if (segments[0] === SKILLS_DIR) return segments.length >= 3;
|
|
165
|
+
return segments.length >= 4 && segments[0] === PLUGINS_DIR;
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
/** The skill folder holding a SKILL.md. */
|