@bevel-software/platform-core-backend 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +6 -6
- package/dist/core/catalog-cache-invalidation.d.ts +29 -0
- package/dist/core/catalog-cache-invalidation.d.ts.map +1 -0
- package/dist/core/catalog-cache-invalidation.js +71 -0
- package/dist/core/catalog-cache-invalidation.js.map +1 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +99 -8
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +18 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +96 -43
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +18 -6
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +26 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +213 -71
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +59 -7
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +51 -1
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +95 -1
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/access-model/group-files.d.ts.map +1 -1
- package/dist/modules/access-model/group-files.js +4 -1
- package/dist/modules/access-model/group-files.js.map +1 -1
- package/dist/modules/access-model/plugin-principals.d.ts +49 -0
- package/dist/modules/access-model/plugin-principals.d.ts.map +1 -0
- package/dist/modules/access-model/plugin-principals.js +125 -0
- package/dist/modules/access-model/plugin-principals.js.map +1 -0
- package/dist/modules/database/core-schema.d.ts +326 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +52 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/kb-sync/index.d.ts +6 -0
- package/dist/modules/kb-sync/index.d.ts.map +1 -0
- package/dist/modules/kb-sync/index.js +5 -0
- package/dist/modules/kb-sync/index.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts +77 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.interface.js +2 -0
- package/dist/modules/kb-sync/kb-sync.interface.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +70 -0
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.routes.js +135 -0
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts +45 -0
- package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -0
- package/dist/modules/kb-sync/kb-sync.service.js +183 -0
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -0
- package/dist/modules/kb-sync/sync-auth.d.ts +59 -0
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-auth.js +62 -0
- package/dist/modules/kb-sync/sync-auth.js.map +1 -0
- package/dist/modules/kb-sync/sync-payload.d.ts +32 -0
- package/dist/modules/kb-sync/sync-payload.d.ts.map +1 -0
- package/dist/modules/kb-sync/sync-payload.js +105 -0
- package/dist/modules/kb-sync/sync-payload.js.map +1 -0
- package/dist/modules/marketplace/git-http.routes.d.ts +42 -0
- package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/git-http.routes.js +190 -0
- package/dist/modules/marketplace/git-http.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +23 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts +59 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js +96 -0
- package/dist/modules/marketplace/github-facade/github-facade-codes.store.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +99 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +147 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts +47 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js +228 -0
- package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +109 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js +172 -0
- package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -0
- package/dist/modules/marketplace/github-facade/index.d.ts +6 -0
- package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -0
- package/dist/modules/marketplace/github-facade/index.js +6 -0
- package/dist/modules/marketplace/github-facade/index.js.map +1 -0
- package/dist/modules/marketplace/index.d.ts +4 -0
- package/dist/modules/marketplace/index.d.ts.map +1 -0
- package/dist/modules/marketplace/index.js +4 -0
- package/dist/modules/marketplace/index.js.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts +122 -0
- package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -0
- package/dist/modules/marketplace/marketplace-repo.service.js +300 -0
- package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +14 -8
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js +13 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.d.ts +13 -2
- package/dist/modules/mcp/oauth/oauth-state.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-state.js +3 -1
- package/dist/modules/mcp/oauth/oauth-state.js.map +1 -1
- package/dist/modules/plugins/compile/compile-marketplace.d.ts +97 -0
- package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -0
- package/dist/modules/plugins/compile/compile-marketplace.js +240 -0
- package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +66 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js +132 -0
- package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts +37 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +135 -0
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +45 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js +147 -0
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +37 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js +107 -0
- package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -0
- package/dist/modules/plugins/discovery/native.source.d.ts +15 -0
- package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/native.source.js +102 -0
- package/dist/modules/plugins/discovery/native.source.js.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts +96 -0
- package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -0
- package/dist/modules/plugins/discovery/plugin-source.js +2 -0
- package/dist/modules/plugins/discovery/plugin-source.js.map +1 -0
- package/dist/modules/plugins/index.d.ts +10 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +8 -0
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-links.d.ts +66 -0
- package/dist/modules/plugins/plugin-links.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.js +160 -0
- package/dist/modules/plugins/plugin-links.js.map +1 -0
- package/dist/modules/plugins/plugin-links.service.d.ts +114 -0
- package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-links.service.js +304 -0
- package/dist/modules/plugins/plugin-links.service.js.map +1 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts +39 -3
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +152 -52
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugin-rename.service.d.ts +69 -0
- package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -0
- package/dist/modules/plugins/plugin-rename.service.js +245 -0
- package/dist/modules/plugins/plugin-rename.service.js.map +1 -0
- package/dist/modules/plugins/plugins.contract.d.ts +27 -1
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +7 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +120 -12
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/plugins/plugins.service.d.ts +50 -3
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.service.js +108 -47
- package/dist/modules/plugins/plugins.service.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +9 -0
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +82 -2
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts +26 -0
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +9 -1
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/index.d.ts +1 -0
- package/dist/modules/skills/index.d.ts.map +1 -1
- package/dist/modules/skills/index.js +1 -0
- package/dist/modules/skills/index.js.map +1 -1
- package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
- package/dist/modules/skills/pending-skills.service.js +9 -4
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.d.ts +38 -0
- package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -0
- package/dist/modules/skills/skill-access-requests.routes.js +168 -0
- package/dist/modules/skills/skill-access-requests.routes.js.map +1 -0
- package/dist/modules/skills/skills.contract.d.ts +31 -0
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.d.ts +28 -3
- package/dist/modules/skills/skills.routes.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.js +42 -3
- package/dist/modules/skills/skills.routes.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +4 -0
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +83 -22
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +18 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.js +2 -1
- package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +13 -4
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +26 -9
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +36 -0
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.js +130 -92
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +6 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +18 -25
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +40 -2
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +227 -54
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +6 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +8 -0
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +52 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +242 -9
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +35 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +121 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +46 -2
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +112 -25
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +9 -9
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +33 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +110 -16
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +20 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +29 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/fs-errors.d.ts +8 -0
- package/dist/shared/fs-errors.d.ts.map +1 -0
- package/dist/shared/fs-errors.js +11 -0
- package/dist/shared/fs-errors.js.map +1 -0
- package/dist/shared/fs-walk.d.ts +20 -6
- package/dist/shared/fs-walk.d.ts.map +1 -1
- package/dist/shared/fs-walk.js +35 -27
- package/dist/shared/fs-walk.js.map +1 -1
- package/dist/shared/kb-walk.d.ts +52 -0
- package/dist/shared/kb-walk.d.ts.map +1 -0
- package/dist/shared/kb-walk.js +76 -0
- package/dist/shared/kb-walk.js.map +1 -0
- package/dist/shared/path-order.d.ts +18 -0
- package/dist/shared/path-order.d.ts.map +1 -0
- package/dist/shared/path-order.js +35 -0
- package/dist/shared/path-order.js.map +1 -0
- package/dist/shared/ttl-cache.d.ts +28 -1
- package/dist/shared/ttl-cache.d.ts.map +1 -1
- package/dist/shared/ttl-cache.js +33 -1
- package/dist/shared/ttl-cache.js.map +1 -1
- package/kb-template/.bevelignore +1 -1
- package/kb-template/AGENTS.md +95 -30
- package/kb-template/Skills/.gitkeep +0 -0
- package/migrations/0005_github_facade.sql +25 -0
- package/migrations/meta/0005_snapshot.json +1717 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/__tests__/kb-layout-config.test.ts +110 -0
- package/src/core/__tests__/catalog-cache-invalidation.test.ts +146 -0
- package/src/core/catalog-cache-invalidation.ts +92 -0
- package/src/core/create-core-server.ts +123 -7
- package/src/core/create-core-services.ts +162 -41
- package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +86 -0
- package/src/modules/access/__tests__/access-control.service.test.ts +19 -3
- package/src/modules/access/__tests__/access-plugin-principals.test.ts +368 -0
- package/src/modules/access/__tests__/groups-admin.service.test.ts +8 -0
- package/src/modules/access/access-control.interface.ts +20 -9
- package/src/modules/access/access-control.service.ts +240 -78
- package/src/modules/access/access.routes.ts +96 -14
- package/src/modules/access-model/__tests__/access-grammar.test.ts +18 -0
- package/src/modules/access-model/__tests__/plugin-token-canonical.test.ts +40 -0
- package/src/modules/access-model/access-grammar.ts +114 -2
- package/src/modules/access-model/group-files.ts +4 -0
- package/src/modules/access-model/plugin-principals.ts +137 -0
- package/src/modules/database/core-schema.ts +54 -0
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +297 -0
- package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +295 -0
- package/src/modules/kb-sync/__tests__/sync-auth.test.ts +118 -0
- package/src/modules/kb-sync/__tests__/sync-payload.test.ts +148 -0
- package/src/modules/kb-sync/index.ts +19 -0
- package/src/modules/kb-sync/kb-sync.interface.ts +79 -0
- package/src/modules/kb-sync/kb-sync.routes.ts +197 -0
- package/src/modules/kb-sync/kb-sync.service.ts +201 -0
- package/src/modules/kb-sync/sync-auth.ts +118 -0
- package/src/modules/kb-sync/sync-payload.ts +143 -0
- package/src/modules/marketplace/__tests__/github-facade.test.ts +443 -0
- package/src/modules/marketplace/__tests__/marketplace-git.test.ts +179 -0
- package/src/modules/marketplace/git-http.routes.ts +193 -0
- package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +78 -0
- package/src/modules/marketplace/github-facade/github-facade-codes.store.ts +145 -0
- package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +208 -0
- package/src/modules/marketplace/github-facade/github-facade.routes.ts +268 -0
- package/src/modules/marketplace/github-facade/github-facade.service.ts +221 -0
- package/src/modules/marketplace/github-facade/index.ts +25 -0
- package/src/modules/marketplace/index.ts +3 -0
- package/src/modules/marketplace/marketplace-repo.service.ts +337 -0
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -0
- package/src/modules/mcp/oauth/oauth-consent.routes.ts +24 -2
- package/src/modules/mcp/oauth/oauth-state.ts +15 -3
- package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +163 -0
- package/src/modules/plugins/__tests__/compile-marketplace.test.ts +294 -0
- package/src/modules/plugins/__tests__/plugin-index.service.test.ts +56 -12
- package/src/modules/plugins/__tests__/plugin-links.service.test.ts +287 -0
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +205 -5
- package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +394 -0
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +80 -33
- package/src/modules/plugins/compile/compile-marketplace.ts +403 -0
- package/src/modules/plugins/compile/marketplace-compiler.service.ts +150 -0
- package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +329 -0
- package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +140 -0
- package/src/modules/plugins/discovery/bundle-dialect/registry.ts +175 -0
- package/src/modules/plugins/discovery/kb-plugin-source.ts +114 -0
- package/src/modules/plugins/discovery/native.source.ts +122 -0
- package/src/modules/plugins/discovery/plugin-source.ts +99 -0
- package/src/modules/plugins/index.ts +10 -0
- package/src/modules/plugins/plugin-links.service.ts +363 -0
- package/src/modules/plugins/plugin-links.ts +184 -0
- package/src/modules/plugins/plugin-provision.service.ts +160 -54
- package/src/modules/plugins/plugin-rename.service.ts +298 -0
- package/src/modules/plugins/plugins.contract.ts +28 -1
- package/src/modules/plugins/plugins.routes.ts +128 -11
- package/src/modules/plugins/plugins.service.ts +106 -53
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +57 -0
- package/src/modules/settings/__tests__/setup.routes.sync-status.test.ts +77 -0
- package/src/modules/settings/deployment-settings.service.ts +95 -2
- package/src/modules/settings/setup.routes.ts +24 -0
- package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +128 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +85 -0
- package/src/modules/skills/index.ts +1 -0
- package/src/modules/skills/pending-skills.service.ts +7 -5
- package/src/modules/skills/skill-access-requests.routes.ts +199 -0
- package/src/modules/skills/skills.contract.ts +32 -0
- package/src/modules/skills/skills.routes.ts +59 -3
- package/src/modules/skills/skills.service.ts +84 -22
- package/src/modules/tool-auth/external-api-key.interface.ts +20 -1
- package/src/modules/tool-auth/external-api-key.service.ts +31 -13
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +24 -1
- package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +2 -0
- package/src/modules/tool-manuals/mcp-json-discovery.ts +154 -88
- package/src/modules/tool-manuals/tool-manuals.service.ts +14 -23
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +2 -2
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +119 -2
- package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +437 -0
- package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +3 -3
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +47 -4
- package/src/modules/workflow/git/__tests__/git.service.syncFromRemote.test.ts +253 -0
- package/src/modules/workflow/git/git.service.ts +239 -56
- package/src/modules/workflow/git/pull-request.service.ts +9 -0
- package/src/modules/workflow/workflow.service.ts +260 -9
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +6 -7
- package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +93 -0
- package/src/modules/workspace/__tests__/workspace.service.missing-branch.test.ts +28 -0
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +42 -12
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +219 -4
- package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +129 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +44 -2
- package/src/modules/workspace/startup/steps/template-files.step.ts +131 -27
- package/src/modules/workspace/workspace.routes.ts +9 -17
- package/src/modules/workspace/workspace.service.ts +107 -16
- package/src/shared/__tests__/filename.test.ts +0 -0
- package/src/shared/__tests__/fs-walk.test.ts +22 -1
- package/src/shared/__tests__/kb-walk.test.ts +107 -0
- package/src/shared/__tests__/path-order.test.ts +24 -0
- package/src/shared/__tests__/ttl-cache.test.ts +55 -0
- package/src/shared/domain-errors.ts +29 -0
- package/src/shared/fs-errors.ts +10 -0
- package/src/shared/fs-walk.ts +38 -23
- package/src/shared/kb-walk.ts +95 -0
- package/src/shared/path-order.ts +33 -0
- package/src/shared/ttl-cache.ts +33 -1
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execFile, spawn, type ChildProcess } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { WorkspaceMutex } from '../kb-fs/mutex.js';
|
|
7
|
+
import type { VirtualTree } from '../plugins/compile/compile-marketplace.js';
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
/** What the repo service asks the compiler for — the one seam it has. */
|
|
12
|
+
export interface MarketplaceCompiler {
|
|
13
|
+
sourceCommit(): Promise<string>;
|
|
14
|
+
compileFor(
|
|
15
|
+
audience: { userEmail: string },
|
|
16
|
+
/** The commit the tree is compiled from, when the caller already read it. */
|
|
17
|
+
sourceCommit?: string,
|
|
18
|
+
): Promise<VirtualTree & { sourceCommit: string }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface EnsureResult {
|
|
22
|
+
/** The git namespace the caller's tree lives in. */
|
|
23
|
+
namespace: string;
|
|
24
|
+
/** True when a new commit was written this call. */
|
|
25
|
+
compiled: boolean;
|
|
26
|
+
sourceCommit: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* ONE bare repository, ONE git namespace per caller.
|
|
31
|
+
*
|
|
32
|
+
* Every person's compiled marketplace is a branch in its own namespace
|
|
33
|
+
* (`refs/namespaces/<ns>/refs/heads/main`), which is exactly what git
|
|
34
|
+
* namespaces exist for (gitnamespaces(7)): `git http-backend` run with
|
|
35
|
+
* `GIT_NAMESPACE=<ns>` advertises only that namespace's refs, so a clone sees
|
|
36
|
+
* a repository whose `main` is their tree and nothing else — while every
|
|
37
|
+
* identical skill across callers is one blob in one object store.
|
|
38
|
+
*
|
|
39
|
+
* Freshness is LAZY and keyed on the knowledge base's default-branch commit:
|
|
40
|
+
* every input to a compile (skills, manifests, access rules, roles, groups)
|
|
41
|
+
* lives in that repository, so "same source commit" means "same answer".
|
|
42
|
+
* The last compiled source per namespace sits in a sidecar file rather than
|
|
43
|
+
* a commit trailer, so an unchanged tree costs no commit and no re-check.
|
|
44
|
+
*
|
|
45
|
+
* Each recompile appends a commit whose parent is the namespace's current
|
|
46
|
+
* head, so a client's `git pull` is always a fast-forward — including when
|
|
47
|
+
* access was withdrawn and files vanish. Writes go through git plumbing
|
|
48
|
+
* against a temporary index: no working tree of the bare repo, no checkout.
|
|
49
|
+
*
|
|
50
|
+
* Read-only by construction: `http.receivepack` is pinned off (http-backend
|
|
51
|
+
* would otherwise enable pushes for an authenticated REMOTE_USER), and the
|
|
52
|
+
* route refuses the receive-pack service before git ever sees it.
|
|
53
|
+
*/
|
|
54
|
+
export class MarketplaceRepoService {
|
|
55
|
+
private readonly locks = new WorkspaceMutex();
|
|
56
|
+
private initialised: Promise<void> | null = null;
|
|
57
|
+
|
|
58
|
+
constructor(
|
|
59
|
+
/** Absolute path of the bare repository (created on first use). */
|
|
60
|
+
readonly repoDir: string,
|
|
61
|
+
private readonly compiler: MarketplaceCompiler,
|
|
62
|
+
private readonly committer: { name: string; email: string } = {
|
|
63
|
+
name: 'Hexis',
|
|
64
|
+
email: 'hexis@localhost',
|
|
65
|
+
},
|
|
66
|
+
) {}
|
|
67
|
+
|
|
68
|
+
/** The directory `GIT_PROJECT_ROOT` points at, and the repo's name under it. */
|
|
69
|
+
get projectRoot(): string {
|
|
70
|
+
return path.dirname(this.repoDir);
|
|
71
|
+
}
|
|
72
|
+
get repoName(): string {
|
|
73
|
+
return path.basename(this.repoDir);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The namespace for a user id — one path segment, stable, opaque. */
|
|
77
|
+
static namespaceFor(userId: string): string {
|
|
78
|
+
return `u-${userId.replace(/[^A-Za-z0-9._-]+/g, '_').slice(0, 120)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async ensureRepo(): Promise<void> {
|
|
82
|
+
this.initialised ??= (async () => {
|
|
83
|
+
await fs.mkdir(this.projectRoot, { recursive: true });
|
|
84
|
+
const isRepo = await fs
|
|
85
|
+
.access(path.join(this.repoDir, 'HEAD'))
|
|
86
|
+
.then(() => true, () => false);
|
|
87
|
+
if (!isRepo) await this.git(['init', '--bare', '--quiet', this.repoDir]);
|
|
88
|
+
await this.git(['-C', this.repoDir, 'config', 'http.receivepack', 'false']);
|
|
89
|
+
await this.git(['-C', this.repoDir, 'config', 'http.uploadpack', 'true']);
|
|
90
|
+
await fs.mkdir(this.sidecarDir(), { recursive: true });
|
|
91
|
+
})().catch((err: unknown) => {
|
|
92
|
+
// A failed initialisation is retried by the next request, not
|
|
93
|
+
// remembered until restart: a volume that was briefly unavailable must
|
|
94
|
+
// not turn every later fetch into a 500.
|
|
95
|
+
this.initialised = null;
|
|
96
|
+
throw err;
|
|
97
|
+
});
|
|
98
|
+
await this.initialised;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Bring the caller's namespace up to the knowledge base's current commit,
|
|
103
|
+
* compiling only when it moved. Serialised per namespace: two fetches
|
|
104
|
+
* arriving together compile once.
|
|
105
|
+
*/
|
|
106
|
+
async ensureCompiled(user: { id: string; email: string }): Promise<EnsureResult> {
|
|
107
|
+
await this.ensureRepo();
|
|
108
|
+
const namespace = MarketplaceRepoService.namespaceFor(user.id);
|
|
109
|
+
return this.locks.run(`ns:${namespace}`, async () => {
|
|
110
|
+
const last = await this.readSidecar(namespace);
|
|
111
|
+
const head = await this.headOf(namespace);
|
|
112
|
+
let current = await this.compiler.sourceCommit();
|
|
113
|
+
if (last === current && head !== null) {
|
|
114
|
+
// Unchanged source is still a served namespace: its HEAD is checked
|
|
115
|
+
// on every fetch (a read) and written only when it is missing, so a
|
|
116
|
+
// crash after an earlier branch update cannot leave a clone that sees
|
|
117
|
+
// no default branch — and the cache hit stays a read on a healthy repo.
|
|
118
|
+
await this.ensureHead(namespace);
|
|
119
|
+
return { namespace, compiled: false, sourceCommit: current };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The commit just read is the one the tree is stamped with: a second
|
|
123
|
+
// read that failed would otherwise record a placeholder as the compiled
|
|
124
|
+
// source and make every later fetch recompile. And the checkout can
|
|
125
|
+
// move WHILE we compile from it (a merge landing mid-read), so the
|
|
126
|
+
// commit is re-read afterwards: a tree stamped with a commit it was
|
|
127
|
+
// not compiled from would freeze that caller on a stale tree until the
|
|
128
|
+
// next change. Bounded — a checkout that moves on every attempt is
|
|
129
|
+
// served the latest attempt rather than never.
|
|
130
|
+
let tree = await this.compiler.compileFor({ userEmail: user.email }, current);
|
|
131
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
132
|
+
const after = await this.compiler.sourceCommit();
|
|
133
|
+
if (after === current) break;
|
|
134
|
+
current = after;
|
|
135
|
+
tree = await this.compiler.compileFor({ userEmail: user.email }, current);
|
|
136
|
+
}
|
|
137
|
+
const sha = await this.commitTree(namespace, tree, head);
|
|
138
|
+
await this.writeSidecar(namespace, tree.sourceCommit);
|
|
139
|
+
return { namespace, compiled: sha !== head, sourceCommit: tree.sourceCommit };
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The caller's namespace and its head commit, compiled if stale — what the
|
|
145
|
+
* GitHub-shaped REST surface (the Cowork path) reads instead of cloning.
|
|
146
|
+
*/
|
|
147
|
+
async headFor(user: { id: string; email: string }): Promise<{ namespace: string; sha: string }> {
|
|
148
|
+
const { namespace } = await this.ensureCompiled(user);
|
|
149
|
+
const sha = await this.headOf(namespace);
|
|
150
|
+
if (!sha) throw new Error(`namespace ${namespace} has no head after compiling`);
|
|
151
|
+
return { namespace, sha };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** One commit as the REST surface describes it. */
|
|
155
|
+
async describeCommit(sha: string): Promise<{
|
|
156
|
+
sha: string;
|
|
157
|
+
tree: string;
|
|
158
|
+
message: string;
|
|
159
|
+
authorName: string;
|
|
160
|
+
authorEmail: string;
|
|
161
|
+
date: string;
|
|
162
|
+
}> {
|
|
163
|
+
assertObjectId(sha);
|
|
164
|
+
const { stdout } = await this.git(['-C', this.repoDir, 'log', '-1', '--format=%H%n%T%n%an%n%ae%n%aI%n%B', sha]);
|
|
165
|
+
const [full, tree, authorName, authorEmail, date, ...rest] = stdout.split('\n');
|
|
166
|
+
return {
|
|
167
|
+
sha: full ?? sha,
|
|
168
|
+
tree: tree ?? '',
|
|
169
|
+
message: rest.join('\n').trim(),
|
|
170
|
+
authorName: authorName ?? '',
|
|
171
|
+
authorEmail: authorEmail ?? '',
|
|
172
|
+
date: date ?? '',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Whether `sha` is the namespace's head or one of its ancestors — the only
|
|
178
|
+
* commits a caller may read as that namespace. The object store is shared
|
|
179
|
+
* across everyone, so a sha alone is never enough.
|
|
180
|
+
*/
|
|
181
|
+
async contains(namespace: string, sha: string): Promise<boolean> {
|
|
182
|
+
if (!looksLikeObjectId(sha)) return false;
|
|
183
|
+
const head = await this.headOf(namespace);
|
|
184
|
+
if (!head) return false;
|
|
185
|
+
// Two questions with two answers each, and only "no" is ever silent:
|
|
186
|
+
// a commit git does not have is simply not contained; a commit it has
|
|
187
|
+
// is an ancestor (exit 0) or not (exit 1); anything else — a corrupt
|
|
188
|
+
// store, a lock, a bad ref — is a failure the caller must hear about,
|
|
189
|
+
// not a 404 wearing its clothes.
|
|
190
|
+
try {
|
|
191
|
+
await this.git(['-C', this.repoDir, 'rev-parse', '--verify', '--quiet', `${sha}^{commit}`]);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (exitCodeOf(err) === 1) return false;
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
await this.git(['-C', this.repoDir, 'merge-base', '--is-ancestor', sha, head]);
|
|
198
|
+
return true;
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if (exitCodeOf(err) === 1) return false;
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The tree at `sha` as a zip stream, every path under `prefix/`. Check {@link contains} first. */
|
|
206
|
+
archiveZip(sha: string, prefix: string): ChildProcess {
|
|
207
|
+
assertObjectId(sha);
|
|
208
|
+
return spawn('git', ['-C', this.repoDir, 'archive', '--format=zip', `--prefix=${prefix}/`, sha], {
|
|
209
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- internal --------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
private refOf(namespace: string): string {
|
|
216
|
+
return `refs/namespaces/${namespace}/refs/heads/main`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private async headOf(namespace: string): Promise<string | null> {
|
|
220
|
+
try {
|
|
221
|
+
const { stdout } = await this.git(['-C', this.repoDir, 'rev-parse', '--verify', '--quiet', this.refOf(namespace)]);
|
|
222
|
+
return stdout.trim() || null;
|
|
223
|
+
} catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Write `tree` as a commit on the namespace's branch. Materialises the
|
|
230
|
+
* virtual tree into a scratch directory, stages it into a throwaway index
|
|
231
|
+
* against the bare object store, and commits — no working tree involved.
|
|
232
|
+
* When the tree hash equals the head's, nothing is committed.
|
|
233
|
+
*/
|
|
234
|
+
private async commitTree(
|
|
235
|
+
namespace: string,
|
|
236
|
+
tree: VirtualTree & { sourceCommit: string },
|
|
237
|
+
parent: string | null,
|
|
238
|
+
): Promise<string> {
|
|
239
|
+
const scratch = await fs.mkdtemp(path.join(os.tmpdir(), 'hexis-marketplace-'));
|
|
240
|
+
// The throwaway index lives BESIDE the scratch worktree, never inside it:
|
|
241
|
+
// `git add -A` would otherwise stage the index (and its lock) into every
|
|
242
|
+
// compiled tree.
|
|
243
|
+
const indexDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hexis-marketplace-index-'));
|
|
244
|
+
try {
|
|
245
|
+
for (const [rel, bytes] of tree.files) {
|
|
246
|
+
// The compiler sanitises every segment it invents, but the tree is
|
|
247
|
+
// built from repository content, so the sink checks containment
|
|
248
|
+
// itself: nothing is written outside the scratch directory.
|
|
249
|
+
const abs = path.resolve(scratch, rel);
|
|
250
|
+
if (!abs.startsWith(scratch + path.sep)) {
|
|
251
|
+
throw new Error(`refusing to materialise "${rel}": escapes the scratch tree`);
|
|
252
|
+
}
|
|
253
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
254
|
+
await fs.writeFile(abs, bytes);
|
|
255
|
+
}
|
|
256
|
+
const index = path.join(indexDir, 'index');
|
|
257
|
+
const env = { ...process.env, GIT_INDEX_FILE: index, GIT_DIR: this.repoDir, GIT_WORK_TREE: scratch };
|
|
258
|
+
await this.git(['add', '-A', '--', '.'], { cwd: scratch, env });
|
|
259
|
+
const treeSha = (await this.git(['write-tree'], { env })).stdout.trim();
|
|
260
|
+
if (parent) {
|
|
261
|
+
const parentTree = (await this.git(['-C', this.repoDir, 'rev-parse', `${parent}^{tree}`])).stdout.trim();
|
|
262
|
+
if (parentTree === treeSha) return parent;
|
|
263
|
+
}
|
|
264
|
+
const message = `Compile marketplace from ${tree.sourceCommit}\n\nHexis-Source-Commit: ${tree.sourceCommit}\n`;
|
|
265
|
+
const commitEnv = {
|
|
266
|
+
...process.env,
|
|
267
|
+
GIT_AUTHOR_NAME: this.committer.name,
|
|
268
|
+
GIT_AUTHOR_EMAIL: this.committer.email,
|
|
269
|
+
GIT_COMMITTER_NAME: this.committer.name,
|
|
270
|
+
GIT_COMMITTER_EMAIL: this.committer.email,
|
|
271
|
+
};
|
|
272
|
+
const args = ['-C', this.repoDir, 'commit-tree', treeSha, '-m', message, ...(parent ? ['-p', parent] : [])];
|
|
273
|
+
const sha = (await this.git(args, { env: commitEnv })).stdout.trim();
|
|
274
|
+
// HEAD FIRST, then the branch: a symbolic ref may dangle, a branch
|
|
275
|
+
// without a HEAD is a clone that checks out nothing. Ordered so that a
|
|
276
|
+
// crash between the two leaves the recoverable state.
|
|
277
|
+
await this.ensureHead(namespace);
|
|
278
|
+
await this.git(['-C', this.repoDir, 'update-ref', this.refOf(namespace), sha, ...(parent ? [parent] : [])]);
|
|
279
|
+
return sha;
|
|
280
|
+
} finally {
|
|
281
|
+
await fs.rm(scratch, { recursive: true, force: true });
|
|
282
|
+
await fs.rm(indexDir, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Point the namespace's HEAD at its branch — valid before the branch
|
|
288
|
+
* exists. A read first: the symref is written only when absent or wrong,
|
|
289
|
+
* so the common path touches nothing.
|
|
290
|
+
*/
|
|
291
|
+
private async ensureHead(namespace: string): Promise<void> {
|
|
292
|
+
const head = `refs/namespaces/${namespace}/HEAD`;
|
|
293
|
+
const current = await this.git(['-C', this.repoDir, 'symbolic-ref', '--quiet', head]).then(
|
|
294
|
+
(r) => r.stdout.trim(),
|
|
295
|
+
() => null,
|
|
296
|
+
);
|
|
297
|
+
if (current === this.refOf(namespace)) return;
|
|
298
|
+
await this.git(['-C', this.repoDir, 'symbolic-ref', head, this.refOf(namespace)]);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private sidecarDir(): string {
|
|
302
|
+
return path.join(this.repoDir, 'hexis-namespaces');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private async readSidecar(namespace: string): Promise<string | null> {
|
|
306
|
+
try {
|
|
307
|
+
return (await fs.readFile(path.join(this.sidecarDir(), `${namespace}.json`), 'utf-8')).trim() || null;
|
|
308
|
+
} catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private async writeSidecar(namespace: string, sourceCommit: string): Promise<void> {
|
|
314
|
+
await fs.writeFile(path.join(this.sidecarDir(), `${namespace}.json`), sourceCommit);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private git(args: string[], opts: { cwd?: string; env?: NodeJS.ProcessEnv } = {}) {
|
|
318
|
+
return execFileAsync('git', args, { cwd: opts.cwd, env: opts.env ?? process.env, maxBuffer: 64 * 1024 * 1024 });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** The exit code an execFile failure carries, or null when the process never ran. */
|
|
323
|
+
function exitCodeOf(err: unknown): number | null {
|
|
324
|
+
const code = (err as { code?: unknown } | null)?.code;
|
|
325
|
+
return typeof code === 'number' ? code : null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function looksLikeObjectId(sha: string): boolean {
|
|
329
|
+
return /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(sha);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function assertObjectId(sha: string): void {
|
|
333
|
+
// An object id is the only thing these commands accept as a revision — a
|
|
334
|
+
// ref name, an option, anything git would interpret, is refused before it
|
|
335
|
+
// reaches the argument list.
|
|
336
|
+
if (!looksLikeObjectId(sha)) throw new Error(`not an object id: ${sha}`);
|
|
337
|
+
}
|
|
@@ -167,6 +167,9 @@ export class BevelOAuthProvider implements OAuthServerProvider {
|
|
|
167
167
|
userId: string,
|
|
168
168
|
st: McpAuthRequestState,
|
|
169
169
|
): Promise<{ redirectTo: string }> {
|
|
170
|
+
// A state without a PKCE challenge is a Claude-link request, which the
|
|
171
|
+
// marketplace bridge finishes; it must never reach the SDK's code store.
|
|
172
|
+
if (!st.cc) throw new InvalidGrantError('Authorization request carries no PKCE challenge');
|
|
170
173
|
const code = this.deps.tokenPrefix + randomBytes(TOKEN_BYTES).toString('base64url');
|
|
171
174
|
await this.deps.db.insert(oauthAuthCodes).values({
|
|
172
175
|
codeHash: hashToken(code),
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import type { BevelOAuthProvider } from './bevel-oauth-provider.js';
|
|
3
|
-
import { verifyAuthRequest } from './oauth-state.js';
|
|
3
|
+
import { verifyAuthRequest, type McpAuthRequestState } from './oauth-state.js';
|
|
4
4
|
import '../../auth/auth.middleware.js'; // Express Request augmentation (req.userId / req.userEmail)
|
|
5
5
|
|
|
6
6
|
export interface OAuthConsentRoutesDeps {
|
|
7
7
|
provider: BevelOAuthProvider;
|
|
8
8
|
/** Same HMAC secret the provider signs the authorize state with. */
|
|
9
9
|
stateSecret: string;
|
|
10
|
+
/**
|
|
11
|
+
* The GitHub facade, when the deployment has one: a state it signed
|
|
12
|
+
* (`gh`) is a person connecting an account on a product that speaks to
|
|
13
|
+
* this deployment as a GitHub Enterprise host (claude.ai), and the code
|
|
14
|
+
* that flow needs is the facade's, not the SDK's.
|
|
15
|
+
*/
|
|
16
|
+
facade?: {
|
|
17
|
+
isFacadeRequest(st: McpAuthRequestState): boolean;
|
|
18
|
+
clientNameFor(st: McpAuthRequestState): string;
|
|
19
|
+
completeConsent(userId: string, st: McpAuthRequestState): Promise<{ redirectTo: string }>;
|
|
20
|
+
};
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
/**
|
|
@@ -17,6 +28,9 @@ export interface OAuthConsentRoutesDeps {
|
|
|
17
28
|
* landing from the redirect chain authenticates via the HttpOnly bevel_token
|
|
18
29
|
* cookie fallback, so THIS is where a Bevel user attaches to the flow.
|
|
19
30
|
*/
|
|
31
|
+
/** A facade state reaching a deployment whose routes were built without the facade. */
|
|
32
|
+
const NO_FACADE = 'Connecting an external account this way is not enabled on this deployment.';
|
|
33
|
+
|
|
20
34
|
export function createOAuthConsentRoutes(deps: OAuthConsentRoutesDeps): express.Router {
|
|
21
35
|
const router = express.Router();
|
|
22
36
|
|
|
@@ -26,6 +40,11 @@ export function createOAuthConsentRoutes(deps: OAuthConsentRoutesDeps): express.
|
|
|
26
40
|
const raw = typeof req.query.state === 'string' ? req.query.state : '';
|
|
27
41
|
const st = raw ? verifyAuthRequest(deps.stateSecret, raw) : null;
|
|
28
42
|
if (!st) return void res.status(400).json({ error: 'Invalid or expired authorization request. Restart the connection from your agent.' });
|
|
43
|
+
if (st.gh) {
|
|
44
|
+
if (!deps.facade) return void res.status(400).json({ error: NO_FACADE });
|
|
45
|
+
res.json({ clientName: deps.facade.clientNameFor(st), scope: null, resource: null });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
29
48
|
const client = await deps.provider.clientsStore.getClient(st.c);
|
|
30
49
|
res.json({
|
|
31
50
|
clientName: client?.client_name ?? null,
|
|
@@ -42,8 +61,11 @@ export function createOAuthConsentRoutes(deps: OAuthConsentRoutesDeps): express.
|
|
|
42
61
|
const raw = typeof (req.body ?? {}).state === 'string' ? req.body.state : '';
|
|
43
62
|
const st = raw ? verifyAuthRequest(deps.stateSecret, raw) : null;
|
|
44
63
|
if (!st) return void res.status(400).json({ error: 'Invalid or expired authorization request. Restart the connection from your agent.' });
|
|
64
|
+
if (st.gh && !deps.facade) return void res.status(400).json({ error: NO_FACADE });
|
|
45
65
|
try {
|
|
46
|
-
const { redirectTo } =
|
|
66
|
+
const { redirectTo } = st.gh && deps.facade
|
|
67
|
+
? await deps.facade.completeConsent(userId, st)
|
|
68
|
+
: await deps.provider.issueAuthCode(userId, st);
|
|
47
69
|
res.json({ redirectTo });
|
|
48
70
|
} catch (err) {
|
|
49
71
|
// Message only — the raw error object can carry sensitive context
|
|
@@ -18,8 +18,19 @@ export interface McpAuthRequestState {
|
|
|
18
18
|
c: string;
|
|
19
19
|
/** redirect_uri (already validated against the client's registration). */
|
|
20
20
|
r: string;
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* PKCE code_challenge (S256 — the SDK authorize handler enforces it).
|
|
23
|
+
* Absent only on a GitHub-facade request (`gh`), whose client is a
|
|
24
|
+
* confidential one that proves itself with a secret at the token exchange.
|
|
25
|
+
*/
|
|
26
|
+
cc?: string;
|
|
27
|
+
/**
|
|
28
|
+
* A GitHub-shaped authorize request from a product that treats this
|
|
29
|
+
* deployment as a GitHub Enterprise host (the marketplace facade), not an
|
|
30
|
+
* MCP client: the consent routes hand it to the facade, which mints a code
|
|
31
|
+
* of its own instead of the SDK's.
|
|
32
|
+
*/
|
|
33
|
+
gh?: true;
|
|
23
34
|
/** The client's own `state` parameter, echoed back on the code redirect. */
|
|
24
35
|
s?: string;
|
|
25
36
|
/** Requested scope (space-joined). */
|
|
@@ -51,7 +62,8 @@ export function verifyAuthRequest(secret: string, token: string): McpAuthRequest
|
|
|
51
62
|
if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
|
|
52
63
|
try {
|
|
53
64
|
const parsed = JSON.parse(Buffer.from(body, 'base64url').toString()) as McpAuthRequestState;
|
|
54
|
-
if (typeof parsed.iat !== 'number' || !parsed.c || !parsed.r
|
|
65
|
+
if (typeof parsed.iat !== 'number' || !parsed.c || !parsed.r) return null;
|
|
66
|
+
if (!parsed.cc && parsed.gh !== true) return null;
|
|
55
67
|
const age = Date.now() - parsed.iat;
|
|
56
68
|
if (age > STATE_MAX_AGE_MS || age < -STATE_SKEW_MS) return null;
|
|
57
69
|
return parsed;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { DEFAULT_BRANCH, DEFAULT_KB_LAYOUT, configureKbLayout } from '@bevel-software/platform-shared';
|
|
6
|
+
|
|
7
|
+
import type { WorkspaceService } from '../../workspace/workspace.service.js';
|
|
8
|
+
import { AccessControlService } from '../../access/access-control.service.js';
|
|
9
|
+
import { SkillService } from '../../skills/skills.service.js';
|
|
10
|
+
import { ToolManualService } from '../../tool-manuals/tool-manuals.service.js';
|
|
11
|
+
import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
|
|
12
|
+
import { PluginIndexService } from '../plugins.service.js';
|
|
13
|
+
import { PluginLinkIndex } from '../plugin-links.js';
|
|
14
|
+
import { PluginLinksService } from '../plugin-links.service.js';
|
|
15
|
+
import { KbPluginSource } from '../discovery/kb-plugin-source.js';
|
|
16
|
+
import { MarketplaceCompilerService } from '../compile/marketplace-compiler.service.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A customer's repository, read as-is: lowercase `skills/` and `plugins/`
|
|
20
|
+
* roots, bundles at the depths their org uses, a registry with a profile
|
|
21
|
+
* chain, and a committed `dist/` beside the source. Every consumer — catalog,
|
|
22
|
+
* plugin index, link index, tool manuals, compiler — must read the SOURCE
|
|
23
|
+
* trees and agree on what a plugin holds, with nothing written back.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const KB_DIR = 'knowledge-base';
|
|
27
|
+
const wsId = workspaceIdForBranch(DEFAULT_BRANCH);
|
|
28
|
+
|
|
29
|
+
describe('bundle dialect, end to end', () => {
|
|
30
|
+
let root: string;
|
|
31
|
+
let repo: string;
|
|
32
|
+
let access: AccessControlService;
|
|
33
|
+
let skills: SkillService;
|
|
34
|
+
let tools: ToolManualService;
|
|
35
|
+
let links: PluginLinkIndex;
|
|
36
|
+
let index: PluginIndexService;
|
|
37
|
+
let compiler: MarketplaceCompilerService;
|
|
38
|
+
let linkService: PluginLinksService;
|
|
39
|
+
|
|
40
|
+
const write = async (rel: string, text: string) => {
|
|
41
|
+
const abs = path.join(repo, rel);
|
|
42
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
43
|
+
await fs.writeFile(abs, text);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
configureKbLayout({ knowledgeBaseDir: 'docs', skillsDir: 'skills', pluginsDir: 'plugins' });
|
|
48
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-dialect-'));
|
|
49
|
+
repo = path.join(root, wsId, KB_DIR);
|
|
50
|
+
const workspaceService = {
|
|
51
|
+
getOrCreateForBranch: async () => ({ id: wsId }),
|
|
52
|
+
getWorkspacePath: async (id: string) => path.join(root, id),
|
|
53
|
+
readFile: async (id: string, rel: string) => fs.readFile(path.join(root, id, rel), 'utf-8'),
|
|
54
|
+
ensureRemotesFetched: async () => undefined,
|
|
55
|
+
} as unknown as WorkspaceService;
|
|
56
|
+
|
|
57
|
+
await write('roles.yaml', 'roles:\n Admin:\n - admin@x.io\n');
|
|
58
|
+
// Their scopes decide readability; everything here is open to signed-in people.
|
|
59
|
+
await write('access.md', '---\nwrite:\n - Admin\n---\nread:\n - everyone\n');
|
|
60
|
+
await write(
|
|
61
|
+
'configs/mcp/registry.json',
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
servers: [
|
|
64
|
+
{ id: 'jira', config: { command: 'npx', args: ['-y', 'jira-mcp'] } },
|
|
65
|
+
{ id: 'confluence', config: { type: 'http', url: 'https://mcp.confluence.example' } },
|
|
66
|
+
],
|
|
67
|
+
profiles: [{ id: 'base', servers: ['jira'] }, { id: 'global', servers: ['confluence'], extends: 'base' }],
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
// Skills at their four depths; a build output that must be ignored.
|
|
71
|
+
await write('skills/functional/cluster-a/one-skill/SKILL.md', '---\nname: one-skill\ndescription: One.\nmetadata:\n version: "1.4.0"\n author: "acme"\n---\n');
|
|
72
|
+
await write('skills/departments/business/finance/close-books/SKILL.md', '---\ndescription: Close.\n---\n');
|
|
73
|
+
await write('skills/departments/engineering/shared/cluster-a/deploy/SKILL.md', '---\ndescription: Deploy.\n---\n');
|
|
74
|
+
await write('skills/departments/engineering/shared/cluster-a/rollback/SKILL.md', '---\ndescription: Undo.\n---\n');
|
|
75
|
+
await write('skills/departments/engineering/team-x/team-only/SKILL.md', '---\ndescription: Ours.\n---\n');
|
|
76
|
+
await write('dist/docs/plugins/functional/example/skills/deploy/SKILL.md', '---\ndescription: compiled copy\n---\n');
|
|
77
|
+
// Plugins: one links a cluster + a single skill; one links a department.
|
|
78
|
+
await write(
|
|
79
|
+
'plugins/functional/cluster-a/example-plugin/plugin.bundle.json',
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
name: 'example-plugin',
|
|
82
|
+
version: '1.3.1',
|
|
83
|
+
description: 'Example',
|
|
84
|
+
mcpProfile: 'global',
|
|
85
|
+
interface: { displayName: 'Example Plugin' },
|
|
86
|
+
sourceSkillRoots: ['skills/departments/engineering/shared/cluster-a', 'skills/functional/cluster-a/one-skill'],
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
await write(
|
|
90
|
+
'plugins/departments/business/finance/finance-kit/plugin.bundle.json',
|
|
91
|
+
JSON.stringify({ name: 'finance-kit', sourceSkillRoots: ['skills/departments/business/finance'] }),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const source = new KbPluginSource();
|
|
95
|
+
access = new AccessControlService(workspaceService, KB_DIR);
|
|
96
|
+
skills = new SkillService(workspaceService, access, KB_DIR);
|
|
97
|
+
tools = new ToolManualService(workspaceService, access, KB_DIR, Date.now, source);
|
|
98
|
+
links = new PluginLinkIndex(workspaceService, skills, access, KB_DIR, Date.now, source);
|
|
99
|
+
index = new PluginIndexService(workspaceService, access, skills, tools, KB_DIR, Date.now, links, source);
|
|
100
|
+
compiler = new MarketplaceCompilerService(workspaceService, access, skills, links, KB_DIR, { name: 'acme', owner: 'Acme' }, source);
|
|
101
|
+
linkService = new PluginLinksService(workspaceService, { runPendingCommit: async () => undefined }, access, skills, links, KB_DIR);
|
|
102
|
+
});
|
|
103
|
+
afterEach(async () => {
|
|
104
|
+
configureKbLayout({ ...DEFAULT_KB_LAYOUT });
|
|
105
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('reads the source trees: skills at any depth, plugins as skill lists, servers from the registry', async () => {
|
|
109
|
+
const catalog = await skills.listSkills();
|
|
110
|
+
expect(catalog.map((s) => s.name).sort()).toEqual(['close-books', 'deploy', 'one-skill', 'rollback', 'team-only']);
|
|
111
|
+
expect(catalog.find((s) => s.name === 'one-skill')?.version).toBe('1.4.0');
|
|
112
|
+
expect(catalog.some((s) => s.path.startsWith('dist/'))).toBe(false);
|
|
113
|
+
|
|
114
|
+
const membership = await links.membership();
|
|
115
|
+
expect(membership.byPlugin.get('example-plugin')?.linkedSkills.sort()).toEqual([
|
|
116
|
+
'skills/departments/engineering/shared/cluster-a/deploy',
|
|
117
|
+
'skills/departments/engineering/shared/cluster-a/rollback',
|
|
118
|
+
'skills/functional/cluster-a/one-skill',
|
|
119
|
+
]);
|
|
120
|
+
expect(membership.byPlugin.get('example-plugin')?.linksAreManaged).toBe(false);
|
|
121
|
+
expect(membership.bySkill.get('skills/departments/business/finance/close-books')).toEqual([
|
|
122
|
+
{ name: 'finance-kit', linked: true, granted: true },
|
|
123
|
+
]);
|
|
124
|
+
// A standalone skill is in no plugin, and that is a normal state.
|
|
125
|
+
expect(membership.bySkill.get('skills/departments/engineering/team-x/team-only')).toBeUndefined();
|
|
126
|
+
|
|
127
|
+
const plugins = await index.catalog();
|
|
128
|
+
expect(plugins.map((p) => [p.name, p.skillCount, p.toolCount])).toEqual([
|
|
129
|
+
['example-plugin', 3, 2],
|
|
130
|
+
['finance-kit', 1, 0],
|
|
131
|
+
]);
|
|
132
|
+
expect(plugins[0].folders).toEqual(['plugins/functional/cluster-a/example-plugin']);
|
|
133
|
+
|
|
134
|
+
const manuals = await tools.listAllSummaries();
|
|
135
|
+
expect(manuals.map((m) => m.name).sort()).toEqual(['confluence', 'jira']);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('compiles a marketplace from the source trees, per caller', async () => {
|
|
139
|
+
const tree = await compiler.compileFor({ userEmail: 'someone@x.io' });
|
|
140
|
+
const paths = [...tree.files.keys()];
|
|
141
|
+
expect(paths).toContain('plugins/example-plugin/skills/deploy/SKILL.md');
|
|
142
|
+
expect(paths).toContain('plugins/example-plugin/skills/one-skill/SKILL.md');
|
|
143
|
+
expect(paths).toContain('plugins/finance-kit/skills/close-books/SKILL.md');
|
|
144
|
+
// The standalone team skill rides in the leftovers plugin; linked ones do not repeat there.
|
|
145
|
+
expect(paths).toContain('plugins/skills-and-knowledge/skills/team-only/SKILL.md');
|
|
146
|
+
expect(paths).not.toContain('plugins/skills-and-knowledge/skills/deploy/SKILL.md');
|
|
147
|
+
const manifest = JSON.parse(tree.files.get('plugins/example-plugin/plugin.json')!.toString());
|
|
148
|
+
expect(manifest).toMatchObject({ name: 'example-plugin', version: '1.3.1', description: 'Example' });
|
|
149
|
+
const mcp = JSON.parse(tree.files.get('plugins/example-plugin/.mcp.json')!.toString());
|
|
150
|
+
expect(Object.keys(mcp.mcpServers).sort()).toEqual(['confluence', 'jira']);
|
|
151
|
+
expect(paths.some((p) => p.includes('dist/'))).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('never writes into the dialect: linking through hexis is refused as read-only', async () => {
|
|
155
|
+
const user = { id: 'u1', email: 'admin@x.io', name: 'Admin' };
|
|
156
|
+
await expect(linkService.link(user, 'example-plugin', 'skills/departments/engineering/team-x/team-only')).rejects.toMatchObject({
|
|
157
|
+
status: 409,
|
|
158
|
+
payload: { kind: 'read-only-links' },
|
|
159
|
+
});
|
|
160
|
+
const before = await fs.readFile(path.join(repo, 'plugins/functional/cluster-a/example-plugin/plugin.bundle.json'), 'utf-8');
|
|
161
|
+
expect(JSON.parse(before).sourceSkillRoots).toHaveLength(2);
|
|
162
|
+
});
|
|
163
|
+
});
|