@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
|
@@ -3,7 +3,11 @@ import path from 'node:path';
|
|
|
3
3
|
import { execFile, spawn } from 'node:child_process';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
|
+
import { isAbsence } from '../../shared/fs-errors.js';
|
|
7
|
+
import { walkKb } from '../../shared/kb-walk.js';
|
|
8
|
+
import { PLUGINS_DIR, PLUGIN_MANIFEST_FILE, isPersonalPluginDir, pluginIdentityOf, } from '@bevel-software/platform-shared';
|
|
6
9
|
import { AccessConfigError, AccessUnreadableError } from '../access-model/access-errors.js';
|
|
10
|
+
import { synthesizePluginPrincipals } from '../access-model/plugin-principals.js';
|
|
7
11
|
import { GROUPS_YAML, SYNCED_GROUPS_YAML, parseGroupsFile, } from '../access-model/group-files.js';
|
|
8
12
|
import { ADMIN_CANONICAL, EVERYONE_CANONICAL, EVERYONE_DISPLAY, KNOWN_VERBS, ROLE_TOKEN_PREFIX, accessMdSelfEntries, canonicalEmail, canonicalRoleName, isAccessMdPath, mergeGroupsIntoRoles, parseAccessFile, parseOwnAccessEntries, parseRolesYaml, sourceVerbsFor, } from '../access-model/access-grammar.js';
|
|
9
13
|
import { DIRECTORY_SYNC_BOT_EMAIL, DIRECTORY_SYNC_BOT_NAME, } from './directory-sync-bot.js';
|
|
@@ -78,11 +82,6 @@ function isBuiltInRole(canonicalRole) {
|
|
|
78
82
|
function roleKnown(roles, canonicalRole) {
|
|
79
83
|
return roles.byCanonical.has(canonicalRole) || isBuiltInRole(canonicalRole);
|
|
80
84
|
}
|
|
81
|
-
/** True for the errno codes that mean "the file genuinely is not there". */
|
|
82
|
-
function isAbsenceError(err) {
|
|
83
|
-
const code = err?.code;
|
|
84
|
-
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
85
|
-
}
|
|
86
85
|
/**
|
|
87
86
|
* Load the ACTIVE group source through `read` (working tree or at-ref — the
|
|
88
87
|
* caller supplies the reader, so both model loaders share one mode rule):
|
|
@@ -119,7 +118,7 @@ export async function loadActiveGroups(read) {
|
|
|
119
118
|
// the same error; groups cannot be the one input that does not.
|
|
120
119
|
if (err instanceof AccessUnreadableError)
|
|
121
120
|
throw err;
|
|
122
|
-
if (
|
|
121
|
+
if (isAbsence(err)) {
|
|
123
122
|
syncedText = null;
|
|
124
123
|
}
|
|
125
124
|
else {
|
|
@@ -141,7 +140,7 @@ export async function loadActiveGroups(read) {
|
|
|
141
140
|
catch (err) {
|
|
142
141
|
if (err instanceof AccessUnreadableError)
|
|
143
142
|
throw err; // see above
|
|
144
|
-
if (
|
|
143
|
+
if (isAbsence(err))
|
|
145
144
|
text = null;
|
|
146
145
|
else
|
|
147
146
|
return broken(GROUPS_YAML, err instanceof Error ? err.message : String(err));
|
|
@@ -165,6 +164,64 @@ export async function loadActiveGroups(read) {
|
|
|
165
164
|
* `hasPermissionResolved`). `collapseScopes` flattens this into the
|
|
166
165
|
* closest-wins-per-principal view the display helpers use.
|
|
167
166
|
*/
|
|
167
|
+
/**
|
|
168
|
+
* The rules listener on the one walk of the checkout: every `access.md` at
|
|
169
|
+
* any depth (the access tree is structure-agnostic — the root's included),
|
|
170
|
+
* plus every plugin manifest under the plugins root, for principal
|
|
171
|
+
* synthesis. A reader: a folder the walk could not list is simply not in
|
|
172
|
+
* the model (a writer that needs completeness asks the walk for its holes).
|
|
173
|
+
*
|
|
174
|
+
* Per-file failures (malformed YAML, bad role refs, unknown verbs) are
|
|
175
|
+
* logged and the offending file is dropped from the model — they do NOT
|
|
176
|
+
* throw. A typo in one nested access.md must not 500 the entire editor;
|
|
177
|
+
* admins can still write `access.md` / `roles.yaml` because
|
|
178
|
+
* `hasPermissionResolved` admin-rescues those paths, so the bad config
|
|
179
|
+
* remains fixable from inside the app.
|
|
180
|
+
*/
|
|
181
|
+
function accessFileListener(repoDir, out, pluginDirs) {
|
|
182
|
+
return {
|
|
183
|
+
async onFile(dir, name) {
|
|
184
|
+
const rel = dir ? `${dir}/${name}` : name;
|
|
185
|
+
if (name === PLUGIN_MANIFEST_FILE && dir.startsWith(`${PLUGINS_DIR}/`)) {
|
|
186
|
+
const text = await fs.readFile(path.join(repoDir, rel), 'utf-8').catch(() => null);
|
|
187
|
+
pluginDirs.set(dir, pluginIdentityOf(parseManifestText(text), path.posix.basename(dir)));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (name !== 'access.md')
|
|
191
|
+
return;
|
|
192
|
+
const text = await fs.readFile(path.join(repoDir, rel), 'utf-8');
|
|
193
|
+
const parsed = parseAccessFile(text, rel);
|
|
194
|
+
if (!parsed.ok) {
|
|
195
|
+
// Treat as if the file didn't exist for resolution purposes.
|
|
196
|
+
// Admin-rescue on access.md paths still lets an admin fix it.
|
|
197
|
+
for (const e of parsed.errors)
|
|
198
|
+
console.warn(`[access] ${e} — file ignored`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
for (const w of parsed.warnings)
|
|
202
|
+
console.warn(`[access] ${w}`);
|
|
203
|
+
if (out.has(parsed.file.dir)) {
|
|
204
|
+
console.warn(`[access] ${rel}: duplicate access.md for directory '${parsed.file.dir}' — keeping the first one seen`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
out.set(parsed.file.dir, parsed.file);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/** A manifest's parsed object, or null for absent, unparsable or not-an-object text. */
|
|
212
|
+
function parseManifestText(text) {
|
|
213
|
+
if (text === null)
|
|
214
|
+
return null;
|
|
215
|
+
try {
|
|
216
|
+
const parsed = JSON.parse(text);
|
|
217
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
218
|
+
? parsed
|
|
219
|
+
: null;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
168
225
|
function resolveScopes(model, verb, relativePath, fileOwn) {
|
|
169
226
|
const target = verb;
|
|
170
227
|
const supersets = sourceVerbsFor(verb).filter((v) => v !== verb);
|
|
@@ -197,7 +254,32 @@ function resolveScopes(model, verb, relativePath, fileOwn) {
|
|
|
197
254
|
set(entry, 'grant');
|
|
198
255
|
}
|
|
199
256
|
}
|
|
200
|
-
|
|
257
|
+
// The anonymous caller's verdict at this scope — what someone who holds
|
|
258
|
+
// exactly the PUBLIC keys (a plugin principal whose plugin admits
|
|
259
|
+
// everyone, so everyone holds it) resolves to, by the same tiers a named
|
|
260
|
+
// caller gets: a grant via any public key wins, else a denial via one
|
|
261
|
+
// denies, else the built-in `everyone` entry decides. Derived HERE, where
|
|
262
|
+
// a scope's verdicts are built, so every reader of it — the permission
|
|
263
|
+
// check, the eligible lists, the `restricted` flag, the compiler's
|
|
264
|
+
// everyone audience — sees one truth; and derived into its own field,
|
|
265
|
+
// never into `byRole`, so the entries a person can remove stay exactly
|
|
266
|
+
// the lines the files hold.
|
|
267
|
+
let publicGrant = false;
|
|
268
|
+
let publicDeny = false;
|
|
269
|
+
for (const [key, state] of byRole) {
|
|
270
|
+
if (!model.roles.publicKeys?.has(key))
|
|
271
|
+
continue;
|
|
272
|
+
if (state === 'grant')
|
|
273
|
+
publicGrant = true;
|
|
274
|
+
else if (state === 'denied')
|
|
275
|
+
publicDeny = true;
|
|
276
|
+
}
|
|
277
|
+
const everyone = publicGrant
|
|
278
|
+
? 'grant'
|
|
279
|
+
: publicDeny
|
|
280
|
+
? 'denied'
|
|
281
|
+
: byRole.get(EVERYONE_CANONICAL);
|
|
282
|
+
return { byRole, byEmail, everyone, source };
|
|
201
283
|
};
|
|
202
284
|
const scopes = [];
|
|
203
285
|
// The node's own frontmatter is the most specific scope. Role refs there are
|
|
@@ -221,17 +303,33 @@ function resolveScopes(model, verb, relativePath, fileOwn) {
|
|
|
221
303
|
function collapseScopes(scopes) {
|
|
222
304
|
const byRole = new Map();
|
|
223
305
|
const byEmail = new Map();
|
|
306
|
+
let everyone;
|
|
224
307
|
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
225
308
|
for (const [k, v] of scopes[i].byRole)
|
|
226
309
|
byRole.set(k, v);
|
|
227
310
|
for (const [k, v] of scopes[i].byEmail)
|
|
228
311
|
byEmail.set(k, v);
|
|
312
|
+
if (scopes[i].everyone !== undefined)
|
|
313
|
+
everyone = scopes[i].everyone;
|
|
229
314
|
}
|
|
230
|
-
return { byRole, byEmail };
|
|
315
|
+
return { byRole, byEmail, everyone };
|
|
231
316
|
}
|
|
232
317
|
function resolveAtPath(model, verb, relativePath, fileOwn) {
|
|
233
318
|
return collapseScopes(resolveScopes(model, verb, relativePath, fileOwn));
|
|
234
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Every principal key a caller holds: the tokens their email is a member of,
|
|
322
|
+
* plus the PUBLIC keys every caller holds regardless (a plugin principal
|
|
323
|
+
* whose plugin grants `everyone`). Undefined when there is nothing at all,
|
|
324
|
+
* matching the plain `byEmail.get` the tier-2 check used to read.
|
|
325
|
+
*/
|
|
326
|
+
function principalKeysOf(model, email) {
|
|
327
|
+
const own = model.roles.byEmail.get(email);
|
|
328
|
+
const pub = model.roles.publicKeys;
|
|
329
|
+
if (!pub?.size)
|
|
330
|
+
return own;
|
|
331
|
+
return new Set([...(own ?? []), ...pub]);
|
|
332
|
+
}
|
|
235
333
|
function isAdminEmail(model, email) {
|
|
236
334
|
if (model.deploymentOwners.has(email))
|
|
237
335
|
return true;
|
|
@@ -282,7 +380,7 @@ function hasPermissionResolved(model, verb, userEmail, relativePath, fileOwn) {
|
|
|
282
380
|
if (isAccessMdPath(relativePath) && isAdminEmail(model, email))
|
|
283
381
|
return true;
|
|
284
382
|
}
|
|
285
|
-
const userRoles = model
|
|
383
|
+
const userRoles = principalKeysOf(model, email);
|
|
286
384
|
const scopes = resolveScopes(model, verb, relativePath, fileOwn);
|
|
287
385
|
for (const scope of scopes) {
|
|
288
386
|
// Tier 1 — direct email entry is the most specific verdict at this scope.
|
|
@@ -307,10 +405,10 @@ function hasPermissionResolved(model, verb, userEmail, relativePath, fileOwn) {
|
|
|
307
405
|
if (deny)
|
|
308
406
|
return false;
|
|
309
407
|
}
|
|
310
|
-
// Tier 3 — the built-in `everyone` role
|
|
311
|
-
|
|
312
|
-
if (everyone)
|
|
313
|
-
return everyone === 'grant';
|
|
408
|
+
// Tier 3 — the built-in `everyone` role (derived: a public plugin
|
|
409
|
+
// principal granted here counts, see `AccessScope.everyone`).
|
|
410
|
+
if (scope.everyone)
|
|
411
|
+
return scope.everyone === 'grant';
|
|
314
412
|
// No verdict at this scope — fall through to the next (farther) one.
|
|
315
413
|
}
|
|
316
414
|
return false;
|
|
@@ -454,11 +552,11 @@ function dirChainFor(relativePath) {
|
|
|
454
552
|
* reported as restricted.
|
|
455
553
|
*/
|
|
456
554
|
function canEveryoneReadResolved(model, relativePath, fileOwn) {
|
|
457
|
-
const {
|
|
555
|
+
const { byEmail, everyone } = resolveAtPath(model, 'read', relativePath, fileOwn);
|
|
458
556
|
// Baseline: an unnamed signed-in user (no email/role entries) reads only via
|
|
459
557
|
// `everyone`. As a single principal, its collapsed verdict is its closest —
|
|
460
558
|
// exactly what that user resolves to.
|
|
461
|
-
if (
|
|
559
|
+
if (everyone !== 'grant')
|
|
462
560
|
return false;
|
|
463
561
|
// Every *named* principal must also still read. A collapsed deny may have
|
|
464
562
|
// been shadowed by a closer-scope `everyone` grant, so re-resolve each
|
|
@@ -491,7 +589,7 @@ function canReadResolved(model, userEmail, relativePath, fileOwn) {
|
|
|
491
589
|
return hasPermissionResolved(model, 'read', userEmail, relativePath, fileOwn);
|
|
492
590
|
}
|
|
493
591
|
function eligibleHoldersResolved(model, verb, relativePath, fileOwn) {
|
|
494
|
-
const { byRole, byEmail } = resolveAtPath(model, verb, relativePath, fileOwn);
|
|
592
|
+
const { byRole, byEmail, everyone } = resolveAtPath(model, verb, relativePath, fileOwn);
|
|
495
593
|
// (kind, display name) → one entry. The `byRole` keys are the merged
|
|
496
594
|
// index's canonical tokens exactly as granted (bare, or the
|
|
497
595
|
// `role/<canonical>` alias); the `byCanonical` record a key hits carries
|
|
@@ -515,6 +613,12 @@ function eligibleHoldersResolved(model, verb, relativePath, fileOwn) {
|
|
|
515
613
|
const record = model.roles.byCanonical.get(canonical);
|
|
516
614
|
addPrincipal(record ? record.displayName : canonical, record?.kind ?? 'role');
|
|
517
615
|
}
|
|
616
|
+
// The derived everyone verdict: a public plugin principal granted here
|
|
617
|
+
// means anyone signed in holds the verb, and a list that counts holders (an
|
|
618
|
+
// approval gate, a "restricted to" banner) must say so — as `everyone`, the
|
|
619
|
+
// same row a literal grant produces.
|
|
620
|
+
if (everyone === 'grant')
|
|
621
|
+
addPrincipal(EVERYONE_CANONICAL, 'role');
|
|
518
622
|
// Mirror the admin overrides applied in `hasPermissionResolved`: write on
|
|
519
623
|
// `roles.yaml` and on any `access.md` is granted to Admin even if the
|
|
520
624
|
// file content doesn't list it. Without surfacing that here, the
|
|
@@ -674,7 +778,29 @@ export class AccessControlService {
|
|
|
674
778
|
invalidate(workspaceId) {
|
|
675
779
|
this.cache.delete(workspaceId);
|
|
676
780
|
this.ownEntriesCache.delete(workspaceId);
|
|
781
|
+
this.generation++;
|
|
677
782
|
}
|
|
783
|
+
/**
|
|
784
|
+
* Bumped by every `invalidate()`, for any workspace. A model load reads the
|
|
785
|
+
* tree across many awaits, and an invalidation fires exactly when that tree
|
|
786
|
+
* changes underneath it: the load that started on the old tree would
|
|
787
|
+
* otherwise store its model AFTER the drop, and every read for the next TTL
|
|
788
|
+
* would resolve against a tree that is already gone. `loadModel` takes the
|
|
789
|
+
* generation before its first read and refuses to store a model fetched
|
|
790
|
+
* under an older one — the caller that started it still gets its answer,
|
|
791
|
+
* as old as the moment it started, and the next caller reloads.
|
|
792
|
+
*
|
|
793
|
+
* ONE counter, not one per workspace: a per-workspace map would keep an
|
|
794
|
+
* entry for every branch workspace the process ever loaded, with nothing to
|
|
795
|
+
* tell it when a workspace is gone. The price of sharing is that a drop on
|
|
796
|
+
* some other workspace, landing mid-load, costs this one a single reload —
|
|
797
|
+
* a load is tens of milliseconds and a drop is a commit, so that is rare
|
|
798
|
+
* and cheap, and it never stores a wrong model. (The own-entries memo needs
|
|
799
|
+
* no token: it is keyed by the map object a reader took BEFORE its disk
|
|
800
|
+
* read, and `invalidate()` replaces the map, so a late store lands in an
|
|
801
|
+
* orphan nobody consults.)
|
|
802
|
+
*/
|
|
803
|
+
generation = 0;
|
|
678
804
|
/** Memoized `readOwnEntries` for the batch paths; see `ownEntriesCache`. */
|
|
679
805
|
async cachedOwnEntries(workspaceId, repoDir, relativePath) {
|
|
680
806
|
let ws = this.ownEntriesCache.get(workspaceId);
|
|
@@ -773,14 +899,26 @@ export class AccessControlService {
|
|
|
773
899
|
async eligibleReaders(workspaceId, relativePath) {
|
|
774
900
|
const model = await this.loadModel(workspaceId);
|
|
775
901
|
const own = await this.readOwnEntries(await this.repoDir(workspaceId), relativePath);
|
|
776
|
-
//
|
|
777
|
-
// and the
|
|
778
|
-
//
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
902
|
+
// `restricted` is the verdict — whether `read: everyone` applies cleanly —
|
|
903
|
+
// and the lists are the GRANTS that exist regardless: on a public node
|
|
904
|
+
// they name what makes it public (`everyone`, and a public plugin
|
|
905
|
+
// principal whose grant is the one to remove), so the share dialog can
|
|
906
|
+
// show that grant as a row. Consumers that only want to know whether the
|
|
907
|
+
// node is public read the flag; the lists may be empty for a
|
|
908
|
+
// default-denied path with no grants.
|
|
782
909
|
const { principals, roles, users } = eligibleHoldersResolved(model, 'read', relativePath, own);
|
|
783
|
-
|
|
910
|
+
// WHY it is public, when it is: the public plugin principals granted read
|
|
911
|
+
// here — the one thing the dialog cannot tell from the lists alone (a
|
|
912
|
+
// plugin principal in them may or may not be public), so the resolver,
|
|
913
|
+
// which knows, says it. A literal `everyone` line shows up as a source
|
|
914
|
+
// of the `everyone` row instead; together the two answer "what remains
|
|
915
|
+
// public after this grant is removed".
|
|
916
|
+
const collapsed = resolveAtPath(model, 'read', relativePath, own);
|
|
917
|
+
const publicVia = [...collapsed.byRole]
|
|
918
|
+
.filter(([key, state]) => state === 'grant' && model.roles.publicKeys?.has(key))
|
|
919
|
+
.map(([key]) => key)
|
|
920
|
+
.sort();
|
|
921
|
+
return { restricted: !canEveryoneReadResolved(model, relativePath, own), principals, roles, users, publicVia };
|
|
784
922
|
}
|
|
785
923
|
async eligibleDownloaders(workspaceId, relativePath) {
|
|
786
924
|
const model = await this.loadModel(workspaceId);
|
|
@@ -819,7 +957,7 @@ export class AccessControlService {
|
|
|
819
957
|
model = await this.loadModel(workspaceId);
|
|
820
958
|
}
|
|
821
959
|
catch {
|
|
822
|
-
return { roles: [], groups: [], people: [] };
|
|
960
|
+
return { roles: [], groups: [], plugins: [], people: [] };
|
|
823
961
|
}
|
|
824
962
|
// Roles = the built-in `everyone` role plus every declared role's display
|
|
825
963
|
// name — ROLE principals only, never groups. Each role is enumerated via
|
|
@@ -831,11 +969,22 @@ export class AccessControlService {
|
|
|
831
969
|
// direct-access.md edit).
|
|
832
970
|
const roles = [EVERYONE_DISPLAY];
|
|
833
971
|
const groups = [];
|
|
972
|
+
// Plugins = the identities behind the synthesised `plugin/<name>/<verb>`
|
|
973
|
+
// principals, once each (three keys share a folder). Personal shelves are
|
|
974
|
+
// plugins to the resolver but not to a person picking a grantee — told
|
|
975
|
+
// apart by their FOLDER (the one structural rule), never by their name.
|
|
976
|
+
const plugins = new Map();
|
|
834
977
|
for (const [key, principal] of model.roles.byCanonical) {
|
|
835
978
|
if (key.startsWith(ROLE_TOKEN_PREFIX))
|
|
836
979
|
roles.push(principal.displayName);
|
|
837
980
|
else if (principal.kind === 'group')
|
|
838
981
|
groups.push(principal.displayName);
|
|
982
|
+
else if (principal.kind === 'plugin' &&
|
|
983
|
+
principal.pluginName &&
|
|
984
|
+
principal.pluginDir &&
|
|
985
|
+
!isPersonalPluginDir(principal.pluginDir)) {
|
|
986
|
+
plugins.set(principal.pluginName, principal.pluginDir);
|
|
987
|
+
}
|
|
839
988
|
}
|
|
840
989
|
// People = roles.yaml member emails (name-less) ∪ access.md `Name <email>`
|
|
841
990
|
// grants (named). The login-only users table is unioned in by the caller.
|
|
@@ -857,7 +1006,14 @@ export class AccessControlService {
|
|
|
857
1006
|
name: name || email.split('@')[0],
|
|
858
1007
|
email,
|
|
859
1008
|
}));
|
|
860
|
-
return {
|
|
1009
|
+
return {
|
|
1010
|
+
roles,
|
|
1011
|
+
groups,
|
|
1012
|
+
plugins: [...plugins.entries()]
|
|
1013
|
+
.map(([name, folder]) => ({ name, folder }))
|
|
1014
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
1015
|
+
people,
|
|
1016
|
+
};
|
|
861
1017
|
}
|
|
862
1018
|
async findEmailByHash(workspaceId, hash) {
|
|
863
1019
|
let model;
|
|
@@ -1012,6 +1168,8 @@ export class AccessControlService {
|
|
|
1012
1168
|
if (cached && Date.now() - cached.loadedAt < AccessControlService.CACHE_TTL_MS) {
|
|
1013
1169
|
return cached.model;
|
|
1014
1170
|
}
|
|
1171
|
+
// Taken before the first read; see `generation`.
|
|
1172
|
+
const generation = this.generation;
|
|
1015
1173
|
const repoDir = await this.repoDir(workspaceId);
|
|
1016
1174
|
let rolesYaml;
|
|
1017
1175
|
try {
|
|
@@ -1063,7 +1221,14 @@ export class AccessControlService {
|
|
|
1063
1221
|
// must not 500 the entire editor; admins can still write `access.md`
|
|
1064
1222
|
// / `roles.yaml` because `hasPermissionResolved` admin-rescues those
|
|
1065
1223
|
// paths, so the bad config remains fixable from inside the app.
|
|
1066
|
-
|
|
1224
|
+
const pluginDirs = new Map();
|
|
1225
|
+
await walkKb(repoDir, [accessFileListener(repoDir, accessFiles, pluginDirs)]);
|
|
1226
|
+
// Plugin principals (`plugin/<Name>/<verb>`), derived from each plugin
|
|
1227
|
+
// folder's own access.md — a plugin being a folder with a manifest, at
|
|
1228
|
+
// any depth — after groups merged (so their rosters expand through the
|
|
1229
|
+
// finished index) and before the unknown-role sweep below (so a grant
|
|
1230
|
+
// naming one counts as known).
|
|
1231
|
+
synthesizePluginPrincipals(rolesParsed.index, accessFiles, pluginDirs);
|
|
1067
1232
|
// Validate role refs against roles.yaml. `everyone` is a built-in role and
|
|
1068
1233
|
// is valid without a roles.yaml entry. Unknown refs are dropped from the
|
|
1069
1234
|
// parsed entry list (along with a warn log) so the rest of the file still
|
|
@@ -1085,49 +1250,12 @@ export class AccessControlService {
|
|
|
1085
1250
|
groupsHealth: activeGroups.health,
|
|
1086
1251
|
deploymentOwners: this.deploymentOwners,
|
|
1087
1252
|
};
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
let entries;
|
|
1093
|
-
try {
|
|
1094
|
-
entries = await fs.readdir(absDir, { withFileTypes: true });
|
|
1095
|
-
}
|
|
1096
|
-
catch {
|
|
1097
|
-
return;
|
|
1098
|
-
}
|
|
1099
|
-
for (const entry of entries) {
|
|
1100
|
-
// Skip VCS metadata + vendored deps. Hidden dirs (`.git`, `.vscode`,
|
|
1101
|
-
// etc.) and `node_modules` can't host KB rules and are often huge —
|
|
1102
|
-
// walking them would slow every cache miss without benefit.
|
|
1103
|
-
if (entry.name.startsWith('.'))
|
|
1104
|
-
continue;
|
|
1105
|
-
if (entry.name === 'node_modules')
|
|
1106
|
-
continue;
|
|
1107
|
-
const abs = path.join(absDir, entry.name);
|
|
1108
|
-
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
1109
|
-
if (entry.isDirectory()) {
|
|
1110
|
-
await this.collectAccessFiles(abs, rel, out);
|
|
1111
|
-
}
|
|
1112
|
-
else if (entry.isFile() && entry.name === 'access.md') {
|
|
1113
|
-
const text = await fs.readFile(abs, 'utf-8');
|
|
1114
|
-
const parsed = parseAccessFile(text, rel);
|
|
1115
|
-
if (!parsed.ok) {
|
|
1116
|
-
// Treat as if the file didn't exist for resolution purposes.
|
|
1117
|
-
// Admin-rescue on access.md paths still lets an admin fix it.
|
|
1118
|
-
for (const e of parsed.errors)
|
|
1119
|
-
console.warn(`[access] ${e} — file ignored`);
|
|
1120
|
-
continue;
|
|
1121
|
-
}
|
|
1122
|
-
for (const w of parsed.warnings)
|
|
1123
|
-
console.warn(`[access] ${w}`);
|
|
1124
|
-
if (out.has(parsed.file.dir)) {
|
|
1125
|
-
console.warn(`[access] ${rel}: duplicate access.md for directory '${parsed.file.dir}' — keeping the first one seen`);
|
|
1126
|
-
continue;
|
|
1127
|
-
}
|
|
1128
|
-
out.set(parsed.file.dir, parsed.file);
|
|
1129
|
-
}
|
|
1253
|
+
// An invalidation landed mid-load: this model describes a tree that is
|
|
1254
|
+
// already gone, so it is returned to this caller but never stored.
|
|
1255
|
+
if (this.generation === generation) {
|
|
1256
|
+
this.cache.set(workspaceId, { model, loadedAt: Date.now() });
|
|
1130
1257
|
}
|
|
1258
|
+
return model;
|
|
1131
1259
|
}
|
|
1132
1260
|
async repoDir(workspaceId) {
|
|
1133
1261
|
const wsDir = await this.workspaceService.getWorkspacePath(workspaceId);
|
|
@@ -1255,14 +1383,18 @@ export class AccessControlService {
|
|
|
1255
1383
|
try {
|
|
1256
1384
|
const { stdout } = await execFileAsync('git', ['-C', repoDir, 'ls-tree', '-r', '--name-only', ref], { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 });
|
|
1257
1385
|
const out = [];
|
|
1386
|
+
const pluginDirs = [];
|
|
1258
1387
|
for (const line of stdout.split('\n')) {
|
|
1259
1388
|
const p = line.trim();
|
|
1260
1389
|
if (!p)
|
|
1261
1390
|
continue;
|
|
1262
1391
|
if (p === 'access.md' || p.endsWith('/access.md'))
|
|
1263
1392
|
out.push(p);
|
|
1393
|
+
else if (p.startsWith(`${PLUGINS_DIR}/`) && p.endsWith(`/${PLUGIN_MANIFEST_FILE}`)) {
|
|
1394
|
+
pluginDirs.push(p.slice(0, -(PLUGIN_MANIFEST_FILE.length + 1)));
|
|
1395
|
+
}
|
|
1264
1396
|
}
|
|
1265
|
-
return out;
|
|
1397
|
+
return { accessFiles: out, pluginDirs };
|
|
1266
1398
|
}
|
|
1267
1399
|
catch {
|
|
1268
1400
|
return 'error';
|
|
@@ -1407,7 +1539,7 @@ export class AccessControlService {
|
|
|
1407
1539
|
console.warn(`[access@${tag}] listing access.md files failed; refusing to decide from a partial tree`);
|
|
1408
1540
|
throw new AccessUnreadableError(label, 'access.md (ls-tree)');
|
|
1409
1541
|
}
|
|
1410
|
-
for (const p of listed) {
|
|
1542
|
+
for (const p of listed.accessFiles) {
|
|
1411
1543
|
const text = await read(p);
|
|
1412
1544
|
if (text === null)
|
|
1413
1545
|
continue;
|
|
@@ -1419,6 +1551,16 @@ export class AccessControlService {
|
|
|
1419
1551
|
// duplicates, but be defensive.
|
|
1420
1552
|
accessFiles.set(parsed.file.dir, parsed.file);
|
|
1421
1553
|
}
|
|
1554
|
+
// Mirror `loadModel`: plugin principals from the plugins the ref carries,
|
|
1555
|
+
// each under the identity its manifest AT THAT REF declares, so a PR-time
|
|
1556
|
+
// verdict on a skill granted to `plugin/<name>/write` counts the plugin's
|
|
1557
|
+
// writers exactly as the working tree would.
|
|
1558
|
+
const pluginIdentities = new Map();
|
|
1559
|
+
for (const dir of listed.pluginDirs) {
|
|
1560
|
+
const manifest = parseManifestText(await read(`${dir}/${PLUGIN_MANIFEST_FILE}`));
|
|
1561
|
+
pluginIdentities.set(dir, pluginIdentityOf(manifest, path.posix.basename(dir)));
|
|
1562
|
+
}
|
|
1563
|
+
synthesizePluginPrincipals(rolesParsed.index, accessFiles, pluginIdentities);
|
|
1422
1564
|
// Mirror `loadModel`: drop entries whose role ref is unknown to roles.yaml,
|
|
1423
1565
|
// while preserving the built-in `everyone` role.
|
|
1424
1566
|
for (const [, file] of accessFiles) {
|