@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,363 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_BRANCH,
|
|
5
|
+
PLUGIN_MANIFEST_FILE,
|
|
6
|
+
linkedSkillRoots,
|
|
7
|
+
normalizeSkillRoot,
|
|
8
|
+
pluginManifestName,
|
|
9
|
+
renderPluginManifest,
|
|
10
|
+
skillUnderRoot,
|
|
11
|
+
withLinkedSkillRoots,
|
|
12
|
+
type AuthUser,
|
|
13
|
+
} from '@bevel-software/platform-shared';
|
|
14
|
+
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
15
|
+
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
16
|
+
import { AccessMutationService, accessMdPathForFolder } from '../access/access-mutation.service.js';
|
|
17
|
+
import { pluginPrincipalKey } from '../access-model/access-grammar.js';
|
|
18
|
+
import type { Principal } from '../access-model/access-splice.js';
|
|
19
|
+
import { WorkspaceMutex } from '../kb-fs/mutex.js';
|
|
20
|
+
import type { ISkillService } from '../skills/skills.contract.js';
|
|
21
|
+
import type { ProvisionCommitDriver } from './plugin-provision.service.js';
|
|
22
|
+
import { linksWorkspaceId, type PluginLinkIndex } from './plugin-links.js';
|
|
23
|
+
|
|
24
|
+
/** A refusal the route passes through: message + HTTP status + a machine-readable kind. */
|
|
25
|
+
export class PluginLinkError extends Error {
|
|
26
|
+
constructor(
|
|
27
|
+
message: string,
|
|
28
|
+
readonly status: number,
|
|
29
|
+
readonly payload: Record<string, unknown> = {},
|
|
30
|
+
) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'PluginLinkError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Linking shared skills into plugins — the writer behind "add an existing
|
|
38
|
+
* skill to this plugin".
|
|
39
|
+
*
|
|
40
|
+
* A link is TWO edits that only make sense together:
|
|
41
|
+
*
|
|
42
|
+
* 1. the skill's root goes into the plugin manifest's
|
|
43
|
+
* `extensions["software.bevel.hexis"].skills` list (see
|
|
44
|
+
* `HEXIS_LINKED_SKILLS_KEY`), which is what the catalog, the plugin page
|
|
45
|
+
* and the compiled distribution read;
|
|
46
|
+
* 2. the skill folder's own `access.md` grants `read` to
|
|
47
|
+
* `plugin/<Name>/read` and `write` to `plugin/<Name>/write`, which is
|
|
48
|
+
* what lets the plugin's members actually read it — ownership decides,
|
|
49
|
+
* the plugin is a view (see `plugin-principals.ts`).
|
|
50
|
+
*
|
|
51
|
+
* Which is why linking needs write on BOTH sides: on the plugin folder (to
|
|
52
|
+
* edit its manifest) and on the skill's root (to edit its access rules). A
|
|
53
|
+
* manager who lacks the second gets a 409 the UI turns into "request write
|
|
54
|
+
* access", not a link that silently shares nothing.
|
|
55
|
+
*
|
|
56
|
+
* Unlinking removes the manifest entry, and revokes the two tokens only when
|
|
57
|
+
* the actor may edit the skill's rules — otherwise the grant stays, and the
|
|
58
|
+
* skill page says so, for a skill editor to remove.
|
|
59
|
+
*
|
|
60
|
+
* Repair re-grants the tokens for a link that exists but whose grant was
|
|
61
|
+
* hand-removed: the amber dot's one action.
|
|
62
|
+
*
|
|
63
|
+
* Every operation is serialised on BOTH sides it writes — the plugin's
|
|
64
|
+
* manifest slug (the same key provisioning locks on) and the skill root whose
|
|
65
|
+
* access.md carries the grant — and lands as ordinary default-branch commits
|
|
66
|
+
* through the pending-commit driver, so the write gate and the per-user push
|
|
67
|
+
* gate apply exactly as they do to any edit.
|
|
68
|
+
*/
|
|
69
|
+
export class PluginLinksService {
|
|
70
|
+
private readonly locks = new WorkspaceMutex();
|
|
71
|
+
private readonly mutation: AccessMutationService;
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
private readonly workspaceService: WorkspaceService,
|
|
75
|
+
private readonly commits: ProvisionCommitDriver,
|
|
76
|
+
private readonly accessControl: IAccessControl,
|
|
77
|
+
private readonly skillService: ISkillService,
|
|
78
|
+
private readonly links: PluginLinkIndex,
|
|
79
|
+
private readonly kbDirName: string,
|
|
80
|
+
private readonly events?: {
|
|
81
|
+
emit(event: { kind: 'fs-tree-changed'; workspaceId: string; branch: string }): void;
|
|
82
|
+
},
|
|
83
|
+
private readonly onChanged?: () => void,
|
|
84
|
+
) {
|
|
85
|
+
this.mutation = new AccessMutationService(workspaceService, accessControl, kbDirName);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async link(user: AuthUser, plugin: string, rawRoot: string): Promise<{ root: string; skills: string[] }> {
|
|
89
|
+
const folder = await this.pluginFolder(plugin);
|
|
90
|
+
const root = this.rootOrThrow(rawRoot);
|
|
91
|
+
const wsId = linksWorkspaceId();
|
|
92
|
+
await this.requirePluginWrite(wsId, user, folder);
|
|
93
|
+
const skills = await this.resolvedSkills(root);
|
|
94
|
+
if (skills.length === 0) {
|
|
95
|
+
throw new PluginLinkError(
|
|
96
|
+
`"${root}" holds no released skill. Link a skill folder, or a folder that contains skills.`,
|
|
97
|
+
422,
|
|
98
|
+
{ kind: 'no-skills', root },
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (!(await this.accessControl.canWrite(wsId, user.email, accessMdPathForFolder(root)))) {
|
|
102
|
+
// The manager may edit the plugin but not the skill's rules — the link
|
|
103
|
+
// would share nothing. The UI offers the request path on this shape.
|
|
104
|
+
throw new PluginLinkError(
|
|
105
|
+
`You can't change who may read "${root}". Ask its editors for write access first.`,
|
|
106
|
+
409,
|
|
107
|
+
{ kind: 'needs-skill-write', root },
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return this.locks.runAll(this.lockKeys(folder, root), async () => {
|
|
111
|
+
const { manifest, manifestRel } = await this.readManifest(wsId, folder);
|
|
112
|
+
const roots = linkedSkillRoots(manifest);
|
|
113
|
+
// Manifest FIRST, grant second — two commits, deliberately in this
|
|
114
|
+
// order. The two files live in different folders and the commit
|
|
115
|
+
// pipeline is path-scoped, so there is no single commit to be had; what
|
|
116
|
+
// can be chosen is which half-state a failure leaves. Listed-but-not-
|
|
117
|
+
// shared is the visible one: the link index reports the missing grant,
|
|
118
|
+
// the card wears the amber dot, and Repair finishes the job. The other
|
|
119
|
+
// order would leave shared-but-not-listed — an over-share nothing
|
|
120
|
+
// surfaces.
|
|
121
|
+
if (!roots.includes(root)) {
|
|
122
|
+
await this.workspaceService.writeFile(
|
|
123
|
+
wsId,
|
|
124
|
+
manifestRel,
|
|
125
|
+
`${JSON.stringify(withLinkedSkillRoots(manifest, [...roots, root]), null, 2)}\n`,
|
|
126
|
+
);
|
|
127
|
+
await this.commits.runPendingCommit(wsId, DEFAULT_BRANCH, manifestRel, user);
|
|
128
|
+
}
|
|
129
|
+
await this.grantTokens(wsId, user, folder, root);
|
|
130
|
+
this.changed(wsId);
|
|
131
|
+
return { root, skills };
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async unlink(user: AuthUser, plugin: string, rawRoot: string): Promise<{ root: string; revoked: boolean }> {
|
|
136
|
+
const folder = await this.pluginFolder(plugin);
|
|
137
|
+
const root = this.rootOrThrow(rawRoot);
|
|
138
|
+
const wsId = linksWorkspaceId();
|
|
139
|
+
await this.requirePluginWrite(wsId, user, folder);
|
|
140
|
+
return this.locks.runAll(this.lockKeys(folder, root), async () => {
|
|
141
|
+
const { manifest, manifestRel } = await this.readManifest(wsId, folder);
|
|
142
|
+
const roots = linkedSkillRoots(manifest);
|
|
143
|
+
if (!roots.includes(root)) {
|
|
144
|
+
throw new PluginLinkError(`"${root}" is not linked into ${folder}.`, 404, { kind: 'not-linked', root });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Revoke FIRST, manifest second — the mirror of `link`'s ordering and
|
|
148
|
+
// for the same reason: a failure between the two must leave the
|
|
149
|
+
// visible half-state (listed, no grant → amber dot, Repair), never the
|
|
150
|
+
// silent one (unlisted, still shared). Revoke only where the actor may
|
|
151
|
+
// edit the skill's rules; otherwise the grant stays for a skill editor
|
|
152
|
+
// to remove — never a silent no-op that pretends it happened.
|
|
153
|
+
let revoked = false;
|
|
154
|
+
if (await this.accessControl.canWrite(wsId, user.email, accessMdPathForFolder(root))) {
|
|
155
|
+
let changed = false;
|
|
156
|
+
for (const principal of this.tokens(folder)) {
|
|
157
|
+
const r = await this.mutation.revoke(wsId, 'folder', root, principal, user.email, undefined, {
|
|
158
|
+
tokenMatch: 'exact',
|
|
159
|
+
});
|
|
160
|
+
changed ||= r.changed;
|
|
161
|
+
}
|
|
162
|
+
if (changed) {
|
|
163
|
+
await this.commits.runPendingCommit(
|
|
164
|
+
wsId,
|
|
165
|
+
DEFAULT_BRANCH,
|
|
166
|
+
`${this.kbDirName}/${accessMdPathForFolder(root)}`,
|
|
167
|
+
user,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
revoked = true;
|
|
171
|
+
}
|
|
172
|
+
await this.workspaceService.writeFile(
|
|
173
|
+
wsId,
|
|
174
|
+
manifestRel,
|
|
175
|
+
`${JSON.stringify(withLinkedSkillRoots(manifest, roots.filter((r) => r !== root)), null, 2)}\n`,
|
|
176
|
+
);
|
|
177
|
+
await this.commits.runPendingCommit(wsId, DEFAULT_BRANCH, manifestRel, user);
|
|
178
|
+
this.changed(wsId);
|
|
179
|
+
return { root, revoked };
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async repair(user: AuthUser, plugin: string, rawRoot: string): Promise<{ root: string }> {
|
|
184
|
+
const folder = await this.pluginFolder(plugin);
|
|
185
|
+
const root = this.rootOrThrow(rawRoot);
|
|
186
|
+
const wsId = linksWorkspaceId();
|
|
187
|
+
// Everything under the lock, the manifest read included: a repair that
|
|
188
|
+
// checked the link and then waited on an unlink would re-grant a root the
|
|
189
|
+
// manifest no longer names.
|
|
190
|
+
return this.locks.runAll(this.lockKeys(folder, root), async () => {
|
|
191
|
+
const { manifest } = await this.readManifest(wsId, folder);
|
|
192
|
+
if (!linkedSkillRoots(manifest).includes(root)) {
|
|
193
|
+
throw new PluginLinkError(`"${root}" is not linked into ${folder}.`, 404, { kind: 'not-linked', root });
|
|
194
|
+
}
|
|
195
|
+
if (!(await this.accessControl.canWrite(wsId, user.email, accessMdPathForFolder(root)))) {
|
|
196
|
+
throw new PluginLinkError(`You can't change who may read "${root}".`, 403, {
|
|
197
|
+
kind: 'needs-skill-write',
|
|
198
|
+
root,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// The same retired-skill rule as `link`: a repair re-grants on the root.
|
|
202
|
+
await this.resolvedSkills(root);
|
|
203
|
+
await this.grantTokens(wsId, user, folder, root);
|
|
204
|
+
this.changed(wsId);
|
|
205
|
+
return { root };
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- internal --------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* ONE reservation for both sides of a link: the plugin's manifest AND the
|
|
213
|
+
* skill root's access.md. Two plugins linking the same root write the same
|
|
214
|
+
* file, and serialising on the plugin alone let them interleave, one splice
|
|
215
|
+
* overwriting the other's grant. `runAll` takes both keys atomically, so
|
|
216
|
+
* nothing nests and no ordering discipline is needed.
|
|
217
|
+
*/
|
|
218
|
+
private lockKeys(folder: string, root: string): string[] {
|
|
219
|
+
return [`plugin:${pluginManifestName(folder)}`, `root:${root}`];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The two grants a link carries: members read, managers write — written as
|
|
224
|
+
* the CANONICAL keys (slugged name), the spelling every comparison uses, so
|
|
225
|
+
* a repeated link finds its grant and an unlink finds what to revoke
|
|
226
|
+
* whatever the folder is called.
|
|
227
|
+
*/
|
|
228
|
+
private tokens(folder: string): Principal[] {
|
|
229
|
+
const slug = pluginManifestName(folder);
|
|
230
|
+
return [
|
|
231
|
+
{ kind: 'role', role: pluginPrincipalKey(slug, 'read') },
|
|
232
|
+
{ kind: 'role', role: pluginPrincipalKey(slug, 'write') },
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private async grantTokens(wsId: string, user: AuthUser, folder: string, root: string): Promise<void> {
|
|
237
|
+
const [read, write] = this.tokens(folder);
|
|
238
|
+
const a = await this.mutation.grant(wsId, 'folder', root, 'read', read);
|
|
239
|
+
const b = await this.mutation.grant(wsId, 'folder', root, 'write', write);
|
|
240
|
+
if (a.changed || b.changed) {
|
|
241
|
+
await this.commits.runPendingCommit(wsId, DEFAULT_BRANCH, `${this.kbDirName}/${a.editPath}`, user);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private rootOrThrow(raw: string): string {
|
|
246
|
+
const root = normalizeSkillRoot(raw);
|
|
247
|
+
if (root === null) {
|
|
248
|
+
throw new PluginLinkError('A skill path must be a repo-relative folder path.', 422, { kind: 'bad-root' });
|
|
249
|
+
}
|
|
250
|
+
return root;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Released skills under the root — the catalog's answer, not the file
|
|
255
|
+
* system's. A RETIRED skill is kept for its owners and never shared onward:
|
|
256
|
+
* the grant lands on the ROOT folder, so a root that holds a retired skill
|
|
257
|
+
* anywhere beneath it cannot be linked at all — the grant would reach the
|
|
258
|
+
* retired one too. The refusal names them so the manager can link the
|
|
259
|
+
* active skills individually instead.
|
|
260
|
+
*/
|
|
261
|
+
private async resolvedSkills(root: string): Promise<string[]> {
|
|
262
|
+
const under = (await this.skillService.listSkills(undefined)).filter((s) => skillUnderRoot(s.path, root));
|
|
263
|
+
const retired = under.filter((s) => s.lifecycle === 'retired').map((s) => s.path);
|
|
264
|
+
if (retired.length > 0) {
|
|
265
|
+
throw new PluginLinkError(
|
|
266
|
+
`"${root}" holds retired skills (${retired.join(', ')}); a retired skill is never shared onward. Link the active skills one by one.`,
|
|
267
|
+
422,
|
|
268
|
+
{ kind: 'retired-skills', root, retired },
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return under.map((s) => s.path);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The exact on-disk plugin folder for `name`, or 404. Personal folders are
|
|
276
|
+
* not plugins, but that is the MEMBERSHIP's verdict (discovery marks the
|
|
277
|
+
* `personal-*` prefix personal only directly under the root), not a rule
|
|
278
|
+
* on the name — a nested plugin may be called anything.
|
|
279
|
+
*/
|
|
280
|
+
private async pluginFolder(name: string): Promise<string> {
|
|
281
|
+
const trimmed = name.trim();
|
|
282
|
+
if (!trimmed || trimmed.includes('/') || trimmed.includes('\\')) {
|
|
283
|
+
throw new PluginLinkError('Unknown plugin', 404, { kind: 'unknown-plugin' });
|
|
284
|
+
}
|
|
285
|
+
const membership = await this.links.membership();
|
|
286
|
+
const links = membership.byPlugin.get(trimmed);
|
|
287
|
+
if (!links) {
|
|
288
|
+
throw new PluginLinkError('Unknown plugin', 404, { kind: 'unknown-plugin' });
|
|
289
|
+
}
|
|
290
|
+
if (!links.linksAreManaged) {
|
|
291
|
+
// A dialect plugin's links live in a file this platform does not write.
|
|
292
|
+
throw new PluginLinkError(
|
|
293
|
+
`${trimmed} is read from an external plugin format; edit its links in that repository.`,
|
|
294
|
+
409,
|
|
295
|
+
{ kind: 'read-only-links' },
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return trimmed;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** The repo-relative folder of a known plugin (any depth). */
|
|
302
|
+
private async folderOf(plugin: string): Promise<string> {
|
|
303
|
+
const links = (await this.links.membership()).byPlugin.get(plugin);
|
|
304
|
+
if (!links) throw new PluginLinkError('Unknown plugin', 404, { kind: 'unknown-plugin' });
|
|
305
|
+
return links.folder;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async requirePluginWrite(wsId: string, user: AuthUser, plugin: string): Promise<void> {
|
|
309
|
+
if (!(await this.accessControl.canWrite(wsId, user.email, await this.folderOf(plugin)))) {
|
|
310
|
+
// Same answer as an unknown plugin, so probing confirms nothing.
|
|
311
|
+
throw new PluginLinkError('Unknown plugin', 404, { kind: 'unknown-plugin' });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private async readManifest(
|
|
316
|
+
wsId: string,
|
|
317
|
+
plugin: string,
|
|
318
|
+
): Promise<{ manifest: Record<string, unknown>; manifestRel: string }> {
|
|
319
|
+
const folder = await this.folderOf(plugin);
|
|
320
|
+
const manifestRel = `${this.kbDirName}/${folder}/${PLUGIN_MANIFEST_FILE}`;
|
|
321
|
+
const abs = path.join(await this.workspaceService.getWorkspacePath(wsId), manifestRel);
|
|
322
|
+
let manifest: Record<string, unknown> | null = null;
|
|
323
|
+
let text: string | null = null;
|
|
324
|
+
try {
|
|
325
|
+
text = await fs.readFile(abs, 'utf-8');
|
|
326
|
+
} catch (err) {
|
|
327
|
+
// Only a PROVEN absence gets the fresh minimal manifest a pre-manifest
|
|
328
|
+
// folder deserves. Any other failure must surface: the caller writes the
|
|
329
|
+
// manifest back, and a skeleton over a real one would erase its
|
|
330
|
+
// version, description and MCP extension block.
|
|
331
|
+
const code = (err as { code?: unknown } | null)?.code;
|
|
332
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR') throw err;
|
|
333
|
+
}
|
|
334
|
+
if (text !== null) {
|
|
335
|
+
try {
|
|
336
|
+
const parsed: unknown = JSON.parse(text);
|
|
337
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
338
|
+
manifest = parsed as Record<string, unknown>;
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
/* unparsable */
|
|
342
|
+
}
|
|
343
|
+
if (manifest === null) {
|
|
344
|
+
throw new PluginLinkError(
|
|
345
|
+
`${manifestRel} is not a JSON object; fix the manifest before changing its links.`,
|
|
346
|
+
422,
|
|
347
|
+
{ kind: 'bad-manifest' },
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
manifest: manifest ?? (JSON.parse(renderPluginManifest(path.posix.basename(folder))) as Record<string, unknown>),
|
|
353
|
+
manifestRel,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private changed(wsId: string): void {
|
|
358
|
+
this.links.invalidate();
|
|
359
|
+
this.accessControl.invalidate(wsId);
|
|
360
|
+
this.onChanged?.();
|
|
361
|
+
this.events?.emit({ kind: 'fs-tree-changed', workspaceId: wsId, branch: DEFAULT_BRANCH });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { DEFAULT_BRANCH, pluginManifestName, skillUnderRoot } from '@bevel-software/platform-shared';
|
|
3
|
+
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
4
|
+
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
5
|
+
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
6
|
+
import type { ISkillService } from '../skills/skills.contract.js';
|
|
7
|
+
import { TtlCache } from '../../shared/ttl-cache.js';
|
|
8
|
+
import { canonicalRoleName, pluginPrincipalKey } from '../access-model/access-grammar.js';
|
|
9
|
+
import type { PluginMembership } from './plugins.contract.js';
|
|
10
|
+
import type { PluginSource } from './discovery/plugin-source.js';
|
|
11
|
+
import { KbPluginSource } from './discovery/kb-plugin-source.js';
|
|
12
|
+
|
|
13
|
+
const CACHE_TTL_MS = 60_000;
|
|
14
|
+
|
|
15
|
+
/** One plugin's links, resolved against the released catalog. */
|
|
16
|
+
export interface PluginLinks {
|
|
17
|
+
/** The plugin's name — its identity to people and URLs. */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Repo-relative plugin folder. */
|
|
20
|
+
folder: string;
|
|
21
|
+
/** The roots the manifest declares, normalised. */
|
|
22
|
+
roots: string[];
|
|
23
|
+
/** Skill folders the roots resolve to (released catalog only). */
|
|
24
|
+
linkedSkills: string[];
|
|
25
|
+
/** Roots that resolve to no released skill — a typo, or a skill not yet merged. */
|
|
26
|
+
unresolvedRoots: string[];
|
|
27
|
+
/** Whether hexis writes these links and checks their grants — see `DiscoveredPlugin`. */
|
|
28
|
+
linksAreManaged: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface LinkMembership {
|
|
32
|
+
/** skill path → every plugin that holds it, inline or by link. */
|
|
33
|
+
bySkill: Map<string, PluginMembership[]>;
|
|
34
|
+
/** plugin folder → its links. Every plugin folder appears, linked or not. */
|
|
35
|
+
byPlugin: Map<string, PluginLinks>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The link index: which shared skills each plugin links, and — the other way
|
|
40
|
+
* round — which plugins each skill belongs to, whether by sitting inside the
|
|
41
|
+
* plugin folder or by being linked from its manifest.
|
|
42
|
+
*
|
|
43
|
+
* Plugins come from the configured {@link PluginSource} (native manifests, or
|
|
44
|
+
* a customer dialect); resolution runs against the RELEASED catalog
|
|
45
|
+
* (`skillService.listSkills()`, unfiltered), never the file system: the
|
|
46
|
+
* catalog already applies the leaf-folder rule, the `.bevelignore` layers
|
|
47
|
+
* and the global-name dedup, and a second walk here would be a second
|
|
48
|
+
* opinion about what a skill is.
|
|
49
|
+
*
|
|
50
|
+
* `granted` is the consistency check the amber dot renders: a linked skill
|
|
51
|
+
* whose own access rules DO grant the plugin's `plugin/<Name>/read`
|
|
52
|
+
* principal (directly or from a scope above it). The link service writes
|
|
53
|
+
* that grant with the link; only a hand edit takes it away.
|
|
54
|
+
*
|
|
55
|
+
* Cached briefly, dropped by `invalidate()` from the same file-change
|
|
56
|
+
* subscriber that drops the plugin index, so a link committed on the default
|
|
57
|
+
* branch shows within one round-trip.
|
|
58
|
+
*/
|
|
59
|
+
export class PluginLinkIndex {
|
|
60
|
+
private readonly cache: TtlCache<LinkMembership>;
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
private readonly workspaceService: WorkspaceService,
|
|
64
|
+
private readonly skillService: ISkillService,
|
|
65
|
+
private readonly accessControl: IAccessControl,
|
|
66
|
+
private readonly kbDirName: string,
|
|
67
|
+
now: () => number = Date.now,
|
|
68
|
+
private readonly source: PluginSource = new KbPluginSource(),
|
|
69
|
+
) {
|
|
70
|
+
this.cache = new TtlCache(CACHE_TTL_MS, now);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
invalidate(): void {
|
|
74
|
+
this.cache.invalidate();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async membership(): Promise<LinkMembership> {
|
|
78
|
+
const cached = this.cache.get();
|
|
79
|
+
if (cached) return cached;
|
|
80
|
+
// Token first, like the other catalogs: an `invalidate()` landing while
|
|
81
|
+
// the build reads the pre-change tree must not be undone by storing that
|
|
82
|
+
// read afterwards.
|
|
83
|
+
const token = this.cache.begin();
|
|
84
|
+
const built = await this.build();
|
|
85
|
+
// A degraded read (no workspace yet) is served but not stored — the same
|
|
86
|
+
// reasoning as the plugin index: caching a failure hides every link for
|
|
87
|
+
// a full TTL after its cause is gone.
|
|
88
|
+
if (built === null) return { bySkill: new Map(), byPlugin: new Map() };
|
|
89
|
+
this.cache.set(built, token);
|
|
90
|
+
return built;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Convenience: the memberships of one skill (empty when it is in no plugin). */
|
|
94
|
+
async pluginsOf(skillPath: string): Promise<PluginMembership[]> {
|
|
95
|
+
return (await this.membership()).bySkill.get(skillPath) ?? [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- internal --------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
private async build(): Promise<LinkMembership | null> {
|
|
101
|
+
let wsId: string;
|
|
102
|
+
let kbRoot: string;
|
|
103
|
+
try {
|
|
104
|
+
wsId = (await this.workspaceService.getOrCreateForBranch(DEFAULT_BRANCH)).id;
|
|
105
|
+
kbRoot = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName);
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const skills = await this.skillService.listSkills(undefined);
|
|
110
|
+
const bySkill = new Map<string, PluginMembership[]>();
|
|
111
|
+
const byPlugin = new Map<string, PluginLinks>();
|
|
112
|
+
const add = (skillPath: string, m: PluginMembership) => {
|
|
113
|
+
const list = bySkill.get(skillPath) ?? [];
|
|
114
|
+
if (!list.some((x) => x.name === m.name)) list.push(m);
|
|
115
|
+
bySkill.set(skillPath, list);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const discovered = await this.source.discover(kbRoot);
|
|
119
|
+
for (const w of discovered.warnings) console.warn(`[plugins] ${w}`);
|
|
120
|
+
|
|
121
|
+
// Inline skills: the ones sitting INSIDE a plugin's folder, matched by
|
|
122
|
+
// folder prefix (a plugin may sit at any depth, so the second path
|
|
123
|
+
// segment says nothing). Personal folders are places, not plugins —
|
|
124
|
+
// their skills belong to nobody's plugin.
|
|
125
|
+
const ownerOf = (skillPath: string) =>
|
|
126
|
+
discovered.plugins.find((p) => !p.personal && p.exists && skillPath.startsWith(`${p.folder}/`));
|
|
127
|
+
for (const s of skills) {
|
|
128
|
+
const owner = ownerOf(s.path);
|
|
129
|
+
if (owner) add(s.path, { name: owner.name, linked: false, granted: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const plugin of discovered.plugins) {
|
|
133
|
+
// Personal folders are places, not plugins; a manifest without the
|
|
134
|
+
// access.md that makes a plugin EXIST is a ghost the index and the
|
|
135
|
+
// compiler omit, so it must not be linkable or hold grants either.
|
|
136
|
+
if (plugin.personal || !plugin.exists) continue;
|
|
137
|
+
const roots = plugin.linkedRoots;
|
|
138
|
+
const linkedSkills: string[] = [];
|
|
139
|
+
const unresolvedRoots: string[] = [];
|
|
140
|
+
for (const root of roots) {
|
|
141
|
+
const hits = skills.filter((s) => skillUnderRoot(s.path, root)).map((s) => s.path);
|
|
142
|
+
if (hits.length === 0) unresolvedRoots.push(root);
|
|
143
|
+
for (const hit of hits) if (!linkedSkills.includes(hit)) linkedSkills.push(hit);
|
|
144
|
+
}
|
|
145
|
+
byPlugin.set(plugin.name, {
|
|
146
|
+
name: plugin.name,
|
|
147
|
+
folder: plugin.folder,
|
|
148
|
+
roots,
|
|
149
|
+
linkedSkills,
|
|
150
|
+
unresolvedRoots,
|
|
151
|
+
linksAreManaged: plugin.linksAreManaged,
|
|
152
|
+
});
|
|
153
|
+
if (linkedSkills.length === 0) continue;
|
|
154
|
+
const token = pluginPrincipalKey(pluginManifestName(plugin.name), 'read');
|
|
155
|
+
for (const skillPath of linkedSkills) {
|
|
156
|
+
// A skill that also sits INSIDE this plugin folder is inline, and its
|
|
157
|
+
// inline membership already stands.
|
|
158
|
+
if (skillPath.startsWith(`${plugin.folder}/`)) continue;
|
|
159
|
+
// An unmanaged (dialect) link is a plain reference: nothing to grant,
|
|
160
|
+
// nothing to repair — the skill's own scope decides who reads it.
|
|
161
|
+
const granted = plugin.linksAreManaged ? await this.isGranted(wsId, skillPath, token) : true;
|
|
162
|
+
add(skillPath, { name: plugin.name, linked: true, granted });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { bySkill, byPlugin };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Whether the skill folder's effective readers include the plugin's read principal. */
|
|
169
|
+
private async isGranted(wsId: string, skillPath: string, token: string): Promise<boolean> {
|
|
170
|
+
try {
|
|
171
|
+
const readers = await this.accessControl.eligibleReaders(wsId, skillPath);
|
|
172
|
+
if (!readers.restricted) return true; // readable by everyone — the plugin's members included
|
|
173
|
+
// The resolver reports display names; compare through the one canonicaliser.
|
|
174
|
+
return (readers.principals ?? []).some((p) => p.kind === 'plugin' && canonicalRoleName(p.name) === token);
|
|
175
|
+
} catch {
|
|
176
|
+
return false; // fail closed: an unreadable tree reports the link as needing repair
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The default-branch workspace id every link resolution runs against. */
|
|
182
|
+
export function linksWorkspaceId(): string {
|
|
183
|
+
return workspaceIdForBranch(DEFAULT_BRANCH);
|
|
184
|
+
}
|