@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
|
@@ -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. */
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import '../auth/auth.middleware.js'; // Express Request.userId / userEmail augmentation
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_BRANCH,
|
|
5
|
+
joinBranchFor,
|
|
6
|
+
type AuthUser,
|
|
7
|
+
type IWorkflowService,
|
|
8
|
+
} from '@bevel-software/platform-shared';
|
|
9
|
+
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
10
|
+
import { spliceGrant } from '../access-model/access-splice.js';
|
|
11
|
+
import { WorkflowDomainError } from '../../shared/domain-errors.js';
|
|
12
|
+
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
13
|
+
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
14
|
+
import type { JoinRequestsService } from '../plugins/join-requests.service.js';
|
|
15
|
+
import type { ISkillService } from './skills.contract.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Asking for WRITE on a shared skill — the same machinery as asking to join a
|
|
19
|
+
* plugin, pointed at a skill folder instead of a plugin folder.
|
|
20
|
+
*
|
|
21
|
+
* POST /api/skills/:name/access-request → { ok, number }
|
|
22
|
+
* GET /api/skills/:name/access-requests → { requests } (skill editors)
|
|
23
|
+
* POST /api/skills/:name/access-requests/:n/reconcile → { closed }
|
|
24
|
+
*
|
|
25
|
+
* Why it exists: linking a skill into a plugin needs write on the skill's
|
|
26
|
+
* rules (the link grants the plugin's principal there), and a plugin manager
|
|
27
|
+
* often has no say over a skill some other scope owns. The request is a plain
|
|
28
|
+
* change request whose branch splices `write: <requester>` into the skill
|
|
29
|
+
* folder's `access.md`; the skill's editors see its proposals and accept
|
|
30
|
+
* through the ordinary grant path, and it retires itself once the default
|
|
31
|
+
* branch carries the grant (`JoinRequestsService`, keyed on the skill FOLDER
|
|
32
|
+
* path rather than a plugin name — the branch-name convention takes any
|
|
33
|
+
* exact string).
|
|
34
|
+
*
|
|
35
|
+
* Fail-closed like the skill catalog: a skill the caller cannot read answers
|
|
36
|
+
* as unknown, and the editors' listing answers `[]` to anyone else.
|
|
37
|
+
*/
|
|
38
|
+
export function createSkillAccessRequestRoutes(deps: {
|
|
39
|
+
skillService: ISkillService;
|
|
40
|
+
accessControl: IAccessControl;
|
|
41
|
+
workflow: IWorkflowService;
|
|
42
|
+
workspaceService: WorkspaceService;
|
|
43
|
+
joinRequests: JoinRequestsService;
|
|
44
|
+
kbDirName: string;
|
|
45
|
+
resolveUser: (req: express.Request) => Promise<AuthUser | null>;
|
|
46
|
+
}): express.Router {
|
|
47
|
+
const { skillService, accessControl, workflow, workspaceService, joinRequests, kbDirName, resolveUser } = deps;
|
|
48
|
+
const router = express.Router();
|
|
49
|
+
const wsId = () => workspaceIdForBranch(DEFAULT_BRANCH);
|
|
50
|
+
const rulesOf = (folder: string) => `${folder}/access.md`;
|
|
51
|
+
|
|
52
|
+
/** The skill the caller may read, by name — or null after answering 404. */
|
|
53
|
+
async function readableSkill(
|
|
54
|
+
req: express.Request,
|
|
55
|
+
res: express.Response,
|
|
56
|
+
): Promise<{ user: AuthUser; folder: string; name: string } | null> {
|
|
57
|
+
const user = await resolveUser(req);
|
|
58
|
+
if (!user) {
|
|
59
|
+
res.status(401).json({ error: 'Unauthenticated' });
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const name = String(req.params.name);
|
|
63
|
+
const skill = (await skillService.listSkills(user.email)).find((s) => s.name === name);
|
|
64
|
+
if (!skill) {
|
|
65
|
+
res.status(404).json({ error: 'Unknown skill', kind: 'unknown-skill' });
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return { user, folder: skill.path, name };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
router.post('/skills/:name/access-request', async (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
const ctx = await readableSkill(req, res);
|
|
74
|
+
if (!ctx) return;
|
|
75
|
+
const { user, folder, name } = ctx;
|
|
76
|
+
if (await accessControl.canWrite(wsId(), user.email, rulesOf(folder))) {
|
|
77
|
+
res.status(409).json({ error: 'You can already edit this skill', kind: 'already-writable' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const branch = joinBranchFor(user.email, folder);
|
|
81
|
+
// FRESH: a repeated click must find the request the previous one opened,
|
|
82
|
+
// and the cached listing can trail it — a miss here would send the
|
|
83
|
+
// retry into a duplicate refusal.
|
|
84
|
+
const existing = (await workflow.listChangeRequestsAuthoredBy(user.email, { fresh: true })).find(
|
|
85
|
+
(cr) => cr.state === 'open' && cr.branch === branch,
|
|
86
|
+
);
|
|
87
|
+
if (existing) {
|
|
88
|
+
res.json({ ok: true, number: existing.number });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// The branch may already exist (a request listed as closed, a retry
|
|
92
|
+
// after a failed open). Existence is PROBED, before and — if creation
|
|
93
|
+
// fails — after: the race shows up as "already exists" locally or as a
|
|
94
|
+
// rejected push when origin got there first, and a message is not a
|
|
95
|
+
// contract. A branch that is there is proceeded against; anything else
|
|
96
|
+
// is a real failure, and a proposal on a branch that was not made would
|
|
97
|
+
// be worse than the error. STRICT: the list proves absence, and a
|
|
98
|
+
// listing that could not fetch proves nothing — it throws instead of
|
|
99
|
+
// serving refs from before the branch was made (or deleted).
|
|
100
|
+
const branchExists = async () =>
|
|
101
|
+
(await workflow.listBranches(wsId(), { freshFetch: true, strictFetch: true })).some(
|
|
102
|
+
(b) => b.name === branch,
|
|
103
|
+
);
|
|
104
|
+
if (!(await branchExists())) {
|
|
105
|
+
try {
|
|
106
|
+
await workflow.createBranch(wsId(), branch, DEFAULT_BRANCH);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (!(await branchExists())) throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const ws = await workspaceService.getOrCreateForBranch(branch);
|
|
112
|
+
const accessPath = `${kbDirName}/${rulesOf(folder)}`;
|
|
113
|
+
// Only a PROVEN absence reads as "no rules yet": any other failure must
|
|
114
|
+
// not be turned into an empty file that a splice then overwrites.
|
|
115
|
+
const current = await workspaceService.readFile(ws.id, accessPath).catch((err: unknown) => {
|
|
116
|
+
const code = (err as { code?: unknown } | null)?.code;
|
|
117
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR') throw err;
|
|
118
|
+
return '';
|
|
119
|
+
});
|
|
120
|
+
const spliced = spliceGrant(
|
|
121
|
+
current,
|
|
122
|
+
'write',
|
|
123
|
+
{ kind: 'user', email: user.email, displayName: user.name },
|
|
124
|
+
{ target: 'folder' },
|
|
125
|
+
);
|
|
126
|
+
if (spliced.changed) {
|
|
127
|
+
await workspaceService.writeFile(ws.id, accessPath, spliced.text);
|
|
128
|
+
await workflow.commitChanges(ws.id, user, `Request write access to ${name}`);
|
|
129
|
+
}
|
|
130
|
+
const detail = await workflow.openChangeRequest(ws.id, user, {
|
|
131
|
+
sourceBranch: branch,
|
|
132
|
+
targetBranch: DEFAULT_BRANCH,
|
|
133
|
+
title: `Access request: ${name}`,
|
|
134
|
+
description:
|
|
135
|
+
`${user.name} asked to edit the skill ${name}. An editor of the skill accepts by ` +
|
|
136
|
+
`granting the access this branch proposes; the request closes itself once ` +
|
|
137
|
+
`every proposal has landed.`,
|
|
138
|
+
});
|
|
139
|
+
res.json({ ok: true, number: detail.number });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (err instanceof WorkflowDomainError) {
|
|
142
|
+
res.status(err.status).json({ error: err.message, ...(err.payload ?? {}) });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
console.error('[skills] failed to open an access request:', err);
|
|
146
|
+
res.status(500).json({ error: 'Failed to request access' });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
/** The caller as an EDITOR of the skill's rules, or null after answering. */
|
|
151
|
+
async function requireEditor(
|
|
152
|
+
req: express.Request,
|
|
153
|
+
res: express.Response,
|
|
154
|
+
onDenied: () => void,
|
|
155
|
+
): Promise<{ user: AuthUser; folder: string } | null> {
|
|
156
|
+
const ctx = await readableSkill(req, res);
|
|
157
|
+
if (!ctx) return null;
|
|
158
|
+
if (!(await accessControl.canWrite(wsId(), ctx.user.email, rulesOf(ctx.folder)))) {
|
|
159
|
+
onDenied();
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return ctx;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
router.get('/skills/:name/access-requests', async (req, res) => {
|
|
166
|
+
try {
|
|
167
|
+
const ctx = await requireEditor(req, res, () => res.json({ requests: [] }));
|
|
168
|
+
if (!ctx) return;
|
|
169
|
+
const crs = await workflow.listChangeRequests();
|
|
170
|
+
res.json({ requests: await joinRequests.list(ctx.folder, ctx.folder, crs, ctx.user) });
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error('[skills] failed to list access requests:', err);
|
|
173
|
+
res.status(500).json({ error: 'Failed to list access requests' });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
router.post('/skills/:name/access-requests/:number/reconcile', async (req, res) => {
|
|
178
|
+
try {
|
|
179
|
+
const ctx = await requireEditor(req, res, () => res.status(404).json({ error: 'Not found' }));
|
|
180
|
+
if (!ctx) return;
|
|
181
|
+
const crNumber = Number(req.params.number);
|
|
182
|
+
if (!Number.isSafeInteger(crNumber) || crNumber <= 0) {
|
|
183
|
+
res.status(400).json({ error: 'Invalid change request number' });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const cr = await workflow.getChangeRequest(crNumber);
|
|
187
|
+
if (!cr) {
|
|
188
|
+
res.status(404).json({ error: 'Not found' });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
res.json({ closed: await joinRequests.reconcile(ctx.folder, ctx.folder, cr, ctx.user) });
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error('[skills] failed to reconcile an access request:', err);
|
|
194
|
+
res.status(500).json({ error: 'Failed to reconcile the request' });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return router;
|
|
199
|
+
}
|