@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
|
@@ -7,13 +7,45 @@
|
|
|
7
7
|
* bundled file's content (level 3).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* One plugin a skill belongs to — by sitting INSIDE the plugin folder
|
|
12
|
+
* (`linked: false`) or by being LINKED from the plugin's manifest
|
|
13
|
+
* (`linked: true`). For a link, `granted` is the link's HEALTH: whether the
|
|
14
|
+
* plugin's members can read the skill through it. For a link this platform
|
|
15
|
+
* manages that is whether the skill's own access rules name the plugin's
|
|
16
|
+
* `plugin/<Name>/read` principal — the link service writes that grant with
|
|
17
|
+
* the link, so `false` means a hand edit took it away. A link read from an
|
|
18
|
+
* external plugin format needs no grant (the skill's own scope decides who
|
|
19
|
+
* reads it) and is always `true`; the flag never proves a principal exists.
|
|
20
|
+
*/
|
|
21
|
+
export interface PluginMembership {
|
|
22
|
+
/** The plugin folder name. */
|
|
23
|
+
name: string;
|
|
24
|
+
linked: boolean;
|
|
25
|
+
granted: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
export interface SkillSummary {
|
|
11
29
|
/** Canonical id = the skill's folder name (e.g. `rfi`). */
|
|
12
30
|
name: string;
|
|
13
31
|
description: string;
|
|
14
32
|
version?: string;
|
|
33
|
+
/** The governance owner from `metadata.owner`, when the skill declares one. */
|
|
34
|
+
owner?: string;
|
|
35
|
+
/**
|
|
36
|
+
* The governance lifecycle from `metadata.lifecycle`, lowercased — `active`
|
|
37
|
+
* (or absent), `deprecated` (still served, flagged), `retired` (served to the
|
|
38
|
+
* catalog for its owners, never compiled into a distribution).
|
|
39
|
+
*/
|
|
40
|
+
lifecycle?: string;
|
|
15
41
|
/** Repo-root-relative skill folder, e.g. `Plugins/Everyone/rfi`. */
|
|
16
42
|
path: string;
|
|
43
|
+
/**
|
|
44
|
+
* The plugins this skill belongs to, decorated onto the catalog by the
|
|
45
|
+
* browser route (the catalog itself is plugin-unaware). Absent on the agent
|
|
46
|
+
* surfaces, which load skills by name and never by plugin.
|
|
47
|
+
*/
|
|
48
|
+
plugins?: PluginMembership[];
|
|
17
49
|
}
|
|
18
50
|
|
|
19
51
|
export interface Skill extends SkillSummary {
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
+
import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
|
|
2
3
|
import '../auth/auth.middleware.js'; // Express Request.userId / userEmail augmentation
|
|
3
|
-
import
|
|
4
|
+
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
5
|
+
import type { IPendingSkillService, ISkillService, PluginMembership } from './skills.contract.js';
|
|
6
|
+
|
|
7
|
+
/** The slice of the plugin link index this surface reads — see `PluginLinkIndex`. */
|
|
8
|
+
export interface SkillMembershipSource {
|
|
9
|
+
membership(): Promise<{
|
|
10
|
+
bySkill: Map<string, PluginMembership[]>;
|
|
11
|
+
byPlugin: Map<string, { folder: string }>;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The one access verdict this surface needs: may the caller read a path on the default branch? */
|
|
16
|
+
export interface SkillMembershipGate {
|
|
17
|
+
canReadBatch(workspaceId: string, userEmail: string, paths: string[]): Promise<Map<string, boolean>>;
|
|
18
|
+
}
|
|
4
19
|
|
|
5
20
|
/**
|
|
6
21
|
* Browser-facing (JWT) skill routes for the `/`-command menu, mounted behind
|
|
@@ -8,14 +23,27 @@ import type { IPendingSkillService, ISkillService } from './skills.contract.js';
|
|
|
8
23
|
* so discovery, default-branch pinning, and per-user `canRead` filtering live
|
|
9
24
|
* ONCE — the frontend never re-implements skill discovery or bypasses access.
|
|
10
25
|
*
|
|
11
|
-
* GET /api/skills → { skills: SkillSummary[] } (filtered to this user
|
|
26
|
+
* GET /api/skills → { skills: SkillSummary[] } (filtered to this user,
|
|
27
|
+
* each with its `plugins`)
|
|
12
28
|
* GET /api/skills/pending → { skills: PendingSkill[] } (proposed, not released)
|
|
13
29
|
* GET /api/skills/:name → GetSkillResult (body + files)
|
|
14
30
|
* GET /api/skills/:name?file=… → GetSkillResult (a bundled file's content)
|
|
31
|
+
*
|
|
32
|
+
* `plugins` is decorated HERE, not in the catalog: the catalog is what agents
|
|
33
|
+
* load by name and it stays plugin-unaware; the browser is the surface that
|
|
34
|
+
* groups skills by plugin. Optional dependency, so a host without the link
|
|
35
|
+
* index gets undecorated summaries rather than a 500.
|
|
15
36
|
*/
|
|
16
37
|
export function createSkillsRoutes(
|
|
17
38
|
skillService: ISkillService,
|
|
18
39
|
pendingSkills?: IPendingSkillService,
|
|
40
|
+
links?: SkillMembershipSource,
|
|
41
|
+
/**
|
|
42
|
+
* With `links`: the memberships a caller sees name only plugins they may
|
|
43
|
+
* DISCOVER (read the plugin's access.md — the plugin index's own listing
|
|
44
|
+
* verdict), so this surface never reveals a plugin `/api/plugins` omits.
|
|
45
|
+
*/
|
|
46
|
+
gate?: SkillMembershipGate,
|
|
19
47
|
): express.Router {
|
|
20
48
|
const router = express.Router();
|
|
21
49
|
|
|
@@ -25,7 +53,35 @@ export function createSkillsRoutes(
|
|
|
25
53
|
res.status(401).json({ error: 'Unauthenticated' });
|
|
26
54
|
return;
|
|
27
55
|
}
|
|
28
|
-
|
|
56
|
+
const skills = await skillService.listSkills(email);
|
|
57
|
+
if (!links) {
|
|
58
|
+
res.json({ skills });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let bySkill: Map<string, PluginMembership[]>;
|
|
62
|
+
// Fail closed: memberships without a gate to judge them by name nothing.
|
|
63
|
+
let discoverable: (plugin: string) => boolean = () => false;
|
|
64
|
+
try {
|
|
65
|
+
const membership = await links.membership();
|
|
66
|
+
bySkill = membership.bySkill;
|
|
67
|
+
if (gate) {
|
|
68
|
+
const folders = [...membership.byPlugin.entries()];
|
|
69
|
+
const verdicts = folders.length
|
|
70
|
+
? await gate.canReadBatch(
|
|
71
|
+
workspaceIdForBranch(DEFAULT_BRANCH),
|
|
72
|
+
email,
|
|
73
|
+
folders.map(([, p]) => `${p.folder}/access.md`),
|
|
74
|
+
)
|
|
75
|
+
: new Map<string, boolean>();
|
|
76
|
+
const visible = new Set(folders.filter(([, p]) => verdicts.get(`${p.folder}/access.md`) === true).map(([name]) => name));
|
|
77
|
+
discoverable = (plugin) => visible.has(plugin);
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
bySkill = new Map(); // membership is a decoration; the shelf must not fall with it
|
|
81
|
+
}
|
|
82
|
+
res.json({
|
|
83
|
+
skills: skills.map((s) => ({ ...s, plugins: (bySkill.get(s.path) ?? []).filter((m) => discoverable(m.name)) })),
|
|
84
|
+
});
|
|
29
85
|
});
|
|
30
86
|
|
|
31
87
|
/**
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import { parseDocument } from 'yaml';
|
|
4
|
-
import { DEFAULT_BRANCH, PLUGINS_DIR } from '@bevel-software/platform-shared';
|
|
4
|
+
import { DEFAULT_BRANCH, PLUGINS_DIR, SKILLS_DIR } from '@bevel-software/platform-shared';
|
|
5
5
|
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
6
6
|
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
7
7
|
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
8
8
|
import { extractFrontmatter, resolveDeclaredId, dedupeById } from '../../shared/frontmatter-id.js';
|
|
9
9
|
import { walkFiles } from '../../shared/fs-walk.js';
|
|
10
10
|
import { TtlCache } from '../../shared/ttl-cache.js';
|
|
11
|
+
import { BevelIgnoreStack } from '../workspace/bevel-ignore.js';
|
|
11
12
|
import type {
|
|
12
13
|
ISkillService,
|
|
13
14
|
GetSkillResult,
|
|
@@ -75,6 +76,9 @@ export class SkillService implements ISkillService {
|
|
|
75
76
|
if (file !== undefined) {
|
|
76
77
|
if (!isSafeRelFile(file)) return { ok: false, error: 'invalid_file' };
|
|
77
78
|
const repoPath = `${found.summary.path}/${file}`;
|
|
79
|
+
// Only a file the catalog LISTS is served: the listing already applied
|
|
80
|
+
// the ignore rules, so a path that is not in it is one they hid.
|
|
81
|
+
if (!found.files.includes(repoPath)) return { ok: false, error: 'not_found' };
|
|
78
82
|
try {
|
|
79
83
|
const content = await this.workspaceService.readFile(wsId, `${this.kbDirName}/${repoPath}`);
|
|
80
84
|
return { ok: true, kind: 'file', file: { name, file, path: repoPath, content } };
|
|
@@ -97,15 +101,20 @@ export class SkillService implements ISkillService {
|
|
|
97
101
|
private async scan(): Promise<ParsedSkill[]> {
|
|
98
102
|
const cached = this.cache.get();
|
|
99
103
|
if (cached) return cached;
|
|
104
|
+
// Token first: a merge's `invalidate()` can land while `scanDisk` is still
|
|
105
|
+
// reading the pre-merge tree, and storing that read afterwards would undo
|
|
106
|
+
// the drop for a full TTL.
|
|
107
|
+
const token = this.cache.begin();
|
|
100
108
|
const skills = await this.scanDisk();
|
|
101
|
-
this.cache.set(skills);
|
|
109
|
+
this.cache.set(skills, token);
|
|
102
110
|
return skills;
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
private async scanDisk(): Promise<ParsedSkill[]> {
|
|
106
|
-
// Ensure the default-branch clone exists, then scan its
|
|
107
|
-
// failure (no workspace, no
|
|
108
|
-
// manual/tools must never break because skills can't
|
|
114
|
+
// Ensure the default-branch clone exists, then scan its Skills/ and
|
|
115
|
+
// Plugins/ roots. Any failure (no workspace, no such dir) degrades to an
|
|
116
|
+
// empty catalog — the manual/tools must never break because skills can't
|
|
117
|
+
// be read.
|
|
109
118
|
let wsId: string;
|
|
110
119
|
try {
|
|
111
120
|
wsId = (await this.workspaceService.getOrCreateForBranch(DEFAULT_BRANCH)).id;
|
|
@@ -115,20 +124,34 @@ export class SkillService implements ISkillService {
|
|
|
115
124
|
const kbRoot = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName);
|
|
116
125
|
|
|
117
126
|
// Skills may be grouped in category subfolders (each carrying its own
|
|
118
|
-
// access.md), so a SKILL.md can live at any depth under
|
|
119
|
-
// tree and treat every folder that directly contains a SKILL.md as a
|
|
120
|
-
// don't descend past it — its inner files are bundled assets, not
|
|
121
|
-
// skills. The skill name is the leaf folder name; its path is the
|
|
122
|
-
// repo-relative folder (e.g. `
|
|
127
|
+
// access.md), so a SKILL.md can live at any depth under either root. Walk
|
|
128
|
+
// the tree and treat every folder that directly contains a SKILL.md as a
|
|
129
|
+
// skill; don't descend past it — its inner files are bundled assets, not
|
|
130
|
+
// nested skills. The skill name is the leaf folder name; its path is the
|
|
131
|
+
// full repo-relative folder (e.g. `Skills/Engineering/deploy`).
|
|
132
|
+
//
|
|
133
|
+
// `.bevelignore` files INSIDE a root are honoured on the way down, the
|
|
134
|
+
// same layered rules the file tree applies: a repository that carries a
|
|
135
|
+
// build output beside its source (a `dist/` holding compiled copies of
|
|
136
|
+
// every skill) would otherwise list each skill twice and refuse the
|
|
137
|
+
// duplicate — the wrong one, half the time. The REPO-ROOT file is
|
|
138
|
+
// deliberately not consulted: it is where the template hides `Plugins/`
|
|
139
|
+
// from the Knowledge tree, and a rule that hides a root from the browser
|
|
140
|
+
// must not empty the catalog that root exists to feed.
|
|
123
141
|
const out: ParsedSkill[] = [];
|
|
124
|
-
const walk = async (dir: string, relFolder: string): Promise<void> => {
|
|
142
|
+
const walk = async (dir: string, relFolder: string, ignore: BevelIgnoreStack): Promise<void> => {
|
|
125
143
|
let entries: import('node:fs').Dirent[];
|
|
126
144
|
try {
|
|
127
145
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
128
146
|
} catch {
|
|
129
147
|
return;
|
|
130
148
|
}
|
|
149
|
+
ignore = await ignore.extendedWith(dir);
|
|
131
150
|
if (entries.some((e) => e.isFile() && e.name === 'SKILL.md')) {
|
|
151
|
+
// A skill folder is a LEAF whatever the ignore rules say of it: a rule
|
|
152
|
+
// naming its SKILL.md suppresses the skill, it does not turn the
|
|
153
|
+
// folder's assets into skills of their own.
|
|
154
|
+
if (ignore.isIgnored(path.join(dir, 'SKILL.md'), false)) return;
|
|
132
155
|
let raw: string;
|
|
133
156
|
try {
|
|
134
157
|
raw = await fs.readFile(path.join(dir, 'SKILL.md'), 'utf-8');
|
|
@@ -144,26 +167,43 @@ export class SkillService implements ISkillService {
|
|
|
144
167
|
const declared = resolveDeclaredId(fm.frontmatter, path.basename(dir));
|
|
145
168
|
const name = isSafeSkillName(declared) ? declared : path.basename(dir);
|
|
146
169
|
out.push({
|
|
147
|
-
summary: {
|
|
170
|
+
summary: {
|
|
171
|
+
name,
|
|
172
|
+
description: fm.description,
|
|
173
|
+
version: fm.version,
|
|
174
|
+
owner: fm.owner,
|
|
175
|
+
lifecycle: fm.lifecycle,
|
|
176
|
+
path: relFolder,
|
|
177
|
+
},
|
|
148
178
|
body: fm.body,
|
|
149
179
|
allowedTools: fm.allowedTools,
|
|
150
|
-
files: await listBundledFiles(dir, relFolder),
|
|
180
|
+
files: await listBundledFiles(dir, relFolder, ignore),
|
|
151
181
|
});
|
|
152
182
|
return; // a skill folder is a leaf — its subfolders hold assets, not skills
|
|
153
183
|
}
|
|
154
184
|
for (const entry of entries) {
|
|
155
185
|
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
156
|
-
|
|
186
|
+
const abs = path.join(dir, entry.name);
|
|
187
|
+
if (ignore.isIgnored(abs, true)) continue;
|
|
188
|
+
await walk(abs, `${relFolder}/${entry.name}`, ignore);
|
|
157
189
|
}
|
|
158
190
|
};
|
|
159
|
-
|
|
191
|
+
// Each root starts its own ignore stack (the walk extends it with the
|
|
192
|
+
// root's own file first). Order is cosmetic: the sort below is by name
|
|
193
|
+
// then path, so a same-named pair resolves the same way regardless.
|
|
194
|
+
await walk(path.join(kbRoot, SKILLS_DIR), SKILLS_DIR, BevelIgnoreStack.empty());
|
|
195
|
+
await walk(path.join(kbRoot, PLUGINS_DIR), PLUGINS_DIR, BevelIgnoreStack.empty());
|
|
160
196
|
// A skill's id (frontmatter `id`/`name`, else folder name) is how getSkill()
|
|
161
|
-
// resolves it, so it must be unique. Sort by (name, path) for a
|
|
162
|
-
// winner
|
|
163
|
-
//
|
|
197
|
+
// resolves it, so it must be unique. Sort by (name, root, path) for a
|
|
198
|
+
// deterministic winner — the shared root FIRST, since `Skills/` is a
|
|
199
|
+
// skill's canonical home and a same-named inline copy is the stale one —
|
|
200
|
+
// then REFUSE later duplicates via the shared dedup — the same rule tools
|
|
201
|
+
// use (no silent auto-suffix that would rebind an id under the caller).
|
|
202
|
+
const rootRank = (p: string) => (p === SKILLS_DIR || p.startsWith(`${SKILLS_DIR}/`) ? 0 : 1);
|
|
164
203
|
out.sort(
|
|
165
204
|
(a, b) =>
|
|
166
205
|
a.summary.name.localeCompare(b.summary.name) ||
|
|
206
|
+
rootRank(a.summary.path) - rootRank(b.summary.path) ||
|
|
167
207
|
a.summary.path.localeCompare(b.summary.path),
|
|
168
208
|
);
|
|
169
209
|
return dedupeById(out, (s) => s.summary.name, (s, id) =>
|
|
@@ -209,6 +249,10 @@ function scalarToString(value: unknown): string | undefined {
|
|
|
209
249
|
export function parseSkillFrontmatter(raw: string): {
|
|
210
250
|
description: string;
|
|
211
251
|
version?: string;
|
|
252
|
+
/** `metadata.owner` (or a top-level `owner`) — the governance record's owner, verbatim. */
|
|
253
|
+
owner?: string;
|
|
254
|
+
/** `metadata.lifecycle` (or top-level) — e.g. `active`, `deprecated`, `retired`; lowercased. */
|
|
255
|
+
lifecycle?: string;
|
|
212
256
|
allowedTools?: string[];
|
|
213
257
|
body: string;
|
|
214
258
|
/** The parsed frontmatter object (for shared id resolution: `id`/`name`). */
|
|
@@ -233,6 +277,11 @@ export function parseSkillFrontmatter(raw: string): {
|
|
|
233
277
|
const description = typeof data.description === 'string' ? data.description.trim() : '';
|
|
234
278
|
const metadata = (data.metadata ?? {}) as Record<string, unknown>;
|
|
235
279
|
const version = scalarToString(data.version) ?? scalarToString(metadata.version);
|
|
280
|
+
// The governance record: `metadata.owner` / `metadata.lifecycle` first (the
|
|
281
|
+
// agentskills convention this catalog reads), a top-level spelling second.
|
|
282
|
+
const owner = (scalarToString(metadata.owner) ?? scalarToString(data.owner))?.trim() || undefined;
|
|
283
|
+
const lifecycle =
|
|
284
|
+
(scalarToString(metadata.lifecycle) ?? scalarToString(data.lifecycle))?.trim().toLowerCase() || undefined;
|
|
236
285
|
|
|
237
286
|
// `allowed-tools` is a space-separated string (agentskills) or a YAML list.
|
|
238
287
|
const at = data['allowed-tools'];
|
|
@@ -242,12 +291,25 @@ export function parseSkillFrontmatter(raw: string): {
|
|
|
242
291
|
? at.split(/\s+/).filter(Boolean)
|
|
243
292
|
: undefined;
|
|
244
293
|
|
|
245
|
-
return { description, version, allowedTools, body, frontmatter: data };
|
|
294
|
+
return { description, version, owner, lifecycle, allowedTools, body, frontmatter: data };
|
|
246
295
|
}
|
|
247
296
|
|
|
248
|
-
/**
|
|
249
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Repo-root-relative paths of every bundled file under a skill folder
|
|
299
|
+
* (excludes SKILL.md), under the same ignore rules the walk applied on the
|
|
300
|
+
* way down — a rule in the skill's own `.bevelignore` or any folder above
|
|
301
|
+
* hides an asset from the listing, and therefore (see `getSkill`) from
|
|
302
|
+
* being served.
|
|
303
|
+
*/
|
|
304
|
+
async function listBundledFiles(dir: string, relFolder: string, ignore: BevelIgnoreStack): Promise<string[]> {
|
|
305
|
+
const hidden = (rel: string): boolean => {
|
|
306
|
+
const parts = rel.split('/');
|
|
307
|
+
for (let i = 1; i < parts.length; i++) {
|
|
308
|
+
if (ignore.isIgnored(path.join(dir, ...parts.slice(0, i)), true)) return true;
|
|
309
|
+
}
|
|
310
|
+
return ignore.isIgnored(path.join(dir, ...parts), false);
|
|
311
|
+
};
|
|
250
312
|
return (await walkFiles(dir, () => true))
|
|
251
|
-
.filter((rel) => rel !== 'SKILL.md')
|
|
313
|
+
.filter((rel) => rel !== 'SKILL.md' && !hidden(rel))
|
|
252
314
|
.map((rel) => `${relFolder}/${rel}`);
|
|
253
315
|
}
|
|
@@ -22,6 +22,12 @@ declare global {
|
|
|
22
22
|
export interface ExternalApiKeySummary {
|
|
23
23
|
id: string;
|
|
24
24
|
label: string;
|
|
25
|
+
/**
|
|
26
|
+
* What the key was minted AS — `key` for one a person created by hand,
|
|
27
|
+
* another kind for one a flow minted on their behalf (a Claude link).
|
|
28
|
+
* Stored with the row; the label is the person's to edit and proves nothing.
|
|
29
|
+
*/
|
|
30
|
+
kind: string;
|
|
25
31
|
createdAt: number;
|
|
26
32
|
lastUsedAt: number | null;
|
|
27
33
|
revokedAt: number | null;
|
|
@@ -37,6 +43,19 @@ export interface MintedExternalApiKey {
|
|
|
37
43
|
summary: ExternalApiKeySummary;
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
/** The kind of a key a person creates by hand. */
|
|
47
|
+
export const DEFAULT_KEY_KIND = 'key';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* How a key is minted. `kind` names one of the kinds the service was built
|
|
51
|
+
* with, each of which carries its own plaintext prefix (the tenant's for the
|
|
52
|
+
* default kind, `gho_` for a Claude link); an unknown kind is refused, since
|
|
53
|
+
* nothing would route its bearer back.
|
|
54
|
+
*/
|
|
55
|
+
export interface MintOptions {
|
|
56
|
+
kind?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
/**
|
|
41
60
|
* Contract for connection-key (API token) lifecycle. Used by the MCP auth
|
|
42
61
|
* middleware to resolve a Bearer token to a Bevel user, and by the settings
|
|
@@ -59,7 +78,7 @@ export interface IExternalApiKeyService {
|
|
|
59
78
|
* plaintext is **only** returned here — there is no read path that can
|
|
60
79
|
* surface it again.
|
|
61
80
|
*/
|
|
62
|
-
mint(userId: string, label: string): Promise<MintedExternalApiKey>;
|
|
81
|
+
mint(userId: string, label: string, options?: MintOptions): Promise<MintedExternalApiKey>;
|
|
63
82
|
|
|
64
83
|
/**
|
|
65
84
|
* Resolve a plaintext token to the owning user. Returns null when the
|
|
@@ -8,36 +8,53 @@ import {
|
|
|
8
8
|
TokenNotFoundError,
|
|
9
9
|
TokenStillActiveError,
|
|
10
10
|
} from './external-api-key.errors.js';
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_KEY_KIND,
|
|
13
|
+
type ExternalApiKeySummary,
|
|
14
|
+
type IExternalApiKeyService,
|
|
15
|
+
type MintOptions,
|
|
16
|
+
type MintedExternalApiKey,
|
|
15
17
|
} from './external-api-key.interface.js';
|
|
16
18
|
|
|
17
|
-
// Plaintext format: `<
|
|
19
|
+
// Plaintext format: `<prefix><43 base64url chars>`. 32 random bytes encoded
|
|
18
20
|
// base64url is 43 chars and gives 256 bits of entropy — overkill against
|
|
19
|
-
// brute force, cheap to copy. The
|
|
20
|
-
//
|
|
21
|
+
// brute force, cheap to copy. The prefix lets the auth middleware route
|
|
22
|
+
// key-vs-JWT requests without parsing both shapes.
|
|
21
23
|
const TOKEN_BYTES = 32;
|
|
22
24
|
|
|
23
25
|
const MAX_LABEL_LEN = 200;
|
|
24
26
|
|
|
25
27
|
export class ExternalApiKeyService implements IExternalApiKeyService {
|
|
28
|
+
private readonly prefixes: readonly string[];
|
|
29
|
+
|
|
26
30
|
/**
|
|
27
31
|
* @param keyPrefix Tenant-derived plaintext prefix (e.g. `bevel_`) — injected
|
|
28
32
|
* from {@link AppConfig.externalApiKeyPrefix} so a deploy can brand its keys.
|
|
33
|
+
* Every key of the default kind is minted with it.
|
|
34
|
+
* @param kinds Other kinds a key may be minted as, each with the plaintext
|
|
35
|
+
* prefix that kind is spelled with — a Claude link's `gho_`, the shape
|
|
36
|
+
* claude.ai expects from a GitHub host. Same key, same table, same
|
|
37
|
+
* revocation; the KIND is stored on the row and is what tells such keys
|
|
38
|
+
* apart (a label is the person's to edit), the prefix is only what the
|
|
39
|
+
* outside world sees and what routes the bearer back here.
|
|
29
40
|
*/
|
|
30
41
|
constructor(
|
|
31
42
|
private readonly db: Database,
|
|
32
43
|
private readonly keyPrefix: string,
|
|
33
|
-
|
|
44
|
+
private readonly kinds: Readonly<Record<string, string>> = {},
|
|
45
|
+
) {
|
|
46
|
+
this.prefixes = [keyPrefix, ...Object.values(kinds)];
|
|
47
|
+
}
|
|
34
48
|
|
|
35
|
-
/** True if a bearer string carries this
|
|
49
|
+
/** True if a bearer string carries one of this service's key prefixes. */
|
|
36
50
|
looksLikeExternalApiKey(token: string): boolean {
|
|
37
|
-
return typeof token === 'string' && token.startsWith(
|
|
51
|
+
return typeof token === 'string' && this.prefixes.some((p) => token.startsWith(p));
|
|
38
52
|
}
|
|
39
53
|
|
|
40
|
-
async mint(userId: string, label: string): Promise<MintedExternalApiKey> {
|
|
54
|
+
async mint(userId: string, label: string, options: MintOptions = {}): Promise<MintedExternalApiKey> {
|
|
55
|
+
const kind = options.kind ?? DEFAULT_KEY_KIND;
|
|
56
|
+
const prefix = kind === DEFAULT_KEY_KIND ? this.keyPrefix : this.kinds[kind];
|
|
57
|
+
if (!prefix) throw new Error(`Unknown key kind "${kind}"`);
|
|
41
58
|
const trimmed = (label ?? '').trim();
|
|
42
59
|
if (trimmed.length === 0) {
|
|
43
60
|
throw new InvalidTokenLabelError('Label cannot be empty');
|
|
@@ -48,12 +65,12 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
|
|
|
48
65
|
);
|
|
49
66
|
}
|
|
50
67
|
|
|
51
|
-
const plaintext =
|
|
68
|
+
const plaintext = prefix + randomBytes(TOKEN_BYTES).toString('base64url');
|
|
52
69
|
const tokenHash = hashToken(plaintext);
|
|
53
70
|
|
|
54
71
|
const [row] = await this.db
|
|
55
72
|
.insert(externalApiKeys)
|
|
56
|
-
.values({ userId, tokenHash, label: trimmed })
|
|
73
|
+
.values({ userId, tokenHash, label: trimmed, kind })
|
|
57
74
|
.returning();
|
|
58
75
|
|
|
59
76
|
return {
|
|
@@ -202,6 +219,7 @@ function toSummary(row: typeof externalApiKeys.$inferSelect): ExternalApiKeySumm
|
|
|
202
219
|
return {
|
|
203
220
|
id: row.id,
|
|
204
221
|
label: row.label,
|
|
222
|
+
kind: row.kind,
|
|
205
223
|
createdAt: row.createdAt.getTime(),
|
|
206
224
|
lastUsedAt: row.lastUsedAt ? row.lastUsedAt.getTime() : null,
|
|
207
225
|
revokedAt: row.revokedAt ? row.revokedAt.getTime() : null,
|
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import { describe, expect, it, vi, afterEach } from 'vitest';
|
|
2
|
-
import { descriptorsFromMcpJson } from '../mcp-json-discovery.js';
|
|
2
|
+
import { descriptorsFromMcpJson, judgeMcpServerEntry } from '../mcp-json-discovery.js';
|
|
3
3
|
|
|
4
4
|
afterEach(() => vi.restoreAllMocks());
|
|
5
5
|
|
|
6
|
+
describe('judgeMcpServerEntry', () => {
|
|
7
|
+
it('keeps every accepted header and env key — one spelled __proto__ included', () => {
|
|
8
|
+
// Parsed from text, as a repository file is: JSON.parse makes `__proto__`
|
|
9
|
+
// an own key, and a plain-object copy would turn it into a prototype
|
|
10
|
+
// assignment and lose it.
|
|
11
|
+
const http = judgeMcpServerEntry(
|
|
12
|
+
'vendor',
|
|
13
|
+
JSON.parse('{"type":"streamable-http","url":"https://x.example","headers":{"__proto__":"p","X-A":"a"}}'),
|
|
14
|
+
);
|
|
15
|
+
expect(http.ok).toBe(true);
|
|
16
|
+
if (!http.ok || http.transport !== 'streamable-http') throw new Error('unreachable');
|
|
17
|
+
expect(Object.keys(http.entry.headers ?? {}).sort()).toEqual(['X-A', '__proto__']);
|
|
18
|
+
expect(http.entry.headers?.['__proto__']).toBe('p');
|
|
19
|
+
|
|
20
|
+
const stdio = judgeMcpServerEntry(
|
|
21
|
+
'launch',
|
|
22
|
+
JSON.parse('{"type":"stdio","command":"run","env":{"__proto__":"p"}}'),
|
|
23
|
+
);
|
|
24
|
+
if (!stdio.ok || stdio.transport !== 'stdio') throw new Error('unreachable');
|
|
25
|
+
expect(stdio.entry.env?.['__proto__']).toBe('p');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
6
29
|
const MANIFEST = JSON.stringify({
|
|
7
30
|
name: 'gtm',
|
|
8
31
|
extensions: {
|
|
@@ -192,6 +192,8 @@ describe('manual namespaces are unique, not merely manual names', () => {
|
|
|
192
192
|
*/
|
|
193
193
|
async function writeMcpServer(folder: string, serverName: string): Promise<void> {
|
|
194
194
|
await mkdir(join(pluginsDir(), folder), { recursive: true });
|
|
195
|
+
// A plugin is a folder with a manifest; the servers ride in its mcp.json.
|
|
196
|
+
await writeFile(join(pluginsDir(), folder, 'plugin.json'), `{"name":"${folder.toLowerCase()}"}`);
|
|
195
197
|
await writeFile(
|
|
196
198
|
join(pluginsDir(), folder, 'mcp.json'),
|
|
197
199
|
JSON.stringify({ mcpServers: { [serverName]: { type: 'streamable-http', url: 'https://v.example/mcp' } } }),
|