@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
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
AcquireLockResult,
|
|
26
26
|
AuthUser,
|
|
27
27
|
Branch,
|
|
28
|
+
BranchSyncOutcome,
|
|
28
29
|
BranchWorkspaceStatus,
|
|
29
30
|
CancelChangeRequestResult,
|
|
30
31
|
Change,
|
|
@@ -40,6 +41,7 @@ import type {
|
|
|
40
41
|
MergeChangeRequestOutcome,
|
|
41
42
|
OpenChangeRequestInput,
|
|
42
43
|
PostChangeRequestCommentInput,
|
|
44
|
+
RemoteSyncPullResult,
|
|
43
45
|
} from '@bevel-software/platform-shared';
|
|
44
46
|
import { isProtectedBranch, DEFAULT_BRANCH } from '@bevel-software/platform-shared';
|
|
45
47
|
import { and, eq, or } from 'drizzle-orm';
|
|
@@ -65,6 +67,7 @@ import {
|
|
|
65
67
|
RolesYamlPreservationError,
|
|
66
68
|
PullRebaseConflictError,
|
|
67
69
|
PushNeedsAgentResolutionError,
|
|
70
|
+
RemoteBranchGoneError,
|
|
68
71
|
WorkflowDomainError,
|
|
69
72
|
WorkflowValidationError,
|
|
70
73
|
} from '../../shared/domain-errors.js';
|
|
@@ -100,6 +103,33 @@ function isOpenPairViolation(err: unknown): boolean {
|
|
|
100
103
|
return typeof message === 'string' && message.includes(OPEN_PAIR_CONSTRAINT);
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
/**
|
|
107
|
+
* The one sentence a remote-sync conflict is reported with — in the sync
|
|
108
|
+
* response, on the branch's banner and in the log — so whoever sees any of
|
|
109
|
+
* the three reads the same thing: which branch, which files, that automatic
|
|
110
|
+
* recovery is under way, and what to do if it does not clear.
|
|
111
|
+
*/
|
|
112
|
+
export function syncConflictMessage(branch: string, conflictedPaths: string[]): string {
|
|
113
|
+
// Bounded: the same sentence goes to the response, the banner and the log,
|
|
114
|
+
// and it must be one sentence in all three whatever the conflict's size.
|
|
115
|
+
// The full path list rides beside it as `conflictedPaths`.
|
|
116
|
+
const shown = conflictedPaths.slice(0, SYNC_CONFLICT_FILES_NAMED);
|
|
117
|
+
const more = conflictedPaths.length - shown.length;
|
|
118
|
+
const files =
|
|
119
|
+
conflictedPaths.length === 0
|
|
120
|
+
? 'some files'
|
|
121
|
+
: more > 0
|
|
122
|
+
? `${shown.join(', ')} and ${more} more file${more === 1 ? '' : 's'}`
|
|
123
|
+
: shown.join(', ');
|
|
124
|
+
return (
|
|
125
|
+
`${branch} is not in sync yet: ${files} changed both in Hexis and on the git host. ` +
|
|
126
|
+
`Recovery is queued; if this stays, open the files on ${branch} in Hexis, keep what you want, and save.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** How many conflicted files the sync-conflict sentence names before "and N more". */
|
|
131
|
+
const SYNC_CONFLICT_FILES_NAMED = 3;
|
|
132
|
+
|
|
103
133
|
/** Redact the shared GitHub token from any error string before surfacing it. */
|
|
104
134
|
function redactTokens(msg: string): string {
|
|
105
135
|
const tokens = [process.env.GITHUB_TOKEN, process.env.GH_TOKEN].filter(
|
|
@@ -246,7 +276,10 @@ export class WorkflowService implements IWorkflowService {
|
|
|
246
276
|
*/
|
|
247
277
|
private orphanSweepStarted = false;
|
|
248
278
|
|
|
249
|
-
async listBranches(
|
|
279
|
+
async listBranches(
|
|
280
|
+
workspaceId: string,
|
|
281
|
+
opts?: { freshFetch?: boolean; strictFetch?: boolean },
|
|
282
|
+
): Promise<Branch[]> {
|
|
250
283
|
const branches = await this.git.listBranches(workspaceId, opts);
|
|
251
284
|
if (!this.orphanSweepStarted) {
|
|
252
285
|
this.orphanSweepStarted = true;
|
|
@@ -268,7 +301,14 @@ export class WorkflowService implements IWorkflowService {
|
|
|
268
301
|
}
|
|
269
302
|
|
|
270
303
|
createBranch(workspaceId: string, name: string, fromBase?: string): Promise<Branch> {
|
|
271
|
-
|
|
304
|
+
// Under the branch's lifecycle lock, like `deleteBranch` and the
|
|
305
|
+
// retirement of a clone the host deleted: creating a branch is the one
|
|
306
|
+
// operation that can bring a "gone" branch back, and the retirement's
|
|
307
|
+
// "still gone on origin?" check is only a guarantee if nothing can
|
|
308
|
+
// recreate the branch between that check and the delete.
|
|
309
|
+
return this.branchLifecycle.run(`branch:${name}`, () =>
|
|
310
|
+
this.git.createBranch(workspaceId, name, fromBase),
|
|
311
|
+
);
|
|
272
312
|
}
|
|
273
313
|
|
|
274
314
|
/**
|
|
@@ -445,9 +485,41 @@ export class WorkflowService implements IWorkflowService {
|
|
|
445
485
|
return this.git.fetch(workspaceId);
|
|
446
486
|
}
|
|
447
487
|
|
|
488
|
+
/**
|
|
489
|
+
* `git pull` on a workspace, plus the one announcement a pull owes the rest
|
|
490
|
+
* of the process.
|
|
491
|
+
*
|
|
492
|
+
* A pull rewrites a working tree without going through the write routes or
|
|
493
|
+
* the commit pipeline, so it emits neither of the signals those produce —
|
|
494
|
+
* and the catalogs that scan the DEFAULT branch's tree (skills, tool
|
|
495
|
+
* manuals, the plugin index) are keyed to exactly that tree. Every path that
|
|
496
|
+
* pulls the default workspace therefore has to say so, or those catalogs
|
|
497
|
+
* keep serving the pre-pull scan for the rest of their TTL: a merge landing
|
|
498
|
+
* an approved skill, the recovery ladder after a non-fast-forward push, and
|
|
499
|
+
* a user's update-from-remote alike. Routing every pull through here makes
|
|
500
|
+
* that ONE mechanism — "the default branch's tree changed" — instead of a
|
|
501
|
+
* separate special case per event. (The remote sync, `syncWorkspaceFromRemote`,
|
|
502
|
+
* pulls directly: it owes the same announcement to every branch and per
|
|
503
|
+
* file, and makes it from the same `treeChanged` verdict.)
|
|
504
|
+
*
|
|
505
|
+
* Announced only after the pull RESOLVES, and only when it CHANGED the
|
|
506
|
+
* tree's content: a pull that threw left the tree as it was, and so did an
|
|
507
|
+
* "already up to date" pull or one that landed only content-identical
|
|
508
|
+
* commits — dropping every catalog and making every attached browser
|
|
509
|
+
* refetch its file tree for a change that did not happen buys a re-scan
|
|
510
|
+
* and nothing else. The pull itself reports the change, since it is the
|
|
511
|
+
* only code that can observe it under the workspace mutex.
|
|
512
|
+
*/
|
|
513
|
+
private async pullWorkspace(workspaceId: string): Promise<void> {
|
|
514
|
+
const { treeChanged } = await this.git.pull(workspaceId);
|
|
515
|
+
if (treeChanged && branchForWorkspaceId(workspaceId) === DEFAULT_BRANCH) {
|
|
516
|
+
this.events?.emit({ kind: 'fs-tree-changed', workspaceId, branch: DEFAULT_BRANCH });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
448
520
|
async updateFromRemote(workspaceId: string, user?: AuthUser): Promise<void> {
|
|
449
521
|
try {
|
|
450
|
-
await this.
|
|
522
|
+
await this.pullWorkspace(workspaceId);
|
|
451
523
|
} catch (err) {
|
|
452
524
|
if (err instanceof PullRebaseConflictError) {
|
|
453
525
|
await this.queuePullConflictRecovery(workspaceId, err, user);
|
|
@@ -456,6 +528,157 @@ export class WorkflowService implements IWorkflowService {
|
|
|
456
528
|
}
|
|
457
529
|
}
|
|
458
530
|
|
|
531
|
+
async retireRemoteGoneClone(workspaceId: string): Promise<boolean> {
|
|
532
|
+
const branch = branchForWorkspaceId(workspaceId);
|
|
533
|
+
const id = workspaceIdForBranch(branch);
|
|
534
|
+
// The lock `createBranch` and `deleteBranch` take: no Hexis operation can
|
|
535
|
+
// bring this branch back into being while the clone is examined and
|
|
536
|
+
// removed. The sync's own hold on the clone ended when its outcome was
|
|
537
|
+
// returned, so the branch is asked about AGAIN here rather than trusted
|
|
538
|
+
// from then — a recreate that landed in between keeps its clone. A clone
|
|
539
|
+
// being bootstrapped right now is left alone too: it can only be cloning
|
|
540
|
+
// a branch that exists, so the retirement is already moot. What remains
|
|
541
|
+
// is a recreate pushed from OUTSIDE Hexis in the milliseconds between the
|
|
542
|
+
// origin check and the delete, with a bootstrap starting in that same
|
|
543
|
+
// window; the next sync of that branch clones it fresh.
|
|
544
|
+
return this.branchLifecycle.run(`branch:${branch}`, async () => {
|
|
545
|
+
if (!(await this.workspaceService.hasBootstrappedWorkspace(id))) return false;
|
|
546
|
+
if (this.workspaceService.isBootstrapInFlight(branch)) return false;
|
|
547
|
+
if (await this.git.remoteBranchExists(branch, branch)) return false;
|
|
548
|
+
await this.workspaceService.deleteWorkspace(id);
|
|
549
|
+
// Whatever a sync still owed that clone is owed to nothing now.
|
|
550
|
+
this.owedAnnouncements.delete(id);
|
|
551
|
+
return true;
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Cap on per-path `file-changed` events one sync announces for a branch.
|
|
557
|
+
* Past it the tree event alone carries the news — a bulk import that
|
|
558
|
+
* rewrote hundreds of files would otherwise fan hundreds of events out to
|
|
559
|
+
* every open browser to say what one refresh says.
|
|
560
|
+
*/
|
|
561
|
+
private static readonly SYNC_FILE_EVENT_CAP = 200;
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Announcements a sync still OWES, per workspace: a pull landed but the
|
|
565
|
+
* events for it could not be sent. The pull is not redone on retry — HEAD
|
|
566
|
+
* has not moved since — so the announcement cannot be re-derived from sha
|
|
567
|
+
* movement; it is remembered here and delivered by the next sync of that
|
|
568
|
+
* workspace, merged with whatever that sync finds.
|
|
569
|
+
*/
|
|
570
|
+
private readonly owedAnnouncements = new Map<string, { after: string; changedPaths: string[] }>();
|
|
571
|
+
|
|
572
|
+
async syncWorkspaceFromRemote(workspaceId: string): Promise<BranchSyncOutcome> {
|
|
573
|
+
const branch = branchForWorkspaceId(workspaceId);
|
|
574
|
+
const id = workspaceIdForBranch(branch);
|
|
575
|
+
// Two spellings of one workspace id are in circulation — the encoded
|
|
576
|
+
// directory name (`ali%2Fx`, what the clone listing yields) and the
|
|
577
|
+
// Express-decoded form (`ali/x`, what every HTTP route hands the git
|
|
578
|
+
// layer) — and the git layer's mutex keys on the raw string. Speaking the
|
|
579
|
+
// routes' spelling to git is what puts a webhook's rebase in the same
|
|
580
|
+
// queue as the author's concurrent save on a slashed branch; the event
|
|
581
|
+
// bus gets the encoded form, which every SSE consumer canonicalises.
|
|
582
|
+
const gitId = branch;
|
|
583
|
+
// Every failure below is an OUTCOME, never a throw: the caller runs this
|
|
584
|
+
// over many clones, and one broken clone must not abort the others.
|
|
585
|
+
let pulled: RemoteSyncPullResult;
|
|
586
|
+
try {
|
|
587
|
+
pulled = await this.git.syncFromRemote(gitId);
|
|
588
|
+
} catch (err) {
|
|
589
|
+
if (err instanceof RemoteBranchGoneError) {
|
|
590
|
+
// Not a failure: the host deleted the branch, so there is nothing to
|
|
591
|
+
// sync and the caller retires the stale clone. No banner — nobody is
|
|
592
|
+
// editing a branch that no longer exists. An announcement still owed
|
|
593
|
+
// for that clone dies with it: a branch recreated under the same name
|
|
594
|
+
// is a different tree, and replaying the old paths against it would
|
|
595
|
+
// announce changes that never happened there.
|
|
596
|
+
this.owedAnnouncements.delete(id);
|
|
597
|
+
console.log(`[sync] branch "${branch}" no longer exists on the host`);
|
|
598
|
+
return { branch, outcome: 'remote-gone' };
|
|
599
|
+
}
|
|
600
|
+
if (err instanceof PullRebaseConflictError) {
|
|
601
|
+
// Same path as `updateFromRemote`: the rebase was aborted inside
|
|
602
|
+
// the pull (the clone is exactly what it was) and the divergence goes
|
|
603
|
+
// to the retry → recovery-agent → escalate ladder, attributed to the
|
|
604
|
+
// recovery bot since no person triggered this. It is still reported
|
|
605
|
+
// as a conflict because it is not resolved at request time — the
|
|
606
|
+
// caller's retry, or the ladder, settles it; the banner clears on the
|
|
607
|
+
// next clean pull or push.
|
|
608
|
+
await this.queuePullConflictRecovery(gitId, err);
|
|
609
|
+
const message = syncConflictMessage(branch, err.conflictedPaths);
|
|
610
|
+
console.warn(`[sync] ${message}`);
|
|
611
|
+
this.noteGitSyncFailed(gitId, branch, err, { paths: err.conflictedPaths, message });
|
|
612
|
+
return { branch, outcome: 'conflict', conflictedPaths: err.conflictedPaths, error: message };
|
|
613
|
+
}
|
|
614
|
+
const message = sanitizeError(err);
|
|
615
|
+
console.warn(`[sync] pull failed for branch "${branch}": ${message}`);
|
|
616
|
+
this.noteGitSyncFailed(gitId, branch, err);
|
|
617
|
+
return { branch, outcome: 'error', error: message };
|
|
618
|
+
}
|
|
619
|
+
// A clean pull is proof origin is reachable and this clone rebases onto
|
|
620
|
+
// it — the condition a `git-sync-failed` banner on this branch was
|
|
621
|
+
// waiting for, whether a sync or a push raised it.
|
|
622
|
+
this.noteGitSyncOk(gitId, branch);
|
|
623
|
+
const { before, after, treeChanged, changedPaths } = pulled;
|
|
624
|
+
// What this branch reports about ITS clone is decided by the pull alone:
|
|
625
|
+
// `updated` when HEAD moved, `up-to-date` otherwise.
|
|
626
|
+
const outcome: BranchSyncOutcome =
|
|
627
|
+
after === null || after === before
|
|
628
|
+
? { branch, outcome: 'up-to-date', to: after ?? '' }
|
|
629
|
+
: { branch, outcome: 'updated', from: before, to: after };
|
|
630
|
+
// What it ANNOUNCES is what has changed since the last announcement that
|
|
631
|
+
// was actually delivered: this pull's content change, if any, plus one an
|
|
632
|
+
// earlier sync landed but could not send. The announcement keys on the
|
|
633
|
+
// pull's verdict about CONTENT — commits that left the tree identical are
|
|
634
|
+
// nothing for a browser to refetch or a catalog to drop.
|
|
635
|
+
const owed = this.owedAnnouncements.get(id);
|
|
636
|
+
if (!treeChanged && !owed) return outcome;
|
|
637
|
+
const announce = {
|
|
638
|
+
after: after ?? owed?.after ?? '',
|
|
639
|
+
changedPaths: [...new Set([...(owed?.changedPaths ?? []), ...changedPaths])],
|
|
640
|
+
};
|
|
641
|
+
try {
|
|
642
|
+
// Announce the new tree. The tree event alone already refreshes every
|
|
643
|
+
// open file explorer and, on the default branch, drops the skill / tool /
|
|
644
|
+
// plugin catalogues (`catalog-cache-invalidation.ts`); the CR list
|
|
645
|
+
// caches the touched paths it resolved against the old tree, so drop
|
|
646
|
+
// that too.
|
|
647
|
+
this.prs.invalidateListCache();
|
|
648
|
+
this.events?.emit({ kind: 'fs-tree-changed', workspaceId: id, branch });
|
|
649
|
+
// Per-file events so an open tab on a file the host rewrote refetches
|
|
650
|
+
// instead of showing stale bytes until its next tree refresh. The paths
|
|
651
|
+
// were observed under the same hold as the pull, so they are exactly
|
|
652
|
+
// this sync's — never a save that landed alongside. Capped: a bulk
|
|
653
|
+
// import would otherwise fan hundreds of events out to say what one
|
|
654
|
+
// refresh says.
|
|
655
|
+
if (announce.changedPaths.length <= WorkflowService.SYNC_FILE_EVENT_CAP) {
|
|
656
|
+
for (const p of announce.changedPaths) {
|
|
657
|
+
this.events?.emit({
|
|
658
|
+
kind: 'file-changed',
|
|
659
|
+
workspaceId: id,
|
|
660
|
+
branch,
|
|
661
|
+
path: `${this.kbDirName}/${p}`,
|
|
662
|
+
newSha: announce.after,
|
|
663
|
+
byUserId: 'system',
|
|
664
|
+
byUserName: 'Git sync',
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
} catch (err) {
|
|
669
|
+
// The pull landed, so origin is fine and no banner is raised — but the
|
|
670
|
+
// caller must hear that this branch's announcement did not happen, and
|
|
671
|
+
// the announcement stays owed: the retry the error invites will not
|
|
672
|
+
// move HEAD again, so it must find the debt here rather than in the shas.
|
|
673
|
+
this.owedAnnouncements.set(id, announce);
|
|
674
|
+
const message = sanitizeError(err);
|
|
675
|
+
console.warn(`[sync] pulled "${branch}" but could not announce it: ${message}`);
|
|
676
|
+
return { branch, outcome: 'error', error: message };
|
|
677
|
+
}
|
|
678
|
+
this.owedAnnouncements.delete(id);
|
|
679
|
+
return outcome;
|
|
680
|
+
}
|
|
681
|
+
|
|
459
682
|
/**
|
|
460
683
|
* A pull hit a rebase conflict — the workspace carries local commits that
|
|
461
684
|
* conflict with origin, a state that never resolves on its own (every
|
|
@@ -782,7 +1005,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
782
1005
|
let recoveryError: unknown = null;
|
|
783
1006
|
if (looksLikeNonFastForward) {
|
|
784
1007
|
try {
|
|
785
|
-
await this.
|
|
1008
|
+
await this.pullWorkspace(workspaceId);
|
|
786
1009
|
await this.git.push(workspaceId, user);
|
|
787
1010
|
recovered = true;
|
|
788
1011
|
this.noteGitSyncOk(workspaceId, branch);
|
|
@@ -1033,7 +1256,34 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1033
1256
|
* machinery is unaffected — all this adds is that someone finds out, rather
|
|
1034
1257
|
* than the deployment looking healthy while nothing reaches the remote.
|
|
1035
1258
|
*/
|
|
1036
|
-
private noteGitSyncFailed(
|
|
1259
|
+
private noteGitSyncFailed(
|
|
1260
|
+
workspaceId: string,
|
|
1261
|
+
branch: string,
|
|
1262
|
+
err: unknown,
|
|
1263
|
+
/**
|
|
1264
|
+
* The files a person must reconcile, and the sentence we composed about
|
|
1265
|
+
* them. That sentence is ours (branch name + repo paths, no git stderr),
|
|
1266
|
+
* so it goes out verbatim — the same string the sync response and the
|
|
1267
|
+
* log carry — rather than through the sanitiser, whose 200-character
|
|
1268
|
+
* clip would make the banner disagree with them. Callers that already
|
|
1269
|
+
* hold the sentence pass it; otherwise it is derived below.
|
|
1270
|
+
*/
|
|
1271
|
+
explicitConflict?: { paths: string[]; message: string },
|
|
1272
|
+
): void {
|
|
1273
|
+
// A rebase conflict is a conflict whichever path ran into it. The remote
|
|
1274
|
+
// sync names its files on purpose; the push-recovery paths (autosave, the
|
|
1275
|
+
// lock release, the queued retries) raise the same banner with the raw
|
|
1276
|
+
// error — and the banner keeps the LATEST failure per branch, so a retry
|
|
1277
|
+
// landing after the sync would replace the conflict variant, with the
|
|
1278
|
+
// files as links, by the generic "check the server logs" one. Seen on
|
|
1279
|
+
// staging: the sync's 409 named the file, the worker's retry a moment
|
|
1280
|
+
// later took the links away. Derive the paths from the error itself so
|
|
1281
|
+
// every path agrees.
|
|
1282
|
+
const conflict =
|
|
1283
|
+
explicitConflict ??
|
|
1284
|
+
(err instanceof PullRebaseConflictError
|
|
1285
|
+
? { paths: err.conflictedPaths, message: syncConflictMessage(branch, err.conflictedPaths) }
|
|
1286
|
+
: undefined);
|
|
1037
1287
|
// Same canonicalization as `noteGitSyncOk` — the pair must agree on keys.
|
|
1038
1288
|
const id = branchForWorkspaceId(workspaceId);
|
|
1039
1289
|
this.gitSyncFailing.add(id);
|
|
@@ -1043,7 +1293,8 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1043
1293
|
branch,
|
|
1044
1294
|
// Git stderr can quote a credentialed URL; the banner is user-facing and
|
|
1045
1295
|
// the string also lands in client logs, so sanitize before it leaves.
|
|
1046
|
-
reason: sanitizeError(err),
|
|
1296
|
+
reason: conflict ? conflict.message : sanitizeError(err),
|
|
1297
|
+
...(conflict ? { conflictedPaths: conflict.paths } : {}),
|
|
1047
1298
|
});
|
|
1048
1299
|
}
|
|
1049
1300
|
|
|
@@ -1102,7 +1353,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1102
1353
|
let recoveryError: unknown = null;
|
|
1103
1354
|
if (looksLikeNonFastForward) {
|
|
1104
1355
|
try {
|
|
1105
|
-
await this.
|
|
1356
|
+
await this.pullWorkspace(workspaceId);
|
|
1106
1357
|
await this.git.push(workspaceId, user, opts);
|
|
1107
1358
|
recovered = true;
|
|
1108
1359
|
this.noteGitSyncOk(workspaceId, branch);
|
|
@@ -1710,7 +1961,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1710
1961
|
// refs, but the restore commits from the working tree — a stale tree
|
|
1711
1962
|
// would push non-fast-forward and fail loudly anyway; this just makes
|
|
1712
1963
|
// that rare.
|
|
1713
|
-
await this.
|
|
1964
|
+
await this.pullWorkspace(ws.id).catch(() => undefined);
|
|
1714
1965
|
|
|
1715
1966
|
// The verb acts on the request as it is NOW — never a cached file list.
|
|
1716
1967
|
const paths = await this.git.changedPathsForPr(ws.id, baseBranch, headBranch);
|
|
@@ -2221,7 +2472,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
2221
2472
|
try {
|
|
2222
2473
|
const targetWorkspace = await this.workspaceService.getOrCreateForBranch(baseBranch);
|
|
2223
2474
|
targetWorkspaceId = targetWorkspace.id;
|
|
2224
|
-
await this.
|
|
2475
|
+
await this.pullWorkspace(targetWorkspace.id);
|
|
2225
2476
|
} catch (err) {
|
|
2226
2477
|
// Still best-effort for the merge response (the merge already landed
|
|
2227
2478
|
// on origin) — but a rebase CONFLICT here means the target workspace
|
|
@@ -182,11 +182,10 @@ describe('read-permission gates on human read routes', () => {
|
|
|
182
182
|
|
|
183
183
|
it('keeps the structural root folders visible even when nothing is readable', async () => {
|
|
184
184
|
// A user with no read grants: the access batch denies everything. The
|
|
185
|
-
// structural top-level folders (KnowledgeBase, Data, Agents,
|
|
186
|
-
// Plugins) must still resolve readable (so the explorer renders
|
|
187
|
-
// section view), while their contents stay gated. The retired
|
|
188
|
-
//
|
|
189
|
-
// (denied) folders now.
|
|
185
|
+
// structural top-level folders (KnowledgeBase, Skills, Data, Agents,
|
|
186
|
+
// Pipelines, Plugins) must still resolve readable (so the explorer renders
|
|
187
|
+
// its section view), while their contents stay gated. The retired Tools/
|
|
188
|
+
// root gets NO structural treatment — it is an ordinary (denied) folder.
|
|
190
189
|
h = await makeHarness({ allowFn: () => false });
|
|
191
190
|
expect((await get('/files')).status).toBe(200);
|
|
192
191
|
const filter = h.listFilesFilter();
|
|
@@ -208,8 +207,8 @@ describe('read-permission gates on human read routes', () => {
|
|
|
208
207
|
expect(verdict.get(`${KB}/Agents`)).toBe(true);
|
|
209
208
|
expect(verdict.get(`${KB}/Pipelines`)).toBe(true);
|
|
210
209
|
expect(verdict.get(`${KB}/Plugins`)).toBe(true);
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
expect(verdict.get(`${KB}/Skills`)).toBe(true);
|
|
211
|
+
// Retired root: no override, the deny-all batch verdict stands.
|
|
213
212
|
expect(verdict.get(`${KB}/Tools`)).toBe(false);
|
|
214
213
|
// Their contents: still gated (the override matches only the exact folder).
|
|
215
214
|
expect(verdict.get(`${KB}/KnowledgeBase/Open/a.md`)).toBe(false);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { WorkspaceService } from '../workspace.service.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `listClonedWorkspaces` is what the remote sync enumerates. It must read the
|
|
9
|
+
* DISK, not the in-memory bootstrap cache: clones survive a restart, and a
|
|
10
|
+
* hook that fires before anyone has opened a branch must still find them.
|
|
11
|
+
*/
|
|
12
|
+
describe('WorkspaceService.listClonedWorkspaces', () => {
|
|
13
|
+
let root: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-ws-list-'));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
async function clone(dirName: string, withGit = true) {
|
|
24
|
+
const repo = path.join(root, dirName, 'knowledge-base');
|
|
25
|
+
await fs.mkdir(withGit ? path.join(repo, '.git') : repo, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
it('finds every finished clone on disk without any branch having been opened this process', async () => {
|
|
29
|
+
await clone('main');
|
|
30
|
+
await clone('ali%2Fnew-skill');
|
|
31
|
+
// Half-built: the directory exists but the clone never finished.
|
|
32
|
+
await clone('juan%2Fabandoned', false);
|
|
33
|
+
// Not one of ours: the name is not the encoding of any branch.
|
|
34
|
+
await clone('weird%zz');
|
|
35
|
+
// A stray file at the root is ignored.
|
|
36
|
+
await fs.writeFile(path.join(root, 'notes.txt'), 'x');
|
|
37
|
+
|
|
38
|
+
const svc = new WorkspaceService(root, 'https://example.test/kb.git', 'knowledge-base');
|
|
39
|
+
const found = await svc.listClonedWorkspaces();
|
|
40
|
+
expect(found.sort((a, b) => a.branch.localeCompare(b.branch))).toEqual([
|
|
41
|
+
{ id: 'ali%2Fnew-skill', branch: 'ali/new-skill' },
|
|
42
|
+
{ id: 'main', branch: 'main' },
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('is empty when the workspaces root does not exist yet', async () => {
|
|
47
|
+
const svc = new WorkspaceService(path.join(root, 'missing'), 'https://example.test/kb.git', 'knowledge-base');
|
|
48
|
+
expect(await svc.listClonedWorkspaces()).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('WorkspaceService.listClonedWorkspaces — what is not a clone', () => {
|
|
53
|
+
let root: string;
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-ws-list-'));
|
|
56
|
+
});
|
|
57
|
+
afterEach(async () => {
|
|
58
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('a directory whose name round-trips but is not a branch git accepts', async () => {
|
|
62
|
+
// `foo!`, `-leading-dash` and `a..b` survive encodeURIComponent unchanged, so
|
|
63
|
+
// only the validator can exclude them; `has space` does not round-trip and
|
|
64
|
+
// is excluded one step earlier. Both guards have to hold.
|
|
65
|
+
for (const name of ['main', 'foo!', '-leading-dash', 'a..b', 'has space']) {
|
|
66
|
+
await fs.mkdir(path.join(root, name, 'knowledge-base', '.git'), { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
const svc = new WorkspaceService(root, 'https://example.test/kb.git', 'knowledge-base');
|
|
69
|
+
expect(await svc.listClonedWorkspaces()).toEqual([{ id: 'main', branch: 'main' }]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('a branch whose bootstrap is in flight right now', async () => {
|
|
73
|
+
await fs.mkdir(path.join(root, 'main', 'knowledge-base', '.git'), { recursive: true });
|
|
74
|
+
await fs.mkdir(path.join(root, 'ali%2Fx', 'knowledge-base', '.git'), { recursive: true });
|
|
75
|
+
const svc = new WorkspaceService(root, 'https://example.test/kb.git', 'knowledge-base');
|
|
76
|
+
// While a clone runs, `.git` exists but the tree is not checked out yet.
|
|
77
|
+
// The listing asks the public predicate, which is stubbed here rather than
|
|
78
|
+
// the tracker behind it — no public seam can hold a real clone open.
|
|
79
|
+
vi.spyOn(svc, 'isBootstrapInFlight').mockImplementation((branch) => branch === 'ali/x');
|
|
80
|
+
expect(await svc.listClonedWorkspaces()).toEqual([{ id: 'main', branch: 'main' }]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('a root that is a file counts as no root, not as a clone list', async () => {
|
|
84
|
+
// A FILE where the root should be: readdir fails with ENOTDIR on POSIX and
|
|
85
|
+
// ENOENT on Windows — both "no root", so an empty list. A permission
|
|
86
|
+
// failure is the case that must propagate, and there is no portable way
|
|
87
|
+
// to stage one in a temp dir; the code path is the same `throw err`.
|
|
88
|
+
const file = path.join(root, 'not-a-dir');
|
|
89
|
+
await fs.writeFile(file, 'x');
|
|
90
|
+
const svc = new WorkspaceService(file, 'https://example.test/kb.git', 'knowledge-base');
|
|
91
|
+
expect(await svc.listClonedWorkspaces()).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { isMissingRemoteBranchFailure } from '../../../shared/domain-errors.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The one line that decides whether a failed bootstrap is "this branch does
|
|
6
|
+
* not exist" (410, the browser says so) or "something is wrong" (500). Git's
|
|
7
|
+
* two wordings for a missing ref are matched; an unreachable host or a
|
|
8
|
+
* refused credential must NOT be, or a deployment outage would read as
|
|
9
|
+
* "your branch is gone".
|
|
10
|
+
*/
|
|
11
|
+
describe('isMissingRemoteBranchFailure', () => {
|
|
12
|
+
it('recognises both of git’s wordings for a ref origin does not have', () => {
|
|
13
|
+
expect(
|
|
14
|
+
isMissingRemoteBranchFailure(
|
|
15
|
+
"Command failed: git clone -b sync-test/conflict-2 https://[REDACTED]@github.com/x/y.git /w\nfatal: Remote branch sync-test/conflict-2 not found in upstream origin",
|
|
16
|
+
),
|
|
17
|
+
).toBe(true);
|
|
18
|
+
expect(isMissingRemoteBranchFailure("fatal: couldn't find remote ref refs/heads/ali/x")).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('does not mistake an unreachable host or a refused credential for a missing branch', () => {
|
|
22
|
+
expect(
|
|
23
|
+
isMissingRemoteBranchFailure("fatal: unable to access 'https://github.com/x/y.git/': Could not resolve host: github.com"),
|
|
24
|
+
).toBe(false);
|
|
25
|
+
expect(isMissingRemoteBranchFailure('remote: Invalid username or password.\nfatal: Authentication failed')).toBe(false);
|
|
26
|
+
expect(isMissingRemoteBranchFailure('fatal: repository not found')).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -38,8 +38,14 @@ describe('WorkspaceService.listFiles — read filter', () => {
|
|
|
38
38
|
await mkFile(workspaceDir, 'Open/a.md');
|
|
39
39
|
await mkFile(workspaceDir, 'Open/b.md');
|
|
40
40
|
await mkFile(workspaceDir, 'Secret/s1.md');
|
|
41
|
+
await mkFile(workspaceDir, 'Secret/Vault/v1.md');
|
|
41
42
|
await mkFile(workspaceDir, 'Mixed/hidden.md');
|
|
42
43
|
await mkFile(workspaceDir, 'top.md');
|
|
44
|
+
// A denied folder with a grant below it: Scopes/ itself is closed, but
|
|
45
|
+
// Scopes/Deploy/ is shared (a linked skill's folder opened to a plugin's
|
|
46
|
+
// readers, say), and its files are readable.
|
|
47
|
+
await mkFile(workspaceDir, 'Scopes/Deploy/SKILL.md');
|
|
48
|
+
await mkFile(workspaceDir, 'Scopes/other.md');
|
|
43
49
|
svc = new WorkspaceService(root, 'https://example.invalid/repo.git', KB);
|
|
44
50
|
await svc.getWorkspacePath(workspaceId);
|
|
45
51
|
});
|
|
@@ -64,27 +70,51 @@ describe('WorkspaceService.listFiles — read filter', () => {
|
|
|
64
70
|
expect(filtered).toEqual(unfiltered);
|
|
65
71
|
});
|
|
66
72
|
|
|
67
|
-
it('drops denied files and denied
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return new Map(ps.map((p) => [p, !(p.includes('Secret') || p.endsWith('hidden.md'))]));
|
|
72
|
-
};
|
|
73
|
-
const tree = await svc.listFiles(workspaceId, filter);
|
|
74
|
-
const all = paths(tree);
|
|
73
|
+
it('drops denied files, and a denied directory with nothing readable beneath it', async () => {
|
|
74
|
+
const filter: ReadTreeFilter = async (ps) =>
|
|
75
|
+
new Map(ps.map((p) => [p, !(p.includes('Secret') || p.includes('Scopes') || p.endsWith('hidden.md'))]));
|
|
76
|
+
const all = paths(await svc.listFiles(workspaceId, filter));
|
|
75
77
|
|
|
76
78
|
// Readable files survive.
|
|
77
79
|
expect(all).toContain('Open/a.md');
|
|
78
80
|
expect(all).toContain('Open/b.md');
|
|
79
81
|
expect(all).toContain('top.md');
|
|
80
|
-
//
|
|
82
|
+
// A denied directory whose whole subtree is denied is gone, with the
|
|
83
|
+
// subtree — including a nested folder the walk did look into.
|
|
81
84
|
expect(all).not.toContain('Secret');
|
|
82
85
|
expect(all).not.toContain('Secret/s1.md');
|
|
86
|
+
expect(all).not.toContain('Secret/Vault');
|
|
87
|
+
expect(all).not.toContain('Scopes');
|
|
83
88
|
// Denied file is gone.
|
|
84
89
|
expect(all).not.toContain('Mixed/hidden.md');
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('keeps a denied directory as the way to what is readable beneath it', async () => {
|
|
93
|
+
// Scopes/ is closed; Scopes/Deploy/ and its file are shared. The closed
|
|
94
|
+
// folder shows as a container — the reader can reach the skill through
|
|
95
|
+
// the tree — with its own contents still filtered (other.md is gone).
|
|
96
|
+
const filter: ReadTreeFilter = async (ps) =>
|
|
97
|
+
new Map(ps.map((p) => [p, p.startsWith('Scopes/Deploy') || (!p.includes('Secret') && !p.includes('Scopes') && !p.endsWith('hidden.md'))]));
|
|
98
|
+
const all = paths(await svc.listFiles(workspaceId, filter));
|
|
99
|
+
expect(all).toContain('Scopes');
|
|
100
|
+
expect(all).toContain('Scopes/Deploy');
|
|
101
|
+
expect(all).toContain('Scopes/Deploy/SKILL.md');
|
|
102
|
+
expect(all).not.toContain('Scopes/other.md');
|
|
103
|
+
// Unchanged for a subtree with nothing readable in it.
|
|
104
|
+
expect(all).not.toContain('Secret');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('keeps a closed folder whose only readable content is an EMPTY sub-folder', async () => {
|
|
108
|
+
// A readable folder is kept on its own verdict, children or not — so the
|
|
109
|
+
// closed folder above it has a child, and stays as the way there.
|
|
110
|
+
await fs.mkdir(path.join(workspaceDir, 'Scopes', 'Empty'), { recursive: true });
|
|
111
|
+
const filter: ReadTreeFilter = async (ps) =>
|
|
112
|
+
new Map(ps.map((p) => [p, p === 'Scopes/Empty' || (!p.includes('Secret') && !p.includes('Scopes') && !p.endsWith('hidden.md'))]));
|
|
113
|
+
const all = paths(await svc.listFiles(workspaceId, filter));
|
|
114
|
+
expect(all).toContain('Scopes');
|
|
115
|
+
expect(all).toContain('Scopes/Empty');
|
|
116
|
+
expect(all).not.toContain('Scopes/Deploy');
|
|
117
|
+
expect(all).not.toContain('Scopes/other.md');
|
|
88
118
|
});
|
|
89
119
|
|
|
90
120
|
it('keeps a readable directory left empty after filtering (D4)', async () => {
|