@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
|
@@ -4,6 +4,8 @@ import { execFile, spawn } from 'node:child_process';
|
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
|
|
7
|
+
import { isAbsence } from '../../shared/fs-errors.js';
|
|
8
|
+
import { walkKb, type KbWalkListener } from '../../shared/kb-walk.js';
|
|
7
9
|
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
8
10
|
import type {
|
|
9
11
|
IAccessControl,
|
|
@@ -13,7 +15,11 @@ import type {
|
|
|
13
15
|
GrantSources,
|
|
14
16
|
ResolvedPrincipal,
|
|
15
17
|
} from './access-control.interface.js';
|
|
18
|
+
import { PLUGINS_DIR, PLUGIN_MANIFEST_FILE, isPersonalPluginDir,
|
|
19
|
+
pluginIdentityOf,
|
|
20
|
+
} from '@bevel-software/platform-shared';
|
|
16
21
|
import { AccessConfigError, AccessUnreadableError } from '../access-model/access-errors.js';
|
|
22
|
+
import { synthesizePluginPrincipals } from '../access-model/plugin-principals.js';
|
|
17
23
|
import {
|
|
18
24
|
GROUPS_YAML,
|
|
19
25
|
SYNCED_GROUPS_YAML,
|
|
@@ -158,12 +164,6 @@ function roleKnown(roles: RolesIndex, canonicalRole: string): boolean {
|
|
|
158
164
|
*/
|
|
159
165
|
export type GroupsHealth = { ok: true } | { ok: false; file: string; reason: string };
|
|
160
166
|
|
|
161
|
-
/** True for the errno codes that mean "the file genuinely is not there". */
|
|
162
|
-
function isAbsenceError(err: unknown): boolean {
|
|
163
|
-
const code = (err as NodeJS.ErrnoException | null)?.code;
|
|
164
|
-
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
165
|
-
}
|
|
166
|
-
|
|
167
167
|
/**
|
|
168
168
|
* Load the ACTIVE group source through `read` (working tree or at-ref — the
|
|
169
169
|
* caller supplies the reader, so both model loaders share one mode rule):
|
|
@@ -201,7 +201,7 @@ export async function loadActiveGroups(
|
|
|
201
201
|
// subprocess. roles.yaml and access.md already fail the build closed on
|
|
202
202
|
// the same error; groups cannot be the one input that does not.
|
|
203
203
|
if (err instanceof AccessUnreadableError) throw err;
|
|
204
|
-
if (
|
|
204
|
+
if (isAbsence(err)) {
|
|
205
205
|
syncedText = null;
|
|
206
206
|
} else {
|
|
207
207
|
// A non-absence read error on the SYNCED source must NOT fall back to
|
|
@@ -220,7 +220,7 @@ export async function loadActiveGroups(
|
|
|
220
220
|
text = await read(GROUPS_YAML);
|
|
221
221
|
} catch (err) {
|
|
222
222
|
if (err instanceof AccessUnreadableError) throw err; // see above
|
|
223
|
-
if (
|
|
223
|
+
if (isAbsence(err)) text = null;
|
|
224
224
|
else return broken(GROUPS_YAML, err instanceof Error ? err.message : String(err));
|
|
225
225
|
}
|
|
226
226
|
}
|
|
@@ -253,12 +253,22 @@ export type ScopeSource = { kind: 'own' } | { kind: 'access-md'; path: string };
|
|
|
253
253
|
* already folded in (grant-only — see `buildScope`), so `byRole`/`byEmail`
|
|
254
254
|
* hold the *effective* verdict each named principal gets at this one scope.
|
|
255
255
|
*
|
|
256
|
+
* `byRole` and `byEmail` hold FILE-BACKED entries only — every key is a
|
|
257
|
+
* token some line in the file spells, which is what makes them the source
|
|
258
|
+
* of "what can be removed here" (`grantSources`). `everyone` is the one
|
|
259
|
+
* DERIVED verdict: what an unnamed signed-in caller gets at this scope. It
|
|
260
|
+
* is the literal `everyone` entry's state, promoted to a grant when the
|
|
261
|
+
* scope grants a PUBLIC plugin principal (one whose plugin admits everyone,
|
|
262
|
+
* so everyone holds it). Kept apart from `byRole` on purpose: a synthetic
|
|
263
|
+
* `everyone` key there would read as a line to revoke that no file has.
|
|
264
|
+
*
|
|
256
265
|
* `source` identifies the file the scope's rules come from (see `ScopeSource`),
|
|
257
266
|
* so a caller can map a per-scope verdict back to the editable file.
|
|
258
267
|
*/
|
|
259
268
|
interface AccessScope {
|
|
260
269
|
byRole: Map<string, GrantState>;
|
|
261
270
|
byEmail: Map<string, GrantState>;
|
|
271
|
+
everyone: GrantState | undefined;
|
|
262
272
|
source: ScopeSource;
|
|
263
273
|
}
|
|
264
274
|
|
|
@@ -273,6 +283,67 @@ interface AccessScope {
|
|
|
273
283
|
* `hasPermissionResolved`). `collapseScopes` flattens this into the
|
|
274
284
|
* closest-wins-per-principal view the display helpers use.
|
|
275
285
|
*/
|
|
286
|
+
/**
|
|
287
|
+
* The rules listener on the one walk of the checkout: every `access.md` at
|
|
288
|
+
* any depth (the access tree is structure-agnostic — the root's included),
|
|
289
|
+
* plus every plugin manifest under the plugins root, for principal
|
|
290
|
+
* synthesis. A reader: a folder the walk could not list is simply not in
|
|
291
|
+
* the model (a writer that needs completeness asks the walk for its holes).
|
|
292
|
+
*
|
|
293
|
+
* Per-file failures (malformed YAML, bad role refs, unknown verbs) are
|
|
294
|
+
* logged and the offending file is dropped from the model — they do NOT
|
|
295
|
+
* throw. A typo in one nested access.md must not 500 the entire editor;
|
|
296
|
+
* admins can still write `access.md` / `roles.yaml` because
|
|
297
|
+
* `hasPermissionResolved` admin-rescues those paths, so the bad config
|
|
298
|
+
* remains fixable from inside the app.
|
|
299
|
+
*/
|
|
300
|
+
function accessFileListener(
|
|
301
|
+
repoDir: string,
|
|
302
|
+
out: Map<string, AccessFile>,
|
|
303
|
+
pluginDirs: Map<string, string>,
|
|
304
|
+
): KbWalkListener {
|
|
305
|
+
return {
|
|
306
|
+
async onFile(dir, name) {
|
|
307
|
+
const rel = dir ? `${dir}/${name}` : name;
|
|
308
|
+
if (name === PLUGIN_MANIFEST_FILE && dir.startsWith(`${PLUGINS_DIR}/`)) {
|
|
309
|
+
const text = await fs.readFile(path.join(repoDir, rel), 'utf-8').catch(() => null);
|
|
310
|
+
pluginDirs.set(dir, pluginIdentityOf(parseManifestText(text), path.posix.basename(dir)));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (name !== 'access.md') return;
|
|
314
|
+
const text = await fs.readFile(path.join(repoDir, rel), 'utf-8');
|
|
315
|
+
const parsed = parseAccessFile(text, rel);
|
|
316
|
+
if (!parsed.ok) {
|
|
317
|
+
// Treat as if the file didn't exist for resolution purposes.
|
|
318
|
+
// Admin-rescue on access.md paths still lets an admin fix it.
|
|
319
|
+
for (const e of parsed.errors) console.warn(`[access] ${e} — file ignored`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
for (const w of parsed.warnings) console.warn(`[access] ${w}`);
|
|
323
|
+
if (out.has(parsed.file.dir)) {
|
|
324
|
+
console.warn(
|
|
325
|
+
`[access] ${rel}: duplicate access.md for directory '${parsed.file.dir}' — keeping the first one seen`,
|
|
326
|
+
);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
out.set(parsed.file.dir, parsed.file);
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** A manifest's parsed object, or null for absent, unparsable or not-an-object text. */
|
|
335
|
+
function parseManifestText(text: string | null): Record<string, unknown> | null {
|
|
336
|
+
if (text === null) return null;
|
|
337
|
+
try {
|
|
338
|
+
const parsed: unknown = JSON.parse(text);
|
|
339
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
340
|
+
? (parsed as Record<string, unknown>)
|
|
341
|
+
: null;
|
|
342
|
+
} catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
276
347
|
function resolveScopes(
|
|
277
348
|
model: AccessModel,
|
|
278
349
|
verb: Verb,
|
|
@@ -311,7 +382,29 @@ function resolveScopes(
|
|
|
311
382
|
set(entry, 'grant');
|
|
312
383
|
}
|
|
313
384
|
}
|
|
314
|
-
|
|
385
|
+
// The anonymous caller's verdict at this scope — what someone who holds
|
|
386
|
+
// exactly the PUBLIC keys (a plugin principal whose plugin admits
|
|
387
|
+
// everyone, so everyone holds it) resolves to, by the same tiers a named
|
|
388
|
+
// caller gets: a grant via any public key wins, else a denial via one
|
|
389
|
+
// denies, else the built-in `everyone` entry decides. Derived HERE, where
|
|
390
|
+
// a scope's verdicts are built, so every reader of it — the permission
|
|
391
|
+
// check, the eligible lists, the `restricted` flag, the compiler's
|
|
392
|
+
// everyone audience — sees one truth; and derived into its own field,
|
|
393
|
+
// never into `byRole`, so the entries a person can remove stay exactly
|
|
394
|
+
// the lines the files hold.
|
|
395
|
+
let publicGrant = false;
|
|
396
|
+
let publicDeny = false;
|
|
397
|
+
for (const [key, state] of byRole) {
|
|
398
|
+
if (!model.roles.publicKeys?.has(key)) continue;
|
|
399
|
+
if (state === 'grant') publicGrant = true;
|
|
400
|
+
else if (state === 'denied') publicDeny = true;
|
|
401
|
+
}
|
|
402
|
+
const everyone: GrantState | undefined = publicGrant
|
|
403
|
+
? 'grant'
|
|
404
|
+
: publicDeny
|
|
405
|
+
? 'denied'
|
|
406
|
+
: byRole.get(EVERYONE_CANONICAL);
|
|
407
|
+
return { byRole, byEmail, everyone, source };
|
|
315
408
|
};
|
|
316
409
|
|
|
317
410
|
const scopes: AccessScope[] = [];
|
|
@@ -328,7 +421,7 @@ function resolveScopes(
|
|
|
328
421
|
|
|
329
422
|
/** The collapsed closest-wins view: per-principal verdicts with no single
|
|
330
423
|
* source (it's a flattening across scopes). */
|
|
331
|
-
type CollapsedScope = Pick<AccessScope, 'byRole' | 'byEmail'>;
|
|
424
|
+
type CollapsedScope = Pick<AccessScope, 'byRole' | 'byEmail' | 'everyone'>;
|
|
332
425
|
|
|
333
426
|
/**
|
|
334
427
|
* Flatten ordered scopes (closest→farthest) into a single closest-wins
|
|
@@ -339,11 +432,13 @@ type CollapsedScope = Pick<AccessScope, 'byRole' | 'byEmail'>;
|
|
|
339
432
|
function collapseScopes(scopes: AccessScope[]): CollapsedScope {
|
|
340
433
|
const byRole = new Map<string, GrantState>();
|
|
341
434
|
const byEmail = new Map<string, GrantState>();
|
|
435
|
+
let everyone: GrantState | undefined;
|
|
342
436
|
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
343
437
|
for (const [k, v] of scopes[i].byRole) byRole.set(k, v);
|
|
344
438
|
for (const [k, v] of scopes[i].byEmail) byEmail.set(k, v);
|
|
439
|
+
if (scopes[i].everyone !== undefined) everyone = scopes[i].everyone;
|
|
345
440
|
}
|
|
346
|
-
return { byRole, byEmail };
|
|
441
|
+
return { byRole, byEmail, everyone };
|
|
347
442
|
}
|
|
348
443
|
|
|
349
444
|
function resolveAtPath(
|
|
@@ -355,6 +450,19 @@ function resolveAtPath(
|
|
|
355
450
|
return collapseScopes(resolveScopes(model, verb, relativePath, fileOwn));
|
|
356
451
|
}
|
|
357
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Every principal key a caller holds: the tokens their email is a member of,
|
|
455
|
+
* plus the PUBLIC keys every caller holds regardless (a plugin principal
|
|
456
|
+
* whose plugin grants `everyone`). Undefined when there is nothing at all,
|
|
457
|
+
* matching the plain `byEmail.get` the tier-2 check used to read.
|
|
458
|
+
*/
|
|
459
|
+
function principalKeysOf(model: AccessModel, email: string): Set<string> | undefined {
|
|
460
|
+
const own = model.roles.byEmail.get(email);
|
|
461
|
+
const pub = model.roles.publicKeys;
|
|
462
|
+
if (!pub?.size) return own;
|
|
463
|
+
return new Set([...(own ?? []), ...pub]);
|
|
464
|
+
}
|
|
465
|
+
|
|
358
466
|
function isAdminEmail(model: AccessModel, email: string): boolean {
|
|
359
467
|
if (model.deploymentOwners.has(email)) return true;
|
|
360
468
|
const roles = model.roles.byEmail.get(email);
|
|
@@ -411,7 +519,7 @@ function hasPermissionResolved(
|
|
|
411
519
|
if (isAccessMdPath(relativePath) && isAdminEmail(model, email)) return true;
|
|
412
520
|
}
|
|
413
521
|
|
|
414
|
-
const userRoles = model
|
|
522
|
+
const userRoles = principalKeysOf(model, email);
|
|
415
523
|
const scopes = resolveScopes(model, verb, relativePath, fileOwn);
|
|
416
524
|
|
|
417
525
|
for (const scope of scopes) {
|
|
@@ -434,9 +542,9 @@ function hasPermissionResolved(
|
|
|
434
542
|
if (deny) return false;
|
|
435
543
|
}
|
|
436
544
|
|
|
437
|
-
// Tier 3 — the built-in `everyone` role
|
|
438
|
-
|
|
439
|
-
if (everyone) return everyone === 'grant';
|
|
545
|
+
// Tier 3 — the built-in `everyone` role (derived: a public plugin
|
|
546
|
+
// principal granted here counts, see `AccessScope.everyone`).
|
|
547
|
+
if (scope.everyone) return scope.everyone === 'grant';
|
|
440
548
|
|
|
441
549
|
// No verdict at this scope — fall through to the next (farther) one.
|
|
442
550
|
}
|
|
@@ -600,11 +708,11 @@ function canEveryoneReadResolved(
|
|
|
600
708
|
relativePath: string,
|
|
601
709
|
fileOwn?: OwnEntries | null,
|
|
602
710
|
): boolean {
|
|
603
|
-
const {
|
|
711
|
+
const { byEmail, everyone } = resolveAtPath(model, 'read', relativePath, fileOwn);
|
|
604
712
|
// Baseline: an unnamed signed-in user (no email/role entries) reads only via
|
|
605
713
|
// `everyone`. As a single principal, its collapsed verdict is its closest —
|
|
606
714
|
// exactly what that user resolves to.
|
|
607
|
-
if (
|
|
715
|
+
if (everyone !== 'grant') return false;
|
|
608
716
|
// Every *named* principal must also still read. A collapsed deny may have
|
|
609
717
|
// been shadowed by a closer-scope `everyone` grant, so re-resolve each
|
|
610
718
|
// candidate through the closeness-first gate rather than trusting the
|
|
@@ -645,7 +753,7 @@ function eligibleHoldersResolved(
|
|
|
645
753
|
relativePath: string,
|
|
646
754
|
fileOwn?: OwnEntries | null,
|
|
647
755
|
): { principals: ResolvedPrincipal[]; roles: string[]; users: { name: string; email: string }[] } {
|
|
648
|
-
const { byRole, byEmail } = resolveAtPath(model, verb, relativePath, fileOwn);
|
|
756
|
+
const { byRole, byEmail, everyone } = resolveAtPath(model, verb, relativePath, fileOwn);
|
|
649
757
|
|
|
650
758
|
// (kind, display name) → one entry. The `byRole` keys are the merged
|
|
651
759
|
// index's canonical tokens exactly as granted (bare, or the
|
|
@@ -659,7 +767,7 @@ function eligibleHoldersResolved(
|
|
|
659
767
|
// principals, and collapsing them to one would hide the role's live
|
|
660
768
|
// `role/<name>` grant from every consumer of the eligible list.
|
|
661
769
|
const byIdentity = new Map<string, ResolvedPrincipal>();
|
|
662
|
-
const addPrincipal = (name: string, kind: '
|
|
770
|
+
const addPrincipal = (name: string, kind: ResolvedPrincipal['kind']) => {
|
|
663
771
|
const key = `${kind}\0${name.toLowerCase()}`;
|
|
664
772
|
if (!byIdentity.has(key)) byIdentity.set(key, { name, kind });
|
|
665
773
|
};
|
|
@@ -668,6 +776,11 @@ function eligibleHoldersResolved(
|
|
|
668
776
|
const record = model.roles.byCanonical.get(canonical);
|
|
669
777
|
addPrincipal(record ? record.displayName : canonical, record?.kind ?? 'role');
|
|
670
778
|
}
|
|
779
|
+
// The derived everyone verdict: a public plugin principal granted here
|
|
780
|
+
// means anyone signed in holds the verb, and a list that counts holders (an
|
|
781
|
+
// approval gate, a "restricted to" banner) must say so — as `everyone`, the
|
|
782
|
+
// same row a literal grant produces.
|
|
783
|
+
if (everyone === 'grant') addPrincipal(EVERYONE_CANONICAL, 'role');
|
|
671
784
|
|
|
672
785
|
// Mirror the admin overrides applied in `hasPermissionResolved`: write on
|
|
673
786
|
// `roles.yaml` and on any `access.md` is granted to Admin even if the
|
|
@@ -871,8 +984,31 @@ export class AccessControlService implements IAccessControl {
|
|
|
871
984
|
invalidate(workspaceId: string): void {
|
|
872
985
|
this.cache.delete(workspaceId);
|
|
873
986
|
this.ownEntriesCache.delete(workspaceId);
|
|
987
|
+
this.generation++;
|
|
874
988
|
}
|
|
875
989
|
|
|
990
|
+
/**
|
|
991
|
+
* Bumped by every `invalidate()`, for any workspace. A model load reads the
|
|
992
|
+
* tree across many awaits, and an invalidation fires exactly when that tree
|
|
993
|
+
* changes underneath it: the load that started on the old tree would
|
|
994
|
+
* otherwise store its model AFTER the drop, and every read for the next TTL
|
|
995
|
+
* would resolve against a tree that is already gone. `loadModel` takes the
|
|
996
|
+
* generation before its first read and refuses to store a model fetched
|
|
997
|
+
* under an older one — the caller that started it still gets its answer,
|
|
998
|
+
* as old as the moment it started, and the next caller reloads.
|
|
999
|
+
*
|
|
1000
|
+
* ONE counter, not one per workspace: a per-workspace map would keep an
|
|
1001
|
+
* entry for every branch workspace the process ever loaded, with nothing to
|
|
1002
|
+
* tell it when a workspace is gone. The price of sharing is that a drop on
|
|
1003
|
+
* some other workspace, landing mid-load, costs this one a single reload —
|
|
1004
|
+
* a load is tens of milliseconds and a drop is a commit, so that is rare
|
|
1005
|
+
* and cheap, and it never stores a wrong model. (The own-entries memo needs
|
|
1006
|
+
* no token: it is keyed by the map object a reader took BEFORE its disk
|
|
1007
|
+
* read, and `invalidate()` replaces the map, so a late store lands in an
|
|
1008
|
+
* orphan nobody consults.)
|
|
1009
|
+
*/
|
|
1010
|
+
private generation = 0;
|
|
1011
|
+
|
|
876
1012
|
/** Memoized `readOwnEntries` for the batch paths; see `ownEntriesCache`. */
|
|
877
1013
|
private async cachedOwnEntries(
|
|
878
1014
|
workspaceId: string,
|
|
@@ -1031,17 +1167,30 @@ export class AccessControlService implements IAccessControl {
|
|
|
1031
1167
|
principals: ResolvedPrincipal[];
|
|
1032
1168
|
roles: string[];
|
|
1033
1169
|
users: { name: string; email: string }[];
|
|
1170
|
+
publicVia: string[];
|
|
1034
1171
|
}> {
|
|
1035
1172
|
const model = await this.loadModel(workspaceId);
|
|
1036
1173
|
const own = await this.readOwnEntries(await this.repoDir(workspaceId), relativePath);
|
|
1037
|
-
//
|
|
1038
|
-
// and the
|
|
1039
|
-
//
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1174
|
+
// `restricted` is the verdict — whether `read: everyone` applies cleanly —
|
|
1175
|
+
// and the lists are the GRANTS that exist regardless: on a public node
|
|
1176
|
+
// they name what makes it public (`everyone`, and a public plugin
|
|
1177
|
+
// principal whose grant is the one to remove), so the share dialog can
|
|
1178
|
+
// show that grant as a row. Consumers that only want to know whether the
|
|
1179
|
+
// node is public read the flag; the lists may be empty for a
|
|
1180
|
+
// default-denied path with no grants.
|
|
1043
1181
|
const { principals, roles, users } = eligibleHoldersResolved(model, 'read', relativePath, own);
|
|
1044
|
-
|
|
1182
|
+
// WHY it is public, when it is: the public plugin principals granted read
|
|
1183
|
+
// here — the one thing the dialog cannot tell from the lists alone (a
|
|
1184
|
+
// plugin principal in them may or may not be public), so the resolver,
|
|
1185
|
+
// which knows, says it. A literal `everyone` line shows up as a source
|
|
1186
|
+
// of the `everyone` row instead; together the two answer "what remains
|
|
1187
|
+
// public after this grant is removed".
|
|
1188
|
+
const collapsed = resolveAtPath(model, 'read', relativePath, own);
|
|
1189
|
+
const publicVia = [...collapsed.byRole]
|
|
1190
|
+
.filter(([key, state]) => state === 'grant' && model.roles.publicKeys?.has(key))
|
|
1191
|
+
.map(([key]) => key)
|
|
1192
|
+
.sort();
|
|
1193
|
+
return { restricted: !canEveryoneReadResolved(model, relativePath, own), principals, roles, users, publicVia };
|
|
1045
1194
|
}
|
|
1046
1195
|
|
|
1047
1196
|
async eligibleDownloaders(
|
|
@@ -1106,14 +1255,17 @@ export class AccessControlService implements IAccessControl {
|
|
|
1106
1255
|
return out;
|
|
1107
1256
|
}
|
|
1108
1257
|
|
|
1109
|
-
async kbPrincipals(
|
|
1110
|
-
|
|
1111
|
-
|
|
1258
|
+
async kbPrincipals(workspaceId: string): Promise<{
|
|
1259
|
+
roles: string[];
|
|
1260
|
+
groups: string[];
|
|
1261
|
+
plugins: { name: string; folder: string }[];
|
|
1262
|
+
people: { name: string; email: string }[];
|
|
1263
|
+
}> {
|
|
1112
1264
|
let model: AccessModel;
|
|
1113
1265
|
try {
|
|
1114
1266
|
model = await this.loadModel(workspaceId);
|
|
1115
1267
|
} catch {
|
|
1116
|
-
return { roles: [], groups: [], people: [] };
|
|
1268
|
+
return { roles: [], groups: [], plugins: [], people: [] };
|
|
1117
1269
|
}
|
|
1118
1270
|
// Roles = the built-in `everyone` role plus every declared role's display
|
|
1119
1271
|
// name — ROLE principals only, never groups. Each role is enumerated via
|
|
@@ -1125,9 +1277,22 @@ export class AccessControlService implements IAccessControl {
|
|
|
1125
1277
|
// direct-access.md edit).
|
|
1126
1278
|
const roles = [EVERYONE_DISPLAY];
|
|
1127
1279
|
const groups: string[] = [];
|
|
1280
|
+
// Plugins = the identities behind the synthesised `plugin/<name>/<verb>`
|
|
1281
|
+
// principals, once each (three keys share a folder). Personal shelves are
|
|
1282
|
+
// plugins to the resolver but not to a person picking a grantee — told
|
|
1283
|
+
// apart by their FOLDER (the one structural rule), never by their name.
|
|
1284
|
+
const plugins = new Map<string, string>();
|
|
1128
1285
|
for (const [key, principal] of model.roles.byCanonical) {
|
|
1129
1286
|
if (key.startsWith(ROLE_TOKEN_PREFIX)) roles.push(principal.displayName);
|
|
1130
1287
|
else if (principal.kind === 'group') groups.push(principal.displayName);
|
|
1288
|
+
else if (
|
|
1289
|
+
principal.kind === 'plugin' &&
|
|
1290
|
+
principal.pluginName &&
|
|
1291
|
+
principal.pluginDir &&
|
|
1292
|
+
!isPersonalPluginDir(principal.pluginDir)
|
|
1293
|
+
) {
|
|
1294
|
+
plugins.set(principal.pluginName, principal.pluginDir);
|
|
1295
|
+
}
|
|
1131
1296
|
}
|
|
1132
1297
|
// People = roles.yaml member emails (name-less) ∪ access.md `Name <email>`
|
|
1133
1298
|
// grants (named). The login-only users table is unioned in by the caller.
|
|
@@ -1148,7 +1313,14 @@ export class AccessControlService implements IAccessControl {
|
|
|
1148
1313
|
name: name || email.split('@')[0],
|
|
1149
1314
|
email,
|
|
1150
1315
|
}));
|
|
1151
|
-
return {
|
|
1316
|
+
return {
|
|
1317
|
+
roles,
|
|
1318
|
+
groups,
|
|
1319
|
+
plugins: [...plugins.entries()]
|
|
1320
|
+
.map(([name, folder]) => ({ name, folder }))
|
|
1321
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
1322
|
+
people,
|
|
1323
|
+
};
|
|
1152
1324
|
}
|
|
1153
1325
|
|
|
1154
1326
|
async findEmailByHash(
|
|
@@ -1347,6 +1519,8 @@ export class AccessControlService implements IAccessControl {
|
|
|
1347
1519
|
if (cached && Date.now() - cached.loadedAt < AccessControlService.CACHE_TTL_MS) {
|
|
1348
1520
|
return cached.model;
|
|
1349
1521
|
}
|
|
1522
|
+
// Taken before the first read; see `generation`.
|
|
1523
|
+
const generation = this.generation;
|
|
1350
1524
|
|
|
1351
1525
|
const repoDir = await this.repoDir(workspaceId);
|
|
1352
1526
|
|
|
@@ -1404,7 +1578,15 @@ export class AccessControlService implements IAccessControl {
|
|
|
1404
1578
|
// must not 500 the entire editor; admins can still write `access.md`
|
|
1405
1579
|
// / `roles.yaml` because `hasPermissionResolved` admin-rescues those
|
|
1406
1580
|
// paths, so the bad config remains fixable from inside the app.
|
|
1407
|
-
|
|
1581
|
+
const pluginDirs = new Map<string, string>();
|
|
1582
|
+
await walkKb(repoDir, [accessFileListener(repoDir, accessFiles, pluginDirs)]);
|
|
1583
|
+
|
|
1584
|
+
// Plugin principals (`plugin/<Name>/<verb>`), derived from each plugin
|
|
1585
|
+
// folder's own access.md — a plugin being a folder with a manifest, at
|
|
1586
|
+
// any depth — after groups merged (so their rosters expand through the
|
|
1587
|
+
// finished index) and before the unknown-role sweep below (so a grant
|
|
1588
|
+
// naming one counts as known).
|
|
1589
|
+
synthesizePluginPrincipals(rolesParsed.index, accessFiles, pluginDirs);
|
|
1408
1590
|
|
|
1409
1591
|
// Validate role refs against roles.yaml. `everyone` is a built-in role and
|
|
1410
1592
|
// is valid without a roles.yaml entry. Unknown refs are dropped from the
|
|
@@ -1430,50 +1612,12 @@ export class AccessControlService implements IAccessControl {
|
|
|
1430
1612
|
groupsHealth: activeGroups.health,
|
|
1431
1613
|
deploymentOwners: this.deploymentOwners,
|
|
1432
1614
|
};
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
private async collectAccessFiles(
|
|
1438
|
-
absDir: string,
|
|
1439
|
-
relDir: string,
|
|
1440
|
-
out: Map<string, AccessFile>,
|
|
1441
|
-
): Promise<void> {
|
|
1442
|
-
let entries;
|
|
1443
|
-
try {
|
|
1444
|
-
entries = await fs.readdir(absDir, { withFileTypes: true });
|
|
1445
|
-
} catch {
|
|
1446
|
-
return;
|
|
1447
|
-
}
|
|
1448
|
-
for (const entry of entries) {
|
|
1449
|
-
// Skip VCS metadata + vendored deps. Hidden dirs (`.git`, `.vscode`,
|
|
1450
|
-
// etc.) and `node_modules` can't host KB rules and are often huge —
|
|
1451
|
-
// walking them would slow every cache miss without benefit.
|
|
1452
|
-
if (entry.name.startsWith('.')) continue;
|
|
1453
|
-
if (entry.name === 'node_modules') continue;
|
|
1454
|
-
const abs = path.join(absDir, entry.name);
|
|
1455
|
-
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
1456
|
-
if (entry.isDirectory()) {
|
|
1457
|
-
await this.collectAccessFiles(abs, rel, out);
|
|
1458
|
-
} else if (entry.isFile() && entry.name === 'access.md') {
|
|
1459
|
-
const text = await fs.readFile(abs, 'utf-8');
|
|
1460
|
-
const parsed = parseAccessFile(text, rel);
|
|
1461
|
-
if (!parsed.ok) {
|
|
1462
|
-
// Treat as if the file didn't exist for resolution purposes.
|
|
1463
|
-
// Admin-rescue on access.md paths still lets an admin fix it.
|
|
1464
|
-
for (const e of parsed.errors) console.warn(`[access] ${e} — file ignored`);
|
|
1465
|
-
continue;
|
|
1466
|
-
}
|
|
1467
|
-
for (const w of parsed.warnings) console.warn(`[access] ${w}`);
|
|
1468
|
-
if (out.has(parsed.file.dir)) {
|
|
1469
|
-
console.warn(
|
|
1470
|
-
`[access] ${rel}: duplicate access.md for directory '${parsed.file.dir}' — keeping the first one seen`,
|
|
1471
|
-
);
|
|
1472
|
-
continue;
|
|
1473
|
-
}
|
|
1474
|
-
out.set(parsed.file.dir, parsed.file);
|
|
1475
|
-
}
|
|
1615
|
+
// An invalidation landed mid-load: this model describes a tree that is
|
|
1616
|
+
// already gone, so it is returned to this caller but never stored.
|
|
1617
|
+
if (this.generation === generation) {
|
|
1618
|
+
this.cache.set(workspaceId, { model, loadedAt: Date.now() });
|
|
1476
1619
|
}
|
|
1620
|
+
return model;
|
|
1477
1621
|
}
|
|
1478
1622
|
|
|
1479
1623
|
private async repoDir(workspaceId: string): Promise<string> {
|
|
@@ -1614,7 +1758,10 @@ export class AccessControlService implements IAccessControl {
|
|
|
1614
1758
|
* `'error'` when git failed: a model built without its access files is
|
|
1615
1759
|
* not the model, and the caller must not cache it as one.
|
|
1616
1760
|
*/
|
|
1617
|
-
private async listAccessFilesAtRef(
|
|
1761
|
+
private async listAccessFilesAtRef(
|
|
1762
|
+
repoDir: string,
|
|
1763
|
+
ref: string,
|
|
1764
|
+
): Promise<{ accessFiles: string[]; pluginDirs: string[] } | 'error'> {
|
|
1618
1765
|
try {
|
|
1619
1766
|
const { stdout } = await execFileAsync(
|
|
1620
1767
|
'git',
|
|
@@ -1622,12 +1769,16 @@ export class AccessControlService implements IAccessControl {
|
|
|
1622
1769
|
{ encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 },
|
|
1623
1770
|
);
|
|
1624
1771
|
const out: string[] = [];
|
|
1772
|
+
const pluginDirs: string[] = [];
|
|
1625
1773
|
for (const line of stdout.split('\n')) {
|
|
1626
1774
|
const p = line.trim();
|
|
1627
1775
|
if (!p) continue;
|
|
1628
1776
|
if (p === 'access.md' || p.endsWith('/access.md')) out.push(p);
|
|
1777
|
+
else if (p.startsWith(`${PLUGINS_DIR}/`) && p.endsWith(`/${PLUGIN_MANIFEST_FILE}`)) {
|
|
1778
|
+
pluginDirs.push(p.slice(0, -(PLUGIN_MANIFEST_FILE.length + 1)));
|
|
1779
|
+
}
|
|
1629
1780
|
}
|
|
1630
|
-
return out;
|
|
1781
|
+
return { accessFiles: out, pluginDirs };
|
|
1631
1782
|
} catch {
|
|
1632
1783
|
return 'error';
|
|
1633
1784
|
}
|
|
@@ -1781,7 +1932,7 @@ export class AccessControlService implements IAccessControl {
|
|
|
1781
1932
|
console.warn(`[access@${tag}] listing access.md files failed; refusing to decide from a partial tree`);
|
|
1782
1933
|
throw new AccessUnreadableError(label, 'access.md (ls-tree)');
|
|
1783
1934
|
}
|
|
1784
|
-
for (const p of listed) {
|
|
1935
|
+
for (const p of listed.accessFiles) {
|
|
1785
1936
|
const text = await read(p);
|
|
1786
1937
|
if (text === null) continue;
|
|
1787
1938
|
const parsed = parseAccessFile(text, p);
|
|
@@ -1792,6 +1943,17 @@ export class AccessControlService implements IAccessControl {
|
|
|
1792
1943
|
accessFiles.set(parsed.file.dir, parsed.file);
|
|
1793
1944
|
}
|
|
1794
1945
|
|
|
1946
|
+
// Mirror `loadModel`: plugin principals from the plugins the ref carries,
|
|
1947
|
+
// each under the identity its manifest AT THAT REF declares, so a PR-time
|
|
1948
|
+
// verdict on a skill granted to `plugin/<name>/write` counts the plugin's
|
|
1949
|
+
// writers exactly as the working tree would.
|
|
1950
|
+
const pluginIdentities = new Map<string, string>();
|
|
1951
|
+
for (const dir of listed.pluginDirs) {
|
|
1952
|
+
const manifest = parseManifestText(await read(`${dir}/${PLUGIN_MANIFEST_FILE}`));
|
|
1953
|
+
pluginIdentities.set(dir, pluginIdentityOf(manifest, path.posix.basename(dir)));
|
|
1954
|
+
}
|
|
1955
|
+
synthesizePluginPrincipals(rolesParsed.index, accessFiles, pluginIdentities);
|
|
1956
|
+
|
|
1795
1957
|
// Mirror `loadModel`: drop entries whose role ref is unknown to roles.yaml,
|
|
1796
1958
|
// while preserving the built-in `everyone` role.
|
|
1797
1959
|
for (const [, file] of accessFiles) {
|