@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
|
@@ -31,8 +31,12 @@
|
|
|
31
31
|
* personal skill.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
+
import { randomUUID } from 'node:crypto';
|
|
34
35
|
import fs from 'node:fs/promises';
|
|
35
36
|
import path from 'node:path';
|
|
37
|
+
import { isAbsence } from '../../shared/fs-errors.js';
|
|
38
|
+
import type { Discovery, PluginSource } from './discovery/plugin-source.js';
|
|
39
|
+
import { KbPluginSource } from './discovery/kb-plugin-source.js';
|
|
36
40
|
|
|
37
41
|
import {
|
|
38
42
|
DEFAULT_BRANCH,
|
|
@@ -41,8 +45,9 @@ import {
|
|
|
41
45
|
pluginManifestName,
|
|
42
46
|
renderPluginManifest,
|
|
43
47
|
PERSONAL_PLUGIN_PREFIX,
|
|
44
|
-
|
|
48
|
+
isPersonalPluginDir,
|
|
45
49
|
personalPluginFolderName,
|
|
50
|
+
validateFilename,
|
|
46
51
|
type AuthUser,
|
|
47
52
|
} from '@bevel-software/platform-shared';
|
|
48
53
|
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
@@ -62,9 +67,40 @@ export interface ProvisionCommitDriver {
|
|
|
62
67
|
): Promise<void>;
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
/**
|
|
71
|
+
* A directory's entries, or null when there is no such directory. ONLY
|
|
72
|
+
* absence reads as "nothing there": a listing that fails for any other
|
|
73
|
+
* reason (permissions, I/O) throws, because "the folder is not there" and
|
|
74
|
+
* "the folder could not be read" must never collapse into one answer — the
|
|
75
|
+
* first is a 404 to a caller, the second an outage an operator must see.
|
|
76
|
+
*/
|
|
77
|
+
async function listDirOrIncomplete(
|
|
78
|
+
dir: string,
|
|
79
|
+
repoRel: string,
|
|
80
|
+
): Promise<Array<{ name: string; isDirectory(): boolean }> | null> {
|
|
81
|
+
try {
|
|
82
|
+
return await fs.readdir(dir, { withFileTypes: true });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (isAbsence(err)) return null;
|
|
85
|
+
// The same refusal a hole in discovery gets: this listing is one more
|
|
86
|
+
// read the operation needed and could not have.
|
|
87
|
+
throw incompleteDiscovery([repoRel]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The refusal for a discovery with a hole in it: a name cannot be checked, an identity cannot be known. */
|
|
92
|
+
function incompleteDiscovery(unreadable: string[]): PluginProvisionError {
|
|
93
|
+
return new PluginProvisionError(
|
|
94
|
+
`Some of the knowledge base could not be read (${unreadable.join(', ')}), so the plugin cannot be checked against every other. Try again, or ask an admin.`,
|
|
95
|
+
503,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
65
99
|
export interface ProvisionedPlugin {
|
|
66
100
|
/** The folder name under `Plugins/` (not the full path). */
|
|
67
101
|
folder: string;
|
|
102
|
+
/** The plugin's identity — the manifest name written for it. */
|
|
103
|
+
name: string;
|
|
68
104
|
/** False when an ensure found the folder already there. */
|
|
69
105
|
created: boolean;
|
|
70
106
|
}
|
|
@@ -97,6 +133,8 @@ export class PluginProvisionService {
|
|
|
97
133
|
private readonly accessControl: IAccessControl,
|
|
98
134
|
private readonly kbDirName: string,
|
|
99
135
|
private readonly events?: { emit(event: { kind: 'fs-tree-changed'; workspaceId: string; branch: string }): void },
|
|
136
|
+
/** Which names are TAKEN is discovery's answer — the same one every catalog gets. */
|
|
137
|
+
private readonly source: PluginSource = new KbPluginSource(),
|
|
100
138
|
) {}
|
|
101
139
|
|
|
102
140
|
/**
|
|
@@ -108,9 +146,9 @@ export class PluginProvisionService {
|
|
|
108
146
|
async createPlugin(user: AuthUser, rawName: string): Promise<ProvisionedPlugin> {
|
|
109
147
|
const name = rawName.trim();
|
|
110
148
|
if (!name) throw new PluginProvisionError('A plugin needs a name.', 422);
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
149
|
+
// NUL and control chars are exactly what a filesystem path cannot carry;
|
|
150
|
+
// refusing them here keeps the refusal a 422 instead of the fs layer's 500.
|
|
151
|
+
// eslint-disable-next-line no-control-regex
|
|
114
152
|
if (/[/\\\u0000-\u001f\u007f]/.test(name) || name === '.' || name === '..' || name.startsWith('.')) {
|
|
115
153
|
throw new PluginProvisionError(
|
|
116
154
|
'A plugin name can\'t contain / or \\ or control characters, or start with a dot.',
|
|
@@ -156,7 +194,7 @@ export class PluginProvisionService {
|
|
|
156
194
|
);
|
|
157
195
|
}
|
|
158
196
|
await this.provision(user, name, pluginAccessMd(user));
|
|
159
|
-
return { folder: name, created: true };
|
|
197
|
+
return { folder: name, name: pluginManifestName(name), created: true };
|
|
160
198
|
});
|
|
161
199
|
}
|
|
162
200
|
|
|
@@ -168,7 +206,7 @@ export class PluginProvisionService {
|
|
|
168
206
|
const folder = personalPluginFolderName(user.id);
|
|
169
207
|
return this.creations.run(`plugin:${pluginManifestName(folder)}`, async () => {
|
|
170
208
|
if ((await this.existingFolder(folder)) !== null) {
|
|
171
|
-
return { folder, created: false };
|
|
209
|
+
return { folder, name: pluginManifestName(folder), created: false };
|
|
172
210
|
}
|
|
173
211
|
try {
|
|
174
212
|
await this.provision(user, folder, personalAccessMd(user));
|
|
@@ -177,11 +215,11 @@ export class PluginProvisionService {
|
|
|
177
215
|
// process, a checkout that appeared between check and write): the
|
|
178
216
|
// folder existing is this method's success case, never its error.
|
|
179
217
|
if (err instanceof PluginProvisionError && err.status === 409) {
|
|
180
|
-
return { folder, created: false };
|
|
218
|
+
return { folder, name: pluginManifestName(folder), created: false };
|
|
181
219
|
}
|
|
182
220
|
throw err;
|
|
183
221
|
}
|
|
184
|
-
return { folder, created: true };
|
|
222
|
+
return { folder, name: pluginManifestName(folder), created: true };
|
|
185
223
|
});
|
|
186
224
|
}
|
|
187
225
|
|
|
@@ -204,29 +242,64 @@ export class PluginProvisionService {
|
|
|
204
242
|
* never interleave with a re-creation of the same name (the key derives
|
|
205
243
|
* from the name, so both spell it identically).
|
|
206
244
|
*/
|
|
207
|
-
async deletePlugin(user: AuthUser,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
245
|
+
async deletePlugin(user: AuthUser, rawFolder: string): Promise<void> {
|
|
246
|
+
// The folder's path BELOW the plugins root — `GTM`, or `teams/deep` for a
|
|
247
|
+
// plugin nested where discovery found it. Segments only: nothing that
|
|
248
|
+
// could climb out of the root, and no backslash — `/` is the one
|
|
249
|
+
// separator the repository speaks, and a `\` would read as a second one
|
|
250
|
+
// on Windows and as a name character everywhere else.
|
|
251
|
+
// Validated AS GIVEN, never trimmed first: a folder spelled with a
|
|
252
|
+
// leading or trailing space names no folder the catalog handed out, and
|
|
253
|
+
// trimming it would delete a different one. (`validateFilename` refuses
|
|
254
|
+
// the whitespace.)
|
|
255
|
+
const name = rawFolder;
|
|
256
|
+
const segments = name.split('/');
|
|
257
|
+
// Every segment must be a name the filesystem carries (the ONE rule
|
|
258
|
+
// creation applies, `validateFilename`: no control characters, no `\`,
|
|
259
|
+
// no `.`/`..`, no reserved names) and none may be dot-prefixed (a parked
|
|
260
|
+
// delete, invisible to every scanner) — refused here as a 422, never
|
|
261
|
+
// discovered by the fs layer as a 500.
|
|
262
|
+
if (!name || segments.some((s) => validateFilename(s) !== null || s.startsWith('.'))) {
|
|
263
|
+
throw new PluginProvisionError('A plugin needs a name.', 422);
|
|
264
|
+
}
|
|
265
|
+
if (isPersonalPluginDir(`${PLUGINS_DIR}/${name}`)) {
|
|
211
266
|
// Personal folders are not plugins (the catalog never lists them), and
|
|
212
267
|
// nobody deletes somebody's private shelf through the plugin door.
|
|
213
268
|
throw new PluginProvisionError('Unknown plugin', 404);
|
|
214
269
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
270
|
+
const wsId = await this.readyWorkspaceId();
|
|
271
|
+
const wsDir = await this.workspaceService.getWorkspacePath(wsId);
|
|
272
|
+
const pluginsDir = path.join(wsDir, this.kbDirName, PLUGINS_DIR);
|
|
273
|
+
const folderDir = path.join(pluginsDir, ...segments);
|
|
274
|
+
// Locked on the plugin's IDENTITY — the same key a creation of that name
|
|
275
|
+
// takes — not on a slug of the folder path, which for a nested plugin is a
|
|
276
|
+
// different string and would let a creation of the same identity run
|
|
277
|
+
// inside the delete's window. The identity is DISCOVERY's answer, in
|
|
278
|
+
// either file shape (a bundle's name as much as a manifest's), so delete
|
|
279
|
+
// and create can never key on two spellings of one plugin; a hole in
|
|
280
|
+
// discovery refuses, as it does for creation. A folder discovery does not
|
|
281
|
+
// list (no manifest at all) locks on its own slug — nothing else can
|
|
282
|
+
// claim that identity either.
|
|
283
|
+
const identity = await this.discoveredIdentity(`${PLUGINS_DIR}/${name}`, segments[segments.length - 1]!);
|
|
284
|
+
return this.creations.run(`plugin:${identity}`, async () => {
|
|
285
|
+
// Exact spelling of EVERY component — the catalog hands the route the
|
|
286
|
+
// on-disk spelling, so a mismatch means the plugin is gone (or was
|
|
287
|
+
// never there). Checked against directory listings, never `stat`: on a
|
|
288
|
+
// case-insensitive filesystem a stale spelling would stat a replacement
|
|
289
|
+
// plugin at the same location and park THAT.
|
|
290
|
+
if (!(await this.exactFolderExists(pluginsDir, segments))) {
|
|
291
|
+
throw new PluginProvisionError('Unknown plugin', 404);
|
|
292
|
+
}
|
|
220
293
|
|
|
221
|
-
const wsId = await this.readyWorkspaceId();
|
|
222
|
-
const wsDir = await this.workspaceService.getWorkspacePath(wsId);
|
|
223
|
-
const pluginsDir = path.join(wsDir, this.kbDirName, PLUGINS_DIR);
|
|
224
|
-
const folderDir = path.join(pluginsDir, name);
|
|
225
294
|
// Dot-prefixed ⇒ invisible to the plugin scanner and the collision
|
|
226
|
-
// check for the whole window the commit is in flight.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
295
|
+
// check for the whole window the commit is in flight. UNIQUE, so the
|
|
296
|
+
// park never lands on — and never removes — a path that was already
|
|
297
|
+
// there: a sibling somebody named that way, or the residue of a run
|
|
298
|
+
// that crashed mid-delete (which stays, invisible, for a person to
|
|
299
|
+
// clear; a delete must never destroy anything but the plugin it names).
|
|
300
|
+
// FIXED LENGTH: the plugin's own name is not part of it, so a name near
|
|
301
|
+
// the filesystem's component limit parks as well as a short one.
|
|
302
|
+
const parkedDir = path.join(path.dirname(folderDir), `.deleting-${randomUUID()}`);
|
|
230
303
|
await fs.rename(folderDir, parkedDir);
|
|
231
304
|
try {
|
|
232
305
|
// Inline and `systemAuthorized`, for `provision`'s reasons in
|
|
@@ -261,45 +334,78 @@ export class PluginProvisionService {
|
|
|
261
334
|
});
|
|
262
335
|
}
|
|
263
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Whether `segments` names a directory below `root` with every component
|
|
339
|
+
* spelled exactly as on disk — by listing each level, which is the one
|
|
340
|
+
* question a case-insensitive filesystem answers honestly.
|
|
341
|
+
*/
|
|
342
|
+
private async exactFolderExists(root: string, segments: string[]): Promise<boolean> {
|
|
343
|
+
let dir = root;
|
|
344
|
+
let rel = PLUGINS_DIR;
|
|
345
|
+
for (const segment of segments) {
|
|
346
|
+
const entries = await listDirOrIncomplete(dir, rel);
|
|
347
|
+
if (!entries?.some((e) => e.isDirectory() && e.name === segment)) return false;
|
|
348
|
+
dir = path.join(dir, segment);
|
|
349
|
+
rel = `${rel}/${segment}`;
|
|
350
|
+
}
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
264
354
|
/** The taken name (in its on-disk casing) colliding with `name`, or null. */
|
|
265
355
|
private async existingFolder(name: string): Promise<string | null> {
|
|
266
356
|
const wsId = await this.readyWorkspaceId();
|
|
267
357
|
const wsDir = await this.workspaceService.getWorkspacePath(wsId);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
} catch {
|
|
272
|
-
return null; // no Plugins/ root yet — nothing can collide
|
|
273
|
-
}
|
|
358
|
+
// No Plugins/ root yet — nothing can collide.
|
|
359
|
+
const children = await listDirOrIncomplete(path.join(wsDir, this.kbDirName, PLUGINS_DIR), PLUGINS_DIR);
|
|
360
|
+
if (!children) return null;
|
|
274
361
|
const lower = name.toLowerCase();
|
|
275
|
-
return children.find((c) => c.toLowerCase() === lower) ?? null;
|
|
362
|
+
return children.find((c) => c.name.toLowerCase() === lower)?.name ?? null;
|
|
276
363
|
}
|
|
277
364
|
|
|
278
|
-
/**
|
|
279
|
-
|
|
365
|
+
/**
|
|
366
|
+
* The identity discovery gives the plugin at `folder`, or the folder's own
|
|
367
|
+
* slug when it lists none there. ONE rule for every write provisioning
|
|
368
|
+
* makes — create, delete — as for the rename: no write over a discovery
|
|
369
|
+
* with a hole in it, whether or not the hole is where this plugin lives.
|
|
370
|
+
* An operation that parks and commits against an identity set it could
|
|
371
|
+
* not fully see is the class of mistake the rule exists to make impossible.
|
|
372
|
+
*/
|
|
373
|
+
private async discoveredIdentity(folder: string, leaf: string): Promise<string> {
|
|
374
|
+
const { plugins, unreadable } = await this.discovered();
|
|
375
|
+
if (unreadable.length > 0) throw incompleteDiscovery(unreadable);
|
|
376
|
+
const found = plugins.find((p) => p.folder === folder);
|
|
377
|
+
// The lock key is the SLUG — what the marketplace publishes and what a
|
|
378
|
+
// creation of the name locks on — so a bundle declaring "Sales Team"
|
|
379
|
+
// and a creation of `sales-team` take one lock. Not listed and nothing
|
|
380
|
+
// unreadable: no manifest, no bundle — the folder is its own identity.
|
|
381
|
+
return pluginManifestName(found ? found.name : leaf);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** One discovery over the knowledge base checkout. */
|
|
385
|
+
private async discovered(): Promise<Discovery> {
|
|
280
386
|
const wsId = await this.readyWorkspaceId();
|
|
281
387
|
const wsDir = await this.workspaceService.getWorkspacePath(wsId);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
388
|
+
return this.source.discover(path.join(wsDir, this.kbDirName));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* An existing plugin — at ANY depth, in either file shape — whose identity
|
|
393
|
+
* folds to the same slug as `name`'s, as its repo-relative folder; or null.
|
|
394
|
+
* Discovery's answer, so creation and every catalog agree on what is taken:
|
|
395
|
+
* a nested plugin claims its identity as fully as a top-level one, and a
|
|
396
|
+
* parked delete (dot-prefixed, invisible to the walk) claims nothing.
|
|
397
|
+
* Personal folders count — they publish a plugin.json like any plugin
|
|
398
|
+
* (though the reservation above means a named plugin never reaches this
|
|
399
|
+
* check with a personal slug). A loose file at the root is not a plugin.
|
|
400
|
+
*
|
|
401
|
+
* Refuses on a HOLE: a listing that could not see part of the tree cannot
|
|
402
|
+
* prove a name free.
|
|
403
|
+
*/
|
|
404
|
+
private async manifestNameTwin(name: string): Promise<string | null> {
|
|
405
|
+
const { plugins, unreadable } = await this.discovered();
|
|
406
|
+
if (unreadable.length > 0) throw incompleteDiscovery(unreadable);
|
|
290
407
|
const slug = pluginManifestName(name);
|
|
291
|
-
|
|
292
|
-
// that is not dot-prefixed (a parked delete — invisible to every
|
|
293
|
-
// scanner). Personal folders count — they publish a plugin.json like
|
|
294
|
-
// any plugin (though the reservation above means a named plugin can
|
|
295
|
-
// never reach this check with a personal slug). A loose file at the
|
|
296
|
-
// root (`Plugins/slack.tool`) is not a plugin and must not 409 a
|
|
297
|
-
// legitimate "Slack Tool".
|
|
298
|
-
return (
|
|
299
|
-
children.find(
|
|
300
|
-
(c) => c.isDirectory() && !c.name.startsWith('.') && pluginManifestName(c.name) === slug,
|
|
301
|
-
)?.name ?? null
|
|
302
|
-
);
|
|
408
|
+
return plugins.find((p) => pluginManifestName(p.name) === slug)?.folder ?? null;
|
|
303
409
|
}
|
|
304
410
|
|
|
305
411
|
private async provision(user: AuthUser, folder: string, accessMd: string): Promise<void> {
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
PERSONAL_PLUGIN_PREFIX,
|
|
5
|
+
PLUGIN_MANIFEST_FILE,
|
|
6
|
+
isPluginIdentifier,
|
|
7
|
+
pluginDisplayNameOf,
|
|
8
|
+
pluginManifestName,
|
|
9
|
+
} from '@bevel-software/platform-shared';
|
|
10
|
+
import type { AuthUser } from '@bevel-software/platform-shared';
|
|
11
|
+
import { PushNeedsAgentResolutionError } from '../../shared/domain-errors.js';
|
|
12
|
+
import { walkKb, type KbWalkListener } from '../../shared/kb-walk.js';
|
|
13
|
+
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
14
|
+
import {
|
|
15
|
+
PLUGIN_TOKEN_PREFIX,
|
|
16
|
+
canonicalPluginToken,
|
|
17
|
+
hasAccessFrontmatterExtension,
|
|
18
|
+
parsePluginPrincipalKey,
|
|
19
|
+
} from '../access-model/access-grammar.js';
|
|
20
|
+
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
21
|
+
import type { PluginSource } from './discovery/plugin-source.js';
|
|
22
|
+
import { linksWorkspaceId } from './plugin-links.js';
|
|
23
|
+
|
|
24
|
+
export class PluginRenameError extends Error {
|
|
25
|
+
constructor(
|
|
26
|
+
message: string,
|
|
27
|
+
readonly status: number,
|
|
28
|
+
readonly payload: Record<string, unknown> = {},
|
|
29
|
+
) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'PluginRenameError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The one commit the rename lands as — a scoped batch, pushed. */
|
|
36
|
+
export interface RenameCommitDriver {
|
|
37
|
+
commitChanges(workspaceId: string, user: AuthUser, summary: string, onlyPaths: string[]): Promise<unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RenameResult {
|
|
41
|
+
name: string;
|
|
42
|
+
displayName: string;
|
|
43
|
+
/** Repo-relative files whose grants were rewritten, for the response and the log. */
|
|
44
|
+
rewritten: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Renaming a plugin — its identifier, its display name, or both.
|
|
49
|
+
*
|
|
50
|
+
* The identifier (the manifest's `name`) IS the plugin: the marketplace
|
|
51
|
+
* publishes it, the URLs key on it, and every grant of `plugin/<name>/<verb>`
|
|
52
|
+
* spells it. Changing it is therefore a rename of a PRINCIPAL, and a
|
|
53
|
+
* principal that is renamed in one file and not another is two principals,
|
|
54
|
+
* one of which nobody is a member of. So the rename is one commit: the
|
|
55
|
+
* manifest, plus every access entry in the knowledge base that names the old
|
|
56
|
+
* identifier, rewritten to the new one. Membership does not change — the
|
|
57
|
+
* same people hold the same verbs through the new spelling — but the caller
|
|
58
|
+
* still needs write on each file the commit touches: the protected-branch
|
|
59
|
+
* gate checks every path, and a rename that lands in some files and is
|
|
60
|
+
* refused in others is the split it exists to prevent. The refusal names the
|
|
61
|
+
* files, so the manager can ask their editors or an admin.
|
|
62
|
+
*
|
|
63
|
+
* The display name is free text with no consequences: the manifest changes,
|
|
64
|
+
* nothing else does.
|
|
65
|
+
*/
|
|
66
|
+
export class PluginRenameService {
|
|
67
|
+
constructor(
|
|
68
|
+
private readonly workspaceService: WorkspaceService,
|
|
69
|
+
private readonly commits: RenameCommitDriver,
|
|
70
|
+
private readonly accessControl: IAccessControl,
|
|
71
|
+
private readonly source: PluginSource,
|
|
72
|
+
private readonly kbDirName: string,
|
|
73
|
+
private readonly events?: {
|
|
74
|
+
emit(event: { kind: 'fs-tree-changed'; workspaceId: string; branch: string }): void;
|
|
75
|
+
},
|
|
76
|
+
private readonly onChanged?: () => void,
|
|
77
|
+
) {}
|
|
78
|
+
|
|
79
|
+
private noteChanged(wsId: string): void {
|
|
80
|
+
noteChangedFor(this.accessControl, this.events, this.onChanged, wsId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The READ phase must have seen everything it needed; a hole refuses the rename before any write. */
|
|
84
|
+
private assertComplete(unreadable: string[]): void {
|
|
85
|
+
if (unreadable.length === 0) return;
|
|
86
|
+
throw new PluginRenameError(
|
|
87
|
+
`Some of the knowledge base could not be read (${unreadable.join(', ')}), so the rename cannot be checked against every plugin and grant. Try again, or ask an admin.`,
|
|
88
|
+
503,
|
|
89
|
+
{ kind: 'incomplete-discovery', unreadable },
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async rename(
|
|
94
|
+
user: AuthUser,
|
|
95
|
+
current: string,
|
|
96
|
+
patch: { name?: unknown; displayName?: unknown },
|
|
97
|
+
): Promise<RenameResult> {
|
|
98
|
+
const wsId = linksWorkspaceId();
|
|
99
|
+
const kbRoot = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName);
|
|
100
|
+
// ONE walk of the checkout for everything the rename needs to know:
|
|
101
|
+
// the plugins (discovery's listener) and every file that can carry a
|
|
102
|
+
// grant (this one's), seeing one set of holes. Listing is all that
|
|
103
|
+
// happens here — no file is read, nothing is decided — so it can run
|
|
104
|
+
// before authorization without telling anyone anything.
|
|
105
|
+
const grantFiles: string[] = [];
|
|
106
|
+
const grantListener: KbWalkListener = {
|
|
107
|
+
onFile(dir, name) {
|
|
108
|
+
if (hasAccessFrontmatterExtension(name)) grantFiles.push(dir ? `${dir}/${name}` : name);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
const { discovery, holes } = this.source.walkWith
|
|
112
|
+
? await this.source.walkWith(kbRoot, [grantListener])
|
|
113
|
+
: { discovery: await this.source.discover(kbRoot), holes: (await walkKb(kbRoot, [grantListener])).holes };
|
|
114
|
+
const { plugins } = discovery;
|
|
115
|
+
const unreadable = [...new Set([...discovery.unreadable, ...holes])];
|
|
116
|
+
// Three phases: READ everything, DECIDE, then WRITE. Nothing about the
|
|
117
|
+
// tree — not even that part of it could not be read — reaches a caller
|
|
118
|
+
// before they are known to manage the plugin they name.
|
|
119
|
+
const plugin = plugins.find((p) => p.name === current.trim() && !p.personal && p.exists);
|
|
120
|
+
// Fail closed, like every plugin surface: unknown and not-yours answer alike.
|
|
121
|
+
if (!plugin || !(await this.accessControl.canWrite(wsId, user.email, `${plugin.folder}/access.md`))) {
|
|
122
|
+
throw new PluginRenameError('Unknown plugin', 404, { kind: 'unknown-plugin' });
|
|
123
|
+
}
|
|
124
|
+
if (!plugin.linksAreManaged) {
|
|
125
|
+
throw new PluginRenameError(
|
|
126
|
+
`${plugin.name} is read from an external plugin format; rename it in that repository.`,
|
|
127
|
+
409,
|
|
128
|
+
{ kind: 'read-only' },
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const nextName = patch.name === undefined ? plugin.name : String(patch.name).trim();
|
|
132
|
+
// The rules govern a NEW identifier only. The current one is whatever the
|
|
133
|
+
// manifest says — a hand-written name that predates a rule (a reserved
|
|
134
|
+
// prefix, say) must not block a display-name change, and cannot be
|
|
135
|
+
// "fixed" by a rename that keeps it.
|
|
136
|
+
const identifierChanges = nextName !== plugin.name;
|
|
137
|
+
// A NEW identifier is claimed against EVERY plugin there is and rewritten
|
|
138
|
+
// into every grant there is. A listing with a hole in it — a folder or
|
|
139
|
+
// manifest that exists but could not be read — is neither set: the name
|
|
140
|
+
// could belong to what was not seen, the grant could live there. Fail
|
|
141
|
+
// closed; nothing is written. A display-name change claims nothing and
|
|
142
|
+
// touches one manifest, so it needs no more than its own plugin read.
|
|
143
|
+
if (identifierChanges) this.assertComplete(unreadable);
|
|
144
|
+
// An identifier must be its own slug: the marketplace and the collision
|
|
145
|
+
// checks key on `pluginManifestName(name)`, so a name that does not
|
|
146
|
+
// round-trip (longer than the slug's 64 characters, say) would be one
|
|
147
|
+
// thing here and another everywhere it is published.
|
|
148
|
+
if (identifierChanges && (!isPluginIdentifier(nextName) || pluginManifestName(nextName) !== nextName)) {
|
|
149
|
+
throw new PluginRenameError(
|
|
150
|
+
'A plugin identifier is lowercase kebab-case: letters, digits and single hyphens, like "sales-team".',
|
|
151
|
+
422,
|
|
152
|
+
{ kind: 'bad-name' },
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (identifierChanges && nextName.startsWith(PERSONAL_PLUGIN_PREFIX)) {
|
|
156
|
+
throw new PluginRenameError(
|
|
157
|
+
`The "${PERSONAL_PLUGIN_PREFIX}" namespace is reserved for personal folders. Pick another identifier.`,
|
|
158
|
+
422,
|
|
159
|
+
{ kind: 'bad-name' },
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
// Taken by SLUG, not by spelling: a bundle may declare a name that is not
|
|
163
|
+
// an identifier, and the marketplace folds every name to its slug — a
|
|
164
|
+
// collision there drops one of the two from the catalog.
|
|
165
|
+
if (identifierChanges && plugins.some((p) => p !== plugin && pluginManifestName(p.name) === nextName)) {
|
|
166
|
+
throw new PluginRenameError(`A plugin named "${nextName}" already exists.`, 409, { kind: 'name-taken' });
|
|
167
|
+
}
|
|
168
|
+
const folderName = path.posix.basename(plugin.folder);
|
|
169
|
+
const nextDisplay =
|
|
170
|
+
patch.displayName === undefined ? plugin.displayName : String(patch.displayName).trim();
|
|
171
|
+
|
|
172
|
+
// The manifest, with everything else in it kept.
|
|
173
|
+
const manifest: Record<string, unknown> = { ...(plugin.manifest ?? {}), name: nextName };
|
|
174
|
+
if (nextDisplay && nextDisplay !== folderName) manifest.displayName = nextDisplay;
|
|
175
|
+
else delete manifest.displayName;
|
|
176
|
+
const manifestRel = `${this.kbDirName}/${plugin.folder}/${PLUGIN_MANIFEST_FILE}`;
|
|
177
|
+
const writes: { rel: string; text: string; before: string | null }[] = [
|
|
178
|
+
{ rel: manifestRel, text: `${JSON.stringify(manifest, null, 2)}\n`, before: plugin.manifestText },
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
if (identifierChanges) {
|
|
182
|
+
// Every access entry in the knowledge base that names the old
|
|
183
|
+
// principal — in a folder's access.md or in the own frontmatter of any
|
|
184
|
+
// file kind the resolver reads grants from (`.md`, `.tool`, whatever
|
|
185
|
+
// an overlay registered). The files came from the walk above, whose
|
|
186
|
+
// holes have already refused: a folder that could not be listed would
|
|
187
|
+
// leave the old spelling live in it, and a principal renamed in some
|
|
188
|
+
// files and not others is two principals. A file the walk listed but
|
|
189
|
+
// that cannot be opened is the same hole: every such file is
|
|
190
|
+
// collected, then refused together.
|
|
191
|
+
const unopened: string[] = [];
|
|
192
|
+
for (const rel of grantFiles) {
|
|
193
|
+
const abs = path.join(kbRoot, rel);
|
|
194
|
+
let before: string;
|
|
195
|
+
try {
|
|
196
|
+
before = await fs.readFile(abs, 'utf-8');
|
|
197
|
+
} catch {
|
|
198
|
+
unopened.push(rel);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const after = renamePluginPrincipalInText(before, plugin.name, nextName);
|
|
202
|
+
if (after !== before) writes.push({ rel: `${this.kbDirName}/${rel}`, text: after, before });
|
|
203
|
+
}
|
|
204
|
+
this.assertComplete(unopened);
|
|
205
|
+
const denied: string[] = [];
|
|
206
|
+
for (const w of writes.slice(1)) {
|
|
207
|
+
const repoRel = w.rel.slice(this.kbDirName.length + 1);
|
|
208
|
+
if (!(await this.accessControl.canWrite(wsId, user.email, repoRel))) denied.push(repoRel);
|
|
209
|
+
}
|
|
210
|
+
if (denied.length > 0) {
|
|
211
|
+
throw new PluginRenameError(
|
|
212
|
+
`Renaming ${plugin.name} would rewrite grants in files you can't edit: ${denied.join(', ')}. Ask their editors, or an admin.`,
|
|
213
|
+
409,
|
|
214
|
+
{ kind: 'needs-write', files: denied },
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Writes and commit share ONE failure envelope: whichever of them fails,
|
|
220
|
+
// the working tree goes back to what origin has — a half-written batch
|
|
221
|
+
// is as much a split principal as a refused commit.
|
|
222
|
+
try {
|
|
223
|
+
for (const w of writes) await this.workspaceService.writeFile(wsId, w.rel, w.text);
|
|
224
|
+
const summary = identifierChanges
|
|
225
|
+
? `Rename plugin ${plugin.name} to ${nextName}`
|
|
226
|
+
: `Rename plugin ${plugin.name}: display name`;
|
|
227
|
+
await this.commits.commitChanges(
|
|
228
|
+
wsId,
|
|
229
|
+
user,
|
|
230
|
+
summary,
|
|
231
|
+
writes.map((w) => w.rel),
|
|
232
|
+
);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
// One failure is NOT a failed commit: the commit landed locally and only
|
|
235
|
+
// the push did not (the typed hand-off the workflow throws). Restoring
|
|
236
|
+
// the old bytes then would stack an uncommitted inverse of a real
|
|
237
|
+
// commit; the recovery flow owns that state, so it is left alone — and
|
|
238
|
+
// since the tree DID change, every cache keyed on the old identity is
|
|
239
|
+
// dropped before the error goes up, exactly as on success.
|
|
240
|
+
if (err instanceof PushNeedsAgentResolutionError) {
|
|
241
|
+
this.noteChanged(wsId);
|
|
242
|
+
} else {
|
|
243
|
+
for (const w of writes) {
|
|
244
|
+
if (w.before !== null) await this.workspaceService.writeFile(wsId, w.rel, w.before).catch(() => undefined);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
249
|
+
this.noteChanged(wsId);
|
|
250
|
+
return {
|
|
251
|
+
name: nextName,
|
|
252
|
+
displayName: pluginDisplayNameOf(manifest, folderName),
|
|
253
|
+
rewritten: writes.slice(1).map((w) => w.rel.slice(this.kbDirName.length + 1)),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The tree changed: drop the resolver's model and the plugin caches, and
|
|
260
|
+
* tell every session. The ONE place this happens, for the landed commit and
|
|
261
|
+
* for the committed-but-unpushed one alike.
|
|
262
|
+
*/
|
|
263
|
+
function noteChangedFor(
|
|
264
|
+
accessControl: IAccessControl,
|
|
265
|
+
events: PluginRenameService['events'],
|
|
266
|
+
onChanged: (() => void) | undefined,
|
|
267
|
+
wsId: string,
|
|
268
|
+
): void {
|
|
269
|
+
accessControl.invalidate(wsId);
|
|
270
|
+
events?.emit({ kind: 'fs-tree-changed', workspaceId: wsId, branch: linksWorkspaceIdBranch() });
|
|
271
|
+
onChanged?.();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Every access entry line naming `plugin/<old>/<verb>` — in ANY spelling the
|
|
276
|
+
* parser accepts — rewritten to `plugin/<next>/<verb>`, the rest of the
|
|
277
|
+
* line (indent, `- `, a `deny` prefix, the line ending) kept. Lines that are
|
|
278
|
+
* not such an entry are untouched, so the file's own formatting survives.
|
|
279
|
+
*/
|
|
280
|
+
export function renamePluginPrincipalInText(text: string, oldName: string, nextName: string): string {
|
|
281
|
+
const oldSlug = pluginManifestName(oldName);
|
|
282
|
+
return text
|
|
283
|
+
.split('\n')
|
|
284
|
+
.map((line) => {
|
|
285
|
+
const m = /^(\s*-\s+(?:deny\s+)?)(\S.*?)(\s*)$/i.exec(line);
|
|
286
|
+
if (!m) return line;
|
|
287
|
+
const key = canonicalPluginToken(m[2]!);
|
|
288
|
+
const parsed = key === null ? null : parsePluginPrincipalKey(key);
|
|
289
|
+
if (!parsed || parsed.slug !== oldSlug) return line;
|
|
290
|
+
return `${m[1]}${PLUGIN_TOKEN_PREFIX}${nextName}/${parsed.verb}${m[3]}`;
|
|
291
|
+
})
|
|
292
|
+
.join('\n');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function linksWorkspaceIdBranch(): string {
|
|
296
|
+
// The default branch's workspace id IS its encoded branch name.
|
|
297
|
+
return decodeURIComponent(linksWorkspaceId());
|
|
298
|
+
}
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
* change requests; only the UI dresses them up.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
+
export type { PluginMembership } from '../skills/skills.contract.js';
|
|
37
|
+
|
|
36
38
|
/** Access-rule principals as the resolver hands them back. */
|
|
37
39
|
export interface ResolvedPrincipals {
|
|
38
40
|
roles: string[];
|
|
@@ -45,10 +47,14 @@ export interface ResolvedReaders extends ResolvedPrincipals {
|
|
|
45
47
|
|
|
46
48
|
/** One plugin as `GET /api/plugins` reports it, resolved for ONE caller. */
|
|
47
49
|
export interface PluginSummary {
|
|
48
|
-
/**
|
|
50
|
+
/** The plugin's identity — its manifest name, e.g. `gtm`. */
|
|
49
51
|
name: string;
|
|
52
|
+
/** What a person sees it called, e.g. `GTM`. */
|
|
53
|
+
displayName: string;
|
|
50
54
|
/** Repo-relative constituent folders, e.g. `['Plugins/GTM']`. */
|
|
51
55
|
folders: string[];
|
|
56
|
+
/** Whether this platform writes the plugin's links — see `PluginCatalogEntry`. */
|
|
57
|
+
linksAreManaged: boolean;
|
|
52
58
|
/**
|
|
53
59
|
* Per-caller: the caller can read the FOLDER (membership). Every returned
|
|
54
60
|
* plugin has at least one of `canRead` / `canWrite` / discoverability; a
|
|
@@ -71,6 +77,16 @@ export interface PluginSummary {
|
|
|
71
77
|
/** Caller-INDEPENDENT total (the plugin's whole content, not the caller's slice). */
|
|
72
78
|
skillCount: number;
|
|
73
79
|
toolCount: number;
|
|
80
|
+
/**
|
|
81
|
+
* Caller-INDEPENDENT: how many of the plugin's LINKED skills its members
|
|
82
|
+
* cannot read — the link is in the manifest, but the skill folder no
|
|
83
|
+
* longer grants the plugin's readers. Counted here, from the unfiltered
|
|
84
|
+
* link index, because the people who most need the warning are the
|
|
85
|
+
* plugin's managers, and they are exactly the people a missing grant
|
|
86
|
+
* locks out: a count derived from what the caller can read would be zero
|
|
87
|
+
* for them. Zero for a plugin whose links are not managed here.
|
|
88
|
+
*/
|
|
89
|
+
brokenLinks: number;
|
|
74
90
|
/** For display: "Run by …" (fallback chain: owners → writers → 'the workspace admins'). */
|
|
75
91
|
owners: ResolvedPrincipals;
|
|
76
92
|
writers: ResolvedPrincipals;
|
|
@@ -90,10 +106,21 @@ export interface PluginSummary {
|
|
|
90
106
|
* appears at all) are resolved per request in the route.
|
|
91
107
|
*/
|
|
92
108
|
export interface PluginCatalogEntry {
|
|
109
|
+
/** The plugin's identity — its manifest name. */
|
|
93
110
|
name: string;
|
|
111
|
+
/** What a person sees it called — the manifest's `displayName`, else the folder name. */
|
|
112
|
+
displayName: string;
|
|
94
113
|
folders: string[];
|
|
114
|
+
/**
|
|
115
|
+
* Whether this platform writes the plugin's links (a native `plugin.json`)
|
|
116
|
+
* — false for a plugin read from an external format, whose links are
|
|
117
|
+
* edited in that repository and which the link endpoints refuse.
|
|
118
|
+
*/
|
|
119
|
+
linksAreManaged: boolean;
|
|
95
120
|
skillCount: number;
|
|
96
121
|
toolCount: number;
|
|
122
|
+
/** Linked skills the members cannot read — see `PluginSummary.brokenLinks`. */
|
|
123
|
+
brokenLinks: number;
|
|
97
124
|
owners: ResolvedPrincipals;
|
|
98
125
|
writers: ResolvedPrincipals;
|
|
99
126
|
readers: ResolvedReaders;
|