@bevel-software/platform-core-backend 0.26.0 → 0.27.0
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/agent-guide/access-control.md +17 -0
- package/agent-guide/conventions.md +8 -0
- package/agent-guide/html-views.md +83 -0
- package/agent-guide/new-knowledge-base.md +24 -0
- package/agent-guide/tool-manuals.md +10 -0
- package/dist/assets.d.ts +19 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +26 -2
- package/dist/assets.js.map +1 -1
- package/dist/core/catalog-revision.d.ts +19 -1
- package/dist/core/catalog-revision.d.ts.map +1 -1
- package/dist/core/catalog-revision.js +14 -3
- package/dist/core/catalog-revision.js.map +1 -1
- package/dist/core/create-core-server.d.ts +5 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +54 -3
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +26 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +100 -8
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +16 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +18 -1
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +21 -0
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +22 -1
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +8 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -0
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +11 -0
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/directory-sync-bot.js +1 -1
- package/dist/modules/access/directory-sync-bot.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.d.ts +6 -0
- package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
- package/dist/modules/agent-guide/agent-guide.js +8 -0
- package/dist/modules/agent-guide/agent-guide.js.map +1 -1
- package/dist/modules/agent-guide/index.d.ts +1 -1
- package/dist/modules/agent-guide/index.d.ts.map +1 -1
- package/dist/modules/agent-guide/index.js +1 -1
- package/dist/modules/agent-guide/index.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +5 -0
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +19 -2
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +19 -19
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
- package/dist/modules/audit/agent-audit.service.d.ts +21 -2
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +43 -2
- package/dist/modules/audit/agent-audit.service.js.map +1 -1
- package/dist/modules/audit/agent-connection.routes.d.ts +34 -0
- package/dist/modules/audit/agent-connection.routes.d.ts.map +1 -0
- package/dist/modules/audit/agent-connection.routes.js +38 -0
- package/dist/modules/audit/agent-connection.routes.js.map +1 -0
- package/dist/modules/audit/audit.contract.d.ts +24 -0
- package/dist/modules/audit/audit.contract.d.ts.map +1 -1
- package/dist/modules/audit/audit.contract.js.map +1 -1
- package/dist/modules/audit/index.d.ts +1 -0
- package/dist/modules/audit/index.d.ts.map +1 -1
- package/dist/modules/audit/index.js +1 -0
- package/dist/modules/audit/index.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +10 -3
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +7 -0
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +1 -1
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +11 -3
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +96 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +32 -3
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/embed/account-link.service.d.ts +42 -0
- package/dist/modules/embed/account-link.service.d.ts.map +1 -0
- package/dist/modules/embed/account-link.service.js +50 -0
- package/dist/modules/embed/account-link.service.js.map +1 -0
- package/dist/modules/embed/embed-link.d.ts +58 -0
- package/dist/modules/embed/embed-link.d.ts.map +1 -0
- package/dist/modules/embed/embed-link.js +160 -0
- package/dist/modules/embed/embed-link.js.map +1 -0
- package/dist/modules/embed/embed.errors.d.ts +30 -0
- package/dist/modules/embed/embed.errors.d.ts.map +1 -0
- package/dist/modules/embed/embed.errors.js +42 -0
- package/dist/modules/embed/embed.errors.js.map +1 -0
- package/dist/modules/embed/embed.interface.d.ts +204 -0
- package/dist/modules/embed/embed.interface.d.ts.map +1 -0
- package/dist/modules/embed/embed.interface.js +2 -0
- package/dist/modules/embed/embed.interface.js.map +1 -0
- package/dist/modules/embed/embed.routes.d.ts +59 -0
- package/dist/modules/embed/embed.routes.d.ts.map +1 -0
- package/dist/modules/embed/embed.routes.js +343 -0
- package/dist/modules/embed/embed.routes.js.map +1 -0
- package/dist/modules/embed/embed.service.d.ts +169 -0
- package/dist/modules/embed/embed.service.d.ts.map +1 -0
- package/dist/modules/embed/embed.service.js +627 -0
- package/dist/modules/embed/embed.service.js.map +1 -0
- package/dist/modules/embed/embed.tools.d.ts +51 -0
- package/dist/modules/embed/embed.tools.d.ts.map +1 -0
- package/dist/modules/embed/embed.tools.js +170 -0
- package/dist/modules/embed/embed.tools.js.map +1 -0
- package/dist/modules/embed/mcp-app.d.ts +64 -0
- package/dist/modules/embed/mcp-app.d.ts.map +1 -0
- package/dist/modules/embed/mcp-app.js +147 -0
- package/dist/modules/embed/mcp-app.js.map +1 -0
- package/dist/modules/embed/mcp-app.routes.d.ts +21 -0
- package/dist/modules/embed/mcp-app.routes.d.ts.map +1 -0
- package/dist/modules/embed/mcp-app.routes.js +36 -0
- package/dist/modules/embed/mcp-app.routes.js.map +1 -0
- package/dist/modules/github-app/github-app.connection.d.ts +15 -0
- package/dist/modules/github-app/github-app.connection.d.ts.map +1 -1
- package/dist/modules/github-app/github-app.connection.js +34 -3
- package/dist/modules/github-app/github-app.connection.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts +65 -0
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +300 -0
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.interface.d.ts +5 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.service.js +6 -0
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +2 -1
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +28 -1
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +64 -8
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +32 -4
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/onboarding/onboarding.contract.d.ts +80 -0
- package/dist/modules/onboarding/onboarding.contract.d.ts.map +1 -0
- package/dist/modules/onboarding/onboarding.contract.js +7 -0
- package/dist/modules/onboarding/onboarding.contract.js.map +1 -0
- package/dist/modules/onboarding/onboarding.routes.d.ts +18 -0
- package/dist/modules/onboarding/onboarding.routes.d.ts.map +1 -0
- package/dist/modules/onboarding/onboarding.routes.js +81 -0
- package/dist/modules/onboarding/onboarding.routes.js.map +1 -0
- package/dist/modules/onboarding/starter-pack.service.d.ts +213 -0
- package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -0
- package/dist/modules/onboarding/starter-pack.service.js +492 -0
- package/dist/modules/onboarding/starter-pack.service.js.map +1 -0
- package/dist/modules/onboarding/starter-packs.d.ts +78 -0
- package/dist/modules/onboarding/starter-packs.d.ts.map +1 -0
- package/dist/modules/onboarding/starter-packs.js +156 -0
- package/dist/modules/onboarding/starter-packs.js.map +1 -0
- package/dist/modules/plugins/index.d.ts +1 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +1 -1
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.d.ts +19 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +18 -1
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +0 -36
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +31 -0
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +34 -0
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +134 -1
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/repository-source.d.ts +1 -0
- package/dist/modules/settings/repository-source.d.ts.map +1 -1
- package/dist/modules/settings/repository-source.js +3 -3
- package/dist/modules/settings/repository-source.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +50 -32
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/connection-key-rejection.d.ts +10 -1
- package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -1
- package/dist/modules/tool-auth/connection-key-rejection.js +11 -3
- package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +8 -0
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +24 -2
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +59 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +9 -4
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/index.d.ts +28 -1
- package/dist/modules/tool-helpers/index.d.ts.map +1 -1
- package/dist/modules/tool-helpers/index.js +28 -1
- package/dist/modules/tool-helpers/index.js.map +1 -1
- package/dist/modules/tool-helpers/route-argument-check.d.ts +10 -0
- package/dist/modules/tool-helpers/route-argument-check.d.ts.map +1 -0
- package/dist/modules/tool-helpers/route-argument-check.js +132 -0
- package/dist/modules/tool-helpers/route-argument-check.js.map +1 -0
- package/dist/modules/tool-helpers/route-tool-schemas.d.ts +89 -0
- package/dist/modules/tool-helpers/route-tool-schemas.d.ts.map +1 -0
- package/dist/modules/tool-helpers/route-tool-schemas.js +113 -0
- package/dist/modules/tool-helpers/route-tool-schemas.js.map +1 -0
- package/dist/modules/tool-helpers/tool-def.d.ts +71 -0
- package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-def.js +70 -6
- package/dist/modules/tool-helpers/tool-def.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +46 -6
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +157 -8
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +9 -0
- package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +14 -5
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +6 -2
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
- package/dist/modules/tool-registry/description-length.js +27 -5
- package/dist/modules/tool-registry/description-length.js.map +1 -1
- package/dist/modules/tool-registry/guide-first.d.ts +4 -0
- package/dist/modules/tool-registry/guide-first.d.ts.map +1 -1
- package/dist/modules/tool-registry/guide-first.js +9 -1
- package/dist/modules/tool-registry/guide-first.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +53 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js +97 -68
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +8 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +124 -37
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +29 -5
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +62 -5
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +72 -1
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +172 -36
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.d.ts +15 -0
- package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.js +78 -6
- package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +15 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +25 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts +6 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.js +23 -6
- package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
- package/dist/modules/workflow/recovery-bot.js +1 -1
- package/dist/modules/workflow/recovery-bot.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +2 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/sanitize-error.d.ts +16 -0
- package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
- package/dist/modules/workflow/sanitize-error.js +45 -0
- package/dist/modules/workflow/sanitize-error.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +183 -15
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +805 -106
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-download.builder.d.ts +105 -0
- package/dist/modules/workspace/agent-download.builder.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.builder.js +238 -0
- package/dist/modules/workspace/agent-download.builder.js.map +1 -0
- package/dist/modules/workspace/agent-download.routes.d.ts +77 -0
- package/dist/modules/workspace/agent-download.routes.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.routes.js +156 -0
- package/dist/modules/workspace/agent-download.routes.js.map +1 -0
- package/dist/modules/workspace/agent-download.store.d.ts +225 -0
- package/dist/modules/workspace/agent-download.store.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.store.js +423 -0
- package/dist/modules/workspace/agent-download.store.js.map +1 -0
- package/dist/modules/workspace/agent-upload.store.d.ts +10 -1
- package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -1
- package/dist/modules/workspace/agent-upload.store.js +16 -6
- package/dist/modules/workspace/agent-upload.store.js.map +1 -1
- package/dist/modules/workspace/file-readers/raw-mime.d.ts +6 -0
- package/dist/modules/workspace/file-readers/raw-mime.d.ts.map +1 -0
- package/dist/modules/workspace/file-readers/raw-mime.js +38 -0
- package/dist/modules/workspace/file-readers/raw-mime.js.map +1 -0
- package/dist/modules/workspace/file-readers/text-reader.d.ts +5 -5
- package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/text-reader.js +5 -8
- package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
- package/dist/modules/workspace/first-run.d.ts +111 -0
- package/dist/modules/workspace/first-run.d.ts.map +1 -0
- package/dist/modules/workspace/first-run.js +181 -0
- package/dist/modules/workspace/first-run.js.map +1 -0
- package/dist/modules/workspace/move-links.d.ts +92 -0
- package/dist/modules/workspace/move-links.d.ts.map +1 -0
- package/dist/modules/workspace/move-links.js +173 -0
- package/dist/modules/workspace/move-links.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 +3 -13
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +63 -22
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +75 -2
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +197 -24
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +59 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +443 -65
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/write-access/write-access.d.ts.map +1 -1
- package/dist/modules/write-access/write-access.js +9 -0
- package/dist/modules/write-access/write-access.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +73 -1
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +86 -12
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +10 -1
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/shared/utf8-text.d.ts +15 -0
- package/dist/shared/utf8-text.d.ts.map +1 -0
- package/dist/shared/utf8-text.js +20 -0
- package/dist/shared/utf8-text.js.map +1 -0
- package/dist/tenancy/static-tenant-source.d.ts +2 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +9 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/mcp-app/page.html +349 -0
- package/migrations/0017_atlassian_account_links.sql +37 -0
- package/migrations/meta/0017_snapshot.json +2396 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +5 -3
- package/src/__tests__/packaged-assets-ship.test.ts +16 -4
- package/src/assets.ts +28 -2
- package/src/core/__tests__/catalog-revision.test.ts +40 -0
- package/src/core/__tests__/gated-boot.test.ts +1 -0
- package/src/core/__tests__/lifecycle.test.ts +14 -0
- package/src/core/catalog-revision.ts +15 -2
- package/src/core/create-core-server.ts +67 -3
- package/src/core/create-core-services.ts +140 -6
- package/src/core/lifecycle.ts +28 -2
- package/src/core-config.ts +24 -1
- package/src/index.ts +2 -2
- package/src/modules/access/__tests__/access-control.service.test.ts +19 -0
- package/src/modules/access/access-control.interface.ts +13 -0
- package/src/modules/access/access-control.service.ts +16 -0
- package/src/modules/access/directory-sync-bot.ts +1 -1
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +58 -0
- package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +19 -1
- package/src/modules/agent-guide/agent-guide.ts +9 -0
- package/src/modules/agent-guide/index.ts +1 -0
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +14 -0
- package/src/modules/agent-instructions/compose.ts +17 -2
- package/src/modules/agent-instructions/shared-file-rules.ts +19 -19
- package/src/modules/audit/__tests__/agent-audit.service.test.ts +64 -4
- package/src/modules/audit/__tests__/agent-connection.routes.test.ts +102 -0
- package/src/modules/audit/agent-audit.service.ts +58 -3
- package/src/modules/audit/agent-connection.routes.ts +56 -0
- package/src/modules/audit/audit.contract.ts +26 -0
- package/src/modules/audit/index.ts +1 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +5 -3
- package/src/modules/auth/account.routes.ts +10 -3
- package/src/modules/auth/auth.middleware.ts +1 -1
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +18 -2
- package/src/modules/code-mode/code-mode.tool.ts +13 -2
- package/src/modules/database/core-schema.ts +33 -3
- package/src/modules/embed/__tests__/account-links.migration.test.ts +98 -0
- package/src/modules/embed/__tests__/embed-link.test.ts +122 -0
- package/src/modules/embed/__tests__/embed.routes.test.ts +362 -0
- package/src/modules/embed/__tests__/embed.service.lock.test.ts +193 -0
- package/src/modules/embed/__tests__/embed.service.test.ts +805 -0
- package/src/modules/embed/__tests__/embed.tools.test.ts +304 -0
- package/src/modules/embed/__tests__/mcp-app.test.ts +207 -0
- package/src/modules/embed/account-link.service.ts +76 -0
- package/src/modules/embed/embed-link.ts +180 -0
- package/src/modules/embed/embed.errors.ts +43 -0
- package/src/modules/embed/embed.interface.ts +203 -0
- package/src/modules/embed/embed.routes.ts +363 -0
- package/src/modules/embed/embed.service.ts +705 -0
- package/src/modules/embed/embed.tools.ts +206 -0
- package/src/modules/embed/mcp-app.routes.ts +41 -0
- package/src/modules/embed/mcp-app.ts +173 -0
- package/src/modules/github-app/__tests__/github-app.test.ts +76 -0
- package/src/modules/github-app/github-app.connection.ts +38 -4
- package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +304 -0
- package/src/modules/kb-fs/locking-filesystem.ts +287 -0
- package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +35 -0
- package/src/modules/kb-sync/kb-sync.interface.ts +2 -0
- package/src/modules/kb-sync/kb-sync.service.ts +6 -0
- package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +68 -11
- package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +37 -2
- package/src/modules/mcp/__tests__/mcp.app.e2e.test.ts +200 -0
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
- package/src/modules/mcp/__tests__/mcp.service.test.ts +23 -13
- package/src/modules/mcp/__tests__/tool-call-interface.e2e.test.ts +499 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +2 -1
- package/src/modules/mcp/mcp.service.ts +81 -12
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +42 -4
- package/src/modules/onboarding/__tests__/onboarding.routes.test.ts +163 -0
- package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +635 -0
- package/src/modules/onboarding/__tests__/starter-packs.test.ts +169 -0
- package/src/modules/onboarding/onboarding.contract.ts +85 -0
- package/src/modules/onboarding/onboarding.routes.ts +85 -0
- package/src/modules/onboarding/starter-pack.service.ts +580 -0
- package/src/modules/onboarding/starter-packs.ts +182 -0
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +25 -2
- package/src/modules/plugins/index.ts +1 -0
- package/src/modules/plugins/plugin-provision.service.ts +22 -1
- package/src/modules/plugins/plugins.routes.ts +29 -0
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +150 -1
- package/src/modules/settings/__tests__/repository-source.test.ts +9 -0
- package/src/modules/settings/deployment-settings.service.ts +131 -1
- package/src/modules/settings/repository-source.ts +4 -4
- package/src/modules/skills/__tests__/skills.tools.description.test.ts +5 -1
- package/src/modules/skills/skills.tools.ts +55 -35
- package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +32 -0
- package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +65 -0
- package/src/modules/tool-auth/connection-key-rejection.ts +13 -3
- package/src/modules/tool-auth/external-api-key.interface.ts +9 -0
- package/src/modules/tool-auth/external-api-key.service.ts +61 -4
- package/src/modules/tool-auth/tool-auth.middleware.ts +13 -4
- package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +502 -0
- package/src/modules/tool-helpers/__tests__/route-argument-check.test.ts +337 -0
- package/src/modules/tool-helpers/index.ts +34 -1
- package/src/modules/tool-helpers/route-argument-check.ts +149 -0
- package/src/modules/tool-helpers/route-tool-schemas.ts +171 -0
- package/src/modules/tool-helpers/tool-def.ts +132 -7
- package/src/modules/tool-helpers/tool-handler.ts +196 -8
- package/src/modules/tool-helpers/tool.contract.ts +9 -0
- package/src/modules/tool-manuals/tool-manuals.tools.ts +15 -5
- package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +14 -1
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +23 -10
- package/src/modules/tool-registry/description-length.ts +34 -7
- package/src/modules/tool-registry/guide-first.ts +8 -1
- package/src/modules/workflow/__tests__/file-lock.service.deletion-gate.test.ts +104 -0
- package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +66 -0
- package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +31 -2
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +110 -6
- package/src/modules/workflow/__tests__/sanitize-error.test.ts +89 -1
- package/src/modules/workflow/__tests__/workflow.routes.push-refused.test.ts +202 -0
- package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +6 -6
- package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +1026 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +468 -10
- package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +4 -0
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +5 -4
- package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +54 -2
- package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +41 -5
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +127 -2
- package/src/modules/workflow/agent-tools/change-request-read.tools.ts +157 -122
- package/src/modules/workflow/agent-tools/workflow.tools.ts +129 -35
- package/src/modules/workflow/file-lock.service.ts +67 -5
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +164 -0
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +48 -1
- package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +231 -2
- package/src/modules/workflow/git/git.service.ts +176 -38
- package/src/modules/workflow/git/node-git-runner.ts +82 -7
- package/src/modules/workflow/pending-commits.service.ts +29 -1
- package/src/modules/workflow/pending-commits.worker.ts +24 -6
- package/src/modules/workflow/recovery-bot.ts +1 -1
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +2 -1
- package/src/modules/workflow/sanitize-error.ts +50 -0
- package/src/modules/workflow/workflow.service.ts +879 -112
- package/src/modules/workspace/__tests__/agent-downloads.test.ts +823 -0
- package/src/modules/workspace/__tests__/first-run.test.ts +234 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +5 -2
- package/src/modules/workspace/__tests__/md-links.test.ts +353 -0
- package/src/modules/workspace/__tests__/move-links.test.ts +261 -0
- package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +219 -0
- package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -0
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +102 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +52 -5
- package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +190 -0
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +8 -4
- package/src/modules/workspace/__tests__/workspace.service.test.ts +50 -8
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +44 -1
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +409 -12
- package/src/modules/workspace/agent-download.builder.ts +308 -0
- package/src/modules/workspace/agent-download.routes.ts +183 -0
- package/src/modules/workspace/agent-download.store.ts +533 -0
- package/src/modules/workspace/agent-upload.store.ts +23 -7
- package/src/modules/workspace/file-readers/raw-mime.ts +40 -0
- package/src/modules/workspace/file-readers/text-reader.ts +5 -8
- package/src/modules/workspace/first-run.ts +222 -0
- package/src/modules/workspace/move-links.ts +237 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +3 -12
- package/src/modules/workspace/workspace.routes.ts +64 -22
- package/src/modules/workspace/workspace.service.ts +202 -25
- package/src/modules/workspace/workspace.tools.ts +492 -63
- package/src/modules/write-access/__tests__/write-access.test.ts +13 -0
- package/src/modules/write-access/write-access.ts +9 -0
- package/src/shared/domain-errors.ts +108 -11
- package/src/shared/git.contract.ts +8 -1
- package/src/shared/utf8-text.ts +21 -0
- package/src/tenancy/__tests__/static-tenant-source.test.ts +1 -0
- package/src/tenancy/static-tenant-source.ts +10 -2
- package/starter-packs/README.md +103 -0
- package/starter-packs/engineering/KnowledgeBase/About us.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/Architecture.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/How we ship.md +9 -0
- package/starter-packs/engineering/Plugins/engineering-starter/LICENSE +21 -0
- package/starter-packs/engineering/Plugins/engineering-starter/SOURCE.md +20 -0
- package/starter-packs/engineering/Plugins/engineering-starter/access.md +19 -0
- package/starter-packs/engineering/Plugins/engineering-starter/plugin.json +10 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/SKILL.md +171 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2a-startup-diagnostic.md +157 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2b-builder-brainstorm.md +39 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-ceo-review/SKILL.md +144 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-eng-review/SKILL.md +144 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/review/SKILL.md +80 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/review/checklist.md +184 -0
- package/starter-packs/engineering/pack.yaml +11 -0
- package/starter-packs/general/KnowledgeBase/About us.md +9 -0
- package/starter-packs/general/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/general/KnowledgeBase/How we work.md +9 -0
- package/starter-packs/general/Plugins/general-starter/LICENSE +202 -0
- package/starter-packs/general/Plugins/general-starter/SOURCE.md +10 -0
- package/starter-packs/general/Plugins/general-starter/access.md +19 -0
- package/starter-packs/general/Plugins/general-starter/plugin.json +10 -0
- package/starter-packs/general/Plugins/general-starter/skills/memory-management/SKILL.md +332 -0
- package/starter-packs/general/Plugins/general-starter/skills/productivity-start/SKILL.md +163 -0
- package/starter-packs/general/Plugins/general-starter/skills/productivity-update/SKILL.md +167 -0
- package/starter-packs/general/Plugins/general-starter/skills/task-management/SKILL.md +83 -0
- package/starter-packs/general/pack.yaml +10 -0
- package/starter-packs/operations/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/operations/KnowledgeBase/How we work.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Processes.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Tools we use.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Vendors.md +9 -0
- package/starter-packs/operations/Plugins/operations-starter/LICENSE +202 -0
- package/starter-packs/operations/Plugins/operations-starter/SOURCE.md +10 -0
- package/starter-packs/operations/Plugins/operations-starter/access.md +19 -0
- package/starter-packs/operations/Plugins/operations-starter/plugin.json +10 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/capacity-plan/SKILL.md +111 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/change-request/SKILL.md +119 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/compliance-tracking/SKILL.md +44 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/process-doc/SKILL.md +80 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/process-optimization/SKILL.md +39 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/risk-assessment/SKILL.md +40 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/runbook/SKILL.md +85 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/status-report/SKILL.md +75 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/vendor-review/SKILL.md +102 -0
- package/starter-packs/operations/pack.yaml +12 -0
- package/starter-packs/sales/KnowledgeBase/About us.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Customers.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/sales/KnowledgeBase/How we sell.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Objections.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Pricing.md +9 -0
- package/starter-packs/sales/Plugins/sales-starter/LICENSE +202 -0
- package/starter-packs/sales/Plugins/sales-starter/SOURCE.md +10 -0
- package/starter-packs/sales/Plugins/sales-starter/access.md +19 -0
- package/starter-packs/sales/Plugins/sales-starter/plugin.json +10 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-context/SKILL.md +94 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-plan/SKILL.md +95 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-research/SKILL.md +99 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-tiering/SKILL.md +101 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/call-prep/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/call-summary/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/close-plan/SKILL.md +106 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/competitive-intelligence/SKILL.md +102 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/create-an-asset/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/crm-hygiene-check/SKILL.md +103 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/customer-health/SKILL.md +108 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/customer-voice/SKILL.md +94 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/daily-briefing/SKILL.md +61 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-advance-gap/SKILL.md +102 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-review/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-signals/SKILL.md +96 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-slip-scenario/SKILL.md +104 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/draft-outreach/SKILL.md +116 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/end-of-day/SKILL.md +114 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/expansion-whitespace/SKILL.md +97 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/forecast/SKILL.md +100 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/handle-objection/SKILL.md +93 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/inbox-sweep/SKILL.md +114 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/lead-triage/SKILL.md +106 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/log-activity/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/pipeline-review/SKILL.md +97 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/renewal-radar/SKILL.md +95 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/rep-context/SKILL.md +90 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/route-lead/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/sales-setup/SKILL.md +200 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/schedule-meeting/SKILL.md +99 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/stakeholder-map/SKILL.md +107 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/team-pipeline/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/update-opportunity/SKILL.md +112 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/weekly-wrap/SKILL.md +90 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/win-loss-review/SKILL.md +88 -0
- package/starter-packs/sales/pack.yaml +13 -0
- package/starter-packs/support/KnowledgeBase/Escalations.md +9 -0
- package/starter-packs/support/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/support/KnowledgeBase/Known issues.md +9 -0
- package/starter-packs/support/KnowledgeBase/Products.md +9 -0
- package/starter-packs/support/KnowledgeBase/Tone of voice.md +9 -0
- package/starter-packs/support/Plugins/support-starter/LICENSE +202 -0
- package/starter-packs/support/Plugins/support-starter/SOURCE.md +10 -0
- package/starter-packs/support/Plugins/support-starter/access.md +19 -0
- package/starter-packs/support/Plugins/support-starter/plugin.json +10 -0
- package/starter-packs/support/Plugins/support-starter/skills/customer-escalation/SKILL.md +246 -0
- package/starter-packs/support/Plugins/support-starter/skills/customer-research/SKILL.md +250 -0
- package/starter-packs/support/Plugins/support-starter/skills/draft-response/SKILL.md +416 -0
- package/starter-packs/support/Plugins/support-starter/skills/kb-article/SKILL.md +350 -0
- package/starter-packs/support/Plugins/support-starter/skills/ticket-triage/SKILL.md +273 -0
- package/starter-packs/support/pack.yaml +12 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eq, inArray } from 'drizzle-orm';
|
|
1
|
+
import { and, eq, inArray } from 'drizzle-orm';
|
|
2
2
|
import { logger } from '../../shared/logging.js';
|
|
3
3
|
|
|
4
4
|
const log = logger('settings');
|
|
@@ -109,6 +109,16 @@ export const validateHttpsRemote = (value: string): string | null => {
|
|
|
109
109
|
/**
|
|
110
110
|
* The core catalogue. Order is the order the setup screen renders them in.
|
|
111
111
|
*/
|
|
112
|
+
/**
|
|
113
|
+
* What the `retireMergedBranches` value in effect means: on, unless it says
|
|
114
|
+
* off. The page saves only `true` or `false`; the environment variable is
|
|
115
|
+
* typed by a person, so `0`, `off` and `no` (any case) mean off too, and
|
|
116
|
+
* anything else — unset included — leaves the cleanup on.
|
|
117
|
+
*/
|
|
118
|
+
export function retireMergedBranchesOn(raw: string): boolean {
|
|
119
|
+
return !/^(false|0|off|no)$/i.test(raw.trim());
|
|
120
|
+
}
|
|
121
|
+
|
|
112
122
|
export const CORE_SETTINGS: SettingDef[] = [
|
|
113
123
|
{
|
|
114
124
|
/**
|
|
@@ -289,6 +299,23 @@ export const CORE_SETTINGS: SettingDef[] = [
|
|
|
289
299
|
restartToApply: true,
|
|
290
300
|
},
|
|
291
301
|
|
|
302
|
+
{
|
|
303
|
+
/**
|
|
304
|
+
* Whether the server removes, on its own, the branches merged change
|
|
305
|
+
* requests left behind (`WorkflowService.retireLeftoverMergedBranches`).
|
|
306
|
+
* On unless set to `false`; a blanked field puts the default back. Read
|
|
307
|
+
* at every round, so it applies without a restart. It covers that cleanup
|
|
308
|
+
* only: the removal of a branch when its change request is merged does
|
|
309
|
+
* not ask it.
|
|
310
|
+
*/
|
|
311
|
+
key: 'retireMergedBranches',
|
|
312
|
+
envVar: 'RETIRE_MERGED_BRANCHES',
|
|
313
|
+
section: 'knowledge-base',
|
|
314
|
+
blankMeansDefault: true,
|
|
315
|
+
unsetMeans: 'true',
|
|
316
|
+
validate: (v) => (v === 'true' || v === 'false' ? null : 'Either true or false.'),
|
|
317
|
+
},
|
|
318
|
+
|
|
292
319
|
{
|
|
293
320
|
/**
|
|
294
321
|
* The credential a git host's webhook or a pipeline presents to
|
|
@@ -372,6 +399,35 @@ export const CORE_SETTINGS: SettingDef[] = [
|
|
|
372
399
|
validate: (v) =>
|
|
373
400
|
parseRetentionWindow(v) === null ? 'Enter a whole number of days, or 0 to keep events forever.' : null,
|
|
374
401
|
},
|
|
402
|
+
|
|
403
|
+
{
|
|
404
|
+
/**
|
|
405
|
+
* Which starter pack the first admin chose for a new knowledge base — a
|
|
406
|
+
* pack's id, or `none` for "I'll start from scratch" — written once, by
|
|
407
|
+
* the deployment, when the choice landed (see
|
|
408
|
+
* `modules/onboarding/starter-pack.service.ts`). Its presence is what
|
|
409
|
+
* retires the "What does your team do?" card for good; its value is what
|
|
410
|
+
* the first-page prompt and the agent's first-run note follow. Internal:
|
|
411
|
+
* a fact about what happened, not something the setup screen offers.
|
|
412
|
+
*/
|
|
413
|
+
key: 'starterPack',
|
|
414
|
+
section: 'knowledge-base',
|
|
415
|
+
internal: true,
|
|
416
|
+
validate: (v) => (/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(v) ? null : 'A starter pack id is lowercase letters, digits and hyphens.'),
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
/**
|
|
420
|
+
* The claim an admin holds while their starter-pack choice is being
|
|
421
|
+
* written: `<user id> <epoch ms>`, inserted only if absent so two replicas
|
|
422
|
+
* cannot both write, and released once the choice is recorded. A claim
|
|
423
|
+
* nobody released in time is a process that died mid-write and is taken
|
|
424
|
+
* over (see `starter-pack.service.ts`). Internal, like the choice.
|
|
425
|
+
*/
|
|
426
|
+
key: 'starterPackClaim',
|
|
427
|
+
section: 'knowledge-base',
|
|
428
|
+
internal: true,
|
|
429
|
+
validate: (v) => (/^\S+ \d+$/.test(v) ? null : 'A claim is a user id and a time.'),
|
|
430
|
+
},
|
|
375
431
|
];
|
|
376
432
|
|
|
377
433
|
/**
|
|
@@ -724,6 +780,61 @@ export class DeploymentSettingsService {
|
|
|
724
780
|
await this.save(entries, updatedBy, 'deployment');
|
|
725
781
|
}
|
|
726
782
|
|
|
783
|
+
/**
|
|
784
|
+
* {@link record} one value ONLY IF NOTHING IS THERE, and say whether it was
|
|
785
|
+
* this call that put it there. The row is the claim for a flow that must
|
|
786
|
+
* happen once across every replica (the starter-pack choice): the insert
|
|
787
|
+
* yields to a row already there, so of two replicas claiming at once the
|
|
788
|
+
* database lets exactly one through. Validated as `record` is; a blank
|
|
789
|
+
* value is no claim (it would be a clear) and answers false.
|
|
790
|
+
*/
|
|
791
|
+
async recordIfAbsent(key: string, value: string, updatedBy: string | null): Promise<boolean> {
|
|
792
|
+
const write = this.plan({ [key]: value }, 'deployment').toWrite.find((w) => w.key === key);
|
|
793
|
+
if (!write) return false;
|
|
794
|
+
const encrypted = write.def.secret === true;
|
|
795
|
+
const stored = encrypted ? this.crypto!.encrypt(write.value) : write.value;
|
|
796
|
+
const inserted = await this.db
|
|
797
|
+
.insert(deploymentSettings)
|
|
798
|
+
.values({ key, value: stored, encrypted, updatedBy })
|
|
799
|
+
.onConflictDoNothing({ target: deploymentSettings.key })
|
|
800
|
+
.returning({ key: deploymentSettings.key });
|
|
801
|
+
if (inserted.length === 0) return false;
|
|
802
|
+
this.stored.set(key, write.value);
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Replace a plain setting's value ONLY IF it still reads `expected` —
|
|
808
|
+
* with `next`, or with nothing when `next` is null — and say whether it
|
|
809
|
+
* was this call that did it. One statement, so of two replicas acting on
|
|
810
|
+
* what they both read, the database lets exactly one through: the other
|
|
811
|
+
* finds the value gone or changed and answers false. For the claim flows
|
|
812
|
+
* that {@link recordIfAbsent} opens: taking over a claim that expired, and
|
|
813
|
+
* releasing one's own without touching a successor's. Plain settings only;
|
|
814
|
+
* `next` is validated as {@link record} would.
|
|
815
|
+
*/
|
|
816
|
+
async swapIfValue(key: string, expected: string, next: string | null, updatedBy: string | null): Promise<boolean> {
|
|
817
|
+
const def = this.defs.get(key);
|
|
818
|
+
if (!def || def.secret) return false;
|
|
819
|
+
const same = and(eq(deploymentSettings.key, key), eq(deploymentSettings.value, expected));
|
|
820
|
+
if (next === null) {
|
|
821
|
+
const gone = await this.db.delete(deploymentSettings).where(same).returning({ key: deploymentSettings.key });
|
|
822
|
+
if (gone.length === 0) return false;
|
|
823
|
+
this.stored.delete(key);
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
const write = this.plan({ [key]: next }, 'deployment').toWrite.find((w) => w.key === key);
|
|
827
|
+
if (!write) return false;
|
|
828
|
+
const swapped = await this.db
|
|
829
|
+
.update(deploymentSettings)
|
|
830
|
+
.set({ value: write.value, updatedBy, updatedAt: new Date() })
|
|
831
|
+
.where(same)
|
|
832
|
+
.returning({ key: deploymentSettings.key });
|
|
833
|
+
if (swapped.length === 0) return false;
|
|
834
|
+
this.stored.set(key, write.value);
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
|
|
727
838
|
/** Validate a batch and return the writes (and the clears) it amounts to; throws on any problem. */
|
|
728
839
|
private plan(
|
|
729
840
|
entries: Record<string, string>,
|
|
@@ -952,6 +1063,25 @@ export class DeploymentSettingsService {
|
|
|
952
1063
|
return `${OIDC_VERIFICATION_PREFIX}${fingerprint}`;
|
|
953
1064
|
}
|
|
954
1065
|
|
|
1066
|
+
/**
|
|
1067
|
+
* One plain (never sealed) setting as the DATABASE has it now, taken into
|
|
1068
|
+
* this replica's cache on the way: for the rare setting a running
|
|
1069
|
+
* deployment writes, so a replica that did not serve the write still
|
|
1070
|
+
* answers with it. The environment still wins, as in {@link resolve}.
|
|
1071
|
+
*/
|
|
1072
|
+
async reload(key: string): Promise<string> {
|
|
1073
|
+
const def = this.defs.get(key);
|
|
1074
|
+
if (!def || def.secret) return this.resolve(key);
|
|
1075
|
+
const rows = await this.db
|
|
1076
|
+
.select({ value: deploymentSettings.value, encrypted: deploymentSettings.encrypted })
|
|
1077
|
+
.from(deploymentSettings)
|
|
1078
|
+
.where(eq(deploymentSettings.key, key));
|
|
1079
|
+
const row = rows.find((r) => !r.encrypted);
|
|
1080
|
+
if (row) this.stored.set(key, row.value);
|
|
1081
|
+
else this.stored.delete(key);
|
|
1082
|
+
return this.resolve(key);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
955
1085
|
/** Remove one stored row (used by tests and by `prune`). */
|
|
956
1086
|
async clear(key: string): Promise<void> {
|
|
957
1087
|
await this.db.delete(deploymentSettings).where(eq(deploymentSettings.key, key));
|
|
@@ -34,7 +34,7 @@ export interface GitHubAppRepository {
|
|
|
34
34
|
* Make sure {@link token} answers with one that is still good. `asked`
|
|
35
35
|
* is for a caller waiting on the answer itself, who is told a failure.
|
|
36
36
|
*/
|
|
37
|
-
prepare(opts?: { asked?: boolean }): Promise<void>;
|
|
37
|
+
prepare(opts?: { asked?: boolean; refused?: boolean }): Promise<void>;
|
|
38
38
|
/** Whether the deployment may be pointed at this repository (`owner/name`). */
|
|
39
39
|
permits(repository: string, read: SettingReader): boolean;
|
|
40
40
|
/** Whether the app, its installation and a repository are all there. */
|
|
@@ -98,7 +98,7 @@ export class RepositorySource {
|
|
|
98
98
|
this.credentials = {
|
|
99
99
|
username: () => this.username(),
|
|
100
100
|
token: () => this.token(),
|
|
101
|
-
prepare: () => this.prepare(),
|
|
101
|
+
prepare: (opts) => this.prepare(opts),
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -185,8 +185,8 @@ export class RepositorySource {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
private async prepare(): Promise<void> {
|
|
189
|
-
if (this.inEffect === 'github-app') await this.opts.githubApp?.prepare();
|
|
188
|
+
private async prepare(opts?: { refused?: boolean }): Promise<void> {
|
|
189
|
+
if (this.inEffect === 'github-app') await this.opts.githubApp?.prepare(opts);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
private tokenIn(read: SettingReader): string {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { callLine } from '@bevel-software/platform-mcp-core';
|
|
1
2
|
import express from 'express';
|
|
2
3
|
import { describe, expect, it } from 'vitest';
|
|
3
4
|
import { ToolRegistry } from '../../tool-registry/tool-registry.js';
|
|
@@ -46,7 +47,10 @@ describe('the skill tools name the available skills within the description cap',
|
|
|
46
47
|
// than guessed, so the names below are sized to fill it EXACTLY.
|
|
47
48
|
const probe = await served([skill('x')]);
|
|
48
49
|
const fixed = probe.list.description!.slice(GUIDE_FIRST_SENTENCE.length + 1, probe.list.description!.indexOf('Currently available skills: '));
|
|
49
|
-
|
|
50
|
+
// And the `Call:` line a client is handed ahead of all of it, with the
|
|
51
|
+
// blank line after it: the example counts toward the cap too.
|
|
52
|
+
const call = callLine(`KNOWLEDGE_BASE.list_skills`, probe.list.inputs).length + 2;
|
|
53
|
+
const budget = TOOL_DESCRIPTION_CAP - call - GUIDE_FIRST_SENTENCE.length - 1 - fixed.length;
|
|
50
54
|
const head = 'Currently available skills: '.length;
|
|
51
55
|
// Twenty-one names, the LAST one short — shorter than the ", and 1 more…"
|
|
52
56
|
// tail a cut before it would carry — and the first one sized so the
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { Router, RequestHandler } from 'express';
|
|
2
|
-
import
|
|
2
|
+
import { callLine } from '@bevel-software/platform-mcp-core';
|
|
3
|
+
import type { IToolRegistry, JsonSchema, UtcpTool } from '../tool-registry/tool.contract.js';
|
|
3
4
|
import { TOOL_DESCRIPTION_CAP } from '../tool-registry/description-length.js';
|
|
4
5
|
import { GUIDE_FIRST_SENTENCE } from '../tool-registry/guide-first.js';
|
|
5
6
|
import { ToolError, type ToolContext } from '../tool-helpers/tool.contract.js';
|
|
6
7
|
import { toolDef } from '../tool-helpers/tool-def.js';
|
|
8
|
+
import { bodyEnvelope, declareRouteTool } from '../tool-helpers/route-tool-schemas.js';
|
|
7
9
|
import type { ToolHandlerFactory } from '../tool-helpers/tool-handler.js';
|
|
8
10
|
import type { ISkillService } from './skills.contract.js';
|
|
11
|
+
import { EXTERNAL_KB_MANUAL_NAME } from '../tool-manuals/tool-manuals.contract.js';
|
|
9
12
|
import type { IAllowedToolsChecker } from './allowed-tools-check.js';
|
|
10
13
|
|
|
11
14
|
/**
|
|
@@ -22,6 +25,39 @@ const BRANCH_SKILLS_INPUT = {
|
|
|
22
25
|
'a branch that does not exist answers 404 naming it. Omit it for the approved skills.',
|
|
23
26
|
};
|
|
24
27
|
|
|
28
|
+
/** What `list_skills` takes. A const so the route's check and the def share one declaration. */
|
|
29
|
+
const LIST_SKILLS_INPUTS = {
|
|
30
|
+
type: 'object' as const,
|
|
31
|
+
properties: { branch: BRANCH_SKILLS_INPUT },
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** What `get_skill` takes. A const for the same reason as {@link LIST_SKILLS_INPUTS}. */
|
|
36
|
+
const GET_SKILL_INPUTS = {
|
|
37
|
+
type: 'object' as const,
|
|
38
|
+
properties: {
|
|
39
|
+
name: { type: 'string' as const, minLength: 1, description: 'Skill name (its folder name, e.g. `rfi`).' },
|
|
40
|
+
file: {
|
|
41
|
+
type: 'string' as const,
|
|
42
|
+
description:
|
|
43
|
+
'Optional bundled file path relative to the skill folder (e.g. `scripts/build_xlsx.py`) — ' +
|
|
44
|
+
'fetch its content instead of the body.',
|
|
45
|
+
},
|
|
46
|
+
version: {
|
|
47
|
+
type: 'string' as const,
|
|
48
|
+
description:
|
|
49
|
+
'Optional: the declared version to load (e.g. `1.4.0` — the `version` that `list_skills` ' +
|
|
50
|
+
'reports: `metadata.version`, else `version`, else `lifecycle.version`). Omitted, the skill is loaded as it ' +
|
|
51
|
+
'is now, which is the latest. Given, the skill — or the `file` — is served as it was at the most ' +
|
|
52
|
+
'recent commit that declared that version; a version the skill never declared answers ' +
|
|
53
|
+
'`version_not_found` with the versions it did declare.',
|
|
54
|
+
},
|
|
55
|
+
branch: BRANCH_SKILLS_INPUT,
|
|
56
|
+
},
|
|
57
|
+
required: ['name'],
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
};
|
|
60
|
+
|
|
25
61
|
/**
|
|
26
62
|
* Registers the two skill tools (both surfaces) and hosts their endpoints.
|
|
27
63
|
*
|
|
@@ -51,6 +87,11 @@ export function registerSkillsTools(
|
|
|
51
87
|
registry.registerInternalTool((ctx) => buildListSkillsDef(skillService, ctx.userEmail));
|
|
52
88
|
registry.registerExternalTool((ctx) => buildGetSkillDef(skillService, ctx.userEmail));
|
|
53
89
|
registry.registerInternalTool((ctx) => buildGetSkillDef(skillService, ctx.userEmail));
|
|
90
|
+
// Both defs are built per catalog listing (their descriptions name the skills
|
|
91
|
+
// THIS caller may read), so declare the arguments here as well: a direct REST
|
|
92
|
+
// call that lands before the first listing must be checked against them too.
|
|
93
|
+
declareRouteTool('list_skills', LIST_SKILLS_INPUTS);
|
|
94
|
+
declareRouteTool('get_skill', GET_SKILL_INPUTS);
|
|
54
95
|
|
|
55
96
|
router.post(
|
|
56
97
|
'/agent/tools/list_skills',
|
|
@@ -139,9 +180,15 @@ async function availableSkillsLine(skillService: ISkillService, userEmail: strin
|
|
|
139
180
|
return `${head}${names.slice(0, shown).join(', ')}${rest(shown)}`;
|
|
140
181
|
}
|
|
141
182
|
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
183
|
+
/**
|
|
184
|
+
* What the fixed part of a description leaves the skills line, with the
|
|
185
|
+
* `Call:` line ahead of everything (and the blank line after it) and the
|
|
186
|
+
* guide-first sentence counted. The call line is generated from the schema
|
|
187
|
+
* the tool advertises: its flat `inputs` inside the `body` envelope.
|
|
188
|
+
*/
|
|
189
|
+
function skillsLineBudget(name: string, inputs: JsonSchema, fixed: string): number {
|
|
190
|
+
const call = callLine(`${EXTERNAL_KB_MANUAL_NAME}.${name}`, bodyEnvelope(inputs)).length + 2;
|
|
191
|
+
return TOOL_DESCRIPTION_CAP - call - GUIDE_FIRST_SENTENCE.length - 1 - fixed.length;
|
|
145
192
|
}
|
|
146
193
|
|
|
147
194
|
const LIST_SKILLS_DESCRIPTION =
|
|
@@ -158,13 +205,9 @@ async function buildListSkillsDef(skillService: ISkillService, userEmail?: strin
|
|
|
158
205
|
name: 'list_skills',
|
|
159
206
|
description:
|
|
160
207
|
LIST_SKILLS_DESCRIPTION +
|
|
161
|
-
(await availableSkillsLine(skillService, userEmail, skillsLineBudget(LIST_SKILLS_DESCRIPTION))),
|
|
208
|
+
(await availableSkillsLine(skillService, userEmail, skillsLineBudget('list_skills', LIST_SKILLS_INPUTS, LIST_SKILLS_DESCRIPTION))),
|
|
162
209
|
path: '/api/agent/tools/list_skills',
|
|
163
|
-
inputs:
|
|
164
|
-
type: 'object',
|
|
165
|
-
properties: { branch: BRANCH_SKILLS_INPUT },
|
|
166
|
-
additionalProperties: false,
|
|
167
|
-
},
|
|
210
|
+
inputs: LIST_SKILLS_INPUTS,
|
|
168
211
|
outputs: {
|
|
169
212
|
type: 'object',
|
|
170
213
|
properties: {
|
|
@@ -210,32 +253,9 @@ async function buildGetSkillDef(skillService: ISkillService, userEmail?: string)
|
|
|
210
253
|
name: 'get_skill',
|
|
211
254
|
description:
|
|
212
255
|
GET_SKILL_DESCRIPTION +
|
|
213
|
-
(await availableSkillsLine(skillService, userEmail, skillsLineBudget(GET_SKILL_DESCRIPTION))),
|
|
256
|
+
(await availableSkillsLine(skillService, userEmail, skillsLineBudget('get_skill', GET_SKILL_INPUTS, GET_SKILL_DESCRIPTION))),
|
|
214
257
|
path: '/api/agent/tools/get_skill',
|
|
215
|
-
inputs:
|
|
216
|
-
type: 'object',
|
|
217
|
-
properties: {
|
|
218
|
-
name: { type: 'string', minLength: 1, description: 'Skill name (its folder name, e.g. `rfi`).' },
|
|
219
|
-
file: {
|
|
220
|
-
type: 'string',
|
|
221
|
-
description:
|
|
222
|
-
'Optional bundled file path relative to the skill folder (e.g. `scripts/build_xlsx.py`) — ' +
|
|
223
|
-
'fetch its content instead of the body.',
|
|
224
|
-
},
|
|
225
|
-
version: {
|
|
226
|
-
type: 'string',
|
|
227
|
-
description:
|
|
228
|
-
'Optional: the declared version to load (e.g. `1.4.0` — the `version` that `list_skills` ' +
|
|
229
|
-
'reports: `metadata.version`, else `version`, else `lifecycle.version`). Omitted, the skill is loaded as it ' +
|
|
230
|
-
'is now, which is the latest. Given, the skill — or the `file` — is served as it was at the most ' +
|
|
231
|
-
'recent commit that declared that version; a version the skill never declared answers ' +
|
|
232
|
-
'`version_not_found` with the versions it did declare.',
|
|
233
|
-
},
|
|
234
|
-
branch: BRANCH_SKILLS_INPUT,
|
|
235
|
-
},
|
|
236
|
-
required: ['name'],
|
|
237
|
-
additionalProperties: false,
|
|
238
|
-
},
|
|
258
|
+
inputs: GET_SKILL_INPUTS,
|
|
239
259
|
outputs: {
|
|
240
260
|
type: 'object',
|
|
241
261
|
description: 'On success carries `skill` (or `file` when `file` was passed); on failure carries `error`.',
|
|
@@ -11,6 +11,7 @@ import type { AuthService } from '../../auth/auth.service.js';
|
|
|
11
11
|
import { InternalTokenService } from '../internal-token.service.js';
|
|
12
12
|
import { createManualAuthMiddleware, createToolAuthMiddleware } from '../tool-auth.middleware.js';
|
|
13
13
|
import type { IExternalApiKeyService } from '../external-api-key.interface.js';
|
|
14
|
+
import { ACCOUNT_DEACTIVATED_MESSAGE } from '../../auth/account-admission.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* A bearer shaped like a connection key that does not verify is answered with
|
|
@@ -24,6 +25,8 @@ import type { IExternalApiKeyService } from '../external-api-key.interface.js';
|
|
|
24
25
|
* server requests, with the same gate `create-core-server.ts` puts on each.
|
|
25
26
|
*/
|
|
26
27
|
const BAD_KEY = 'bevel_revokedKeyThatMustNeverBeEchoed';
|
|
28
|
+
/** A second refused key: on the switched-off service it is not one of a switched-off account. */
|
|
29
|
+
const OTHER_BAD_KEY = 'bevel_unknownKeyThatMustNeverBeEchoed';
|
|
27
30
|
const RESOURCE_METADATA_URL = 'https://hexis.example/.well-known/oauth-protected-resource/api/mcp';
|
|
28
31
|
const DISCOVERY_CHALLENGE = `Bearer realm="bevel-mcp", resource_metadata="${RESOURCE_METADATA_URL}"`;
|
|
29
32
|
const KEY_CHALLENGE = 'Bearer error="invalid_token", error_description="Invalid or revoked connection key"';
|
|
@@ -190,3 +193,32 @@ describe('the REST agent surface keeps its challenge for everything but a key',
|
|
|
190
193
|
}
|
|
191
194
|
});
|
|
192
195
|
});
|
|
196
|
+
|
|
197
|
+
describe('a live connection key whose account is switched off is told so', () => {
|
|
198
|
+
const switchedOffKeys = {
|
|
199
|
+
...externalApiKeys,
|
|
200
|
+
isKeyOfSwitchedOffAccount: async (t: string) => t === BAD_KEY,
|
|
201
|
+
} as unknown as IExternalApiKeyService;
|
|
202
|
+
|
|
203
|
+
for (const endpoint of KEY_ENDPOINTS) {
|
|
204
|
+
it(`${endpoint.name}: 401 with the switched-off reason, not an invitation to mint a new key`, async () => {
|
|
205
|
+
const base = await start(switchedOffKeys);
|
|
206
|
+
const res = await call(base, endpoint.method, endpoint.path, BAD_KEY);
|
|
207
|
+
expect(res.status).toBe(401);
|
|
208
|
+
expect(res.headers.get('www-authenticate')).toBe(
|
|
209
|
+
'Bearer error="invalid_token", error_description="The account of this connection key is switched off"',
|
|
210
|
+
);
|
|
211
|
+
expect(await res.json()).toEqual({ error: ACCOUNT_DEACTIVATED_MESSAGE });
|
|
212
|
+
expect(logged.join('\n')).not.toContain(BAD_KEY);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it(`${endpoint.name}: any other refused key on the same service is still told it is invalid`, async () => {
|
|
216
|
+
const base = await start(switchedOffKeys);
|
|
217
|
+
const res = await call(base, endpoint.method, endpoint.path, OTHER_BAD_KEY);
|
|
218
|
+
expect(res.status).toBe(401);
|
|
219
|
+
expect(res.headers.get('www-authenticate')).toBe(KEY_CHALLENGE);
|
|
220
|
+
expect(await res.json()).toEqual(KEY_BODY);
|
|
221
|
+
expect(logged.join('\n')).not.toContain(OTHER_BAD_KEY);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
});
|
|
@@ -276,6 +276,71 @@ describe('ExternalApiKeyService', () => {
|
|
|
276
276
|
expect(warn).toHaveBeenCalled();
|
|
277
277
|
warn.mockRestore();
|
|
278
278
|
});
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* A key's FIRST use is an agent arriving — what the onboarding waits for
|
|
282
|
+
* — so the owner is told once the stamp has landed, and once only. The
|
|
283
|
+
* first stamp is a CLAIM (`… where last_used_at is null … returning`):
|
|
284
|
+
* the request the database hands the row to announces, a racing one
|
|
285
|
+
* loses the claim, stamps the ordinary way and says nothing, and a key
|
|
286
|
+
* stamped before is touched without a word.
|
|
287
|
+
*/
|
|
288
|
+
it("announces a key's first use to its owner once the claim has landed, and never otherwise", async () => {
|
|
289
|
+
const row = { tokenId: 'tok-1', label: 'Laptop', lastUsedAt: null, userId: 'user-7', email: 'a@x.io', name: 'Alice', avatarUrl: null };
|
|
290
|
+
const events = { emit: vi.fn() };
|
|
291
|
+
// The claim's answer is held back, to pin that the announcement waits for it.
|
|
292
|
+
let claimed!: (rows: { id: string }[]) => void;
|
|
293
|
+
const claim = new Promise<{ id: string }[]>((resolve) => (claimed = resolve));
|
|
294
|
+
const first = makeFakeDb([[row], claim]);
|
|
295
|
+
await new ExternalApiKeyService(first.db, 'bevel_', {}, events).verifyAndLoadUser('bevel_abc');
|
|
296
|
+
await flushMicrotasks();
|
|
297
|
+
expect(first.calls.update).toHaveLength(1);
|
|
298
|
+
expect(first.calls.set[0][0]).toHaveProperty('lastUsedAt');
|
|
299
|
+
expect(renderSql(first.calls.where[1][0]).sql).toMatch(/"api_tokens"."id" = \$1 and "api_tokens"."last_used_at" is null/);
|
|
300
|
+
expect(events.emit).not.toHaveBeenCalled();
|
|
301
|
+
claimed([{ id: 'tok-1' }]);
|
|
302
|
+
await flushMicrotasks();
|
|
303
|
+
expect(events.emit).toHaveBeenCalledTimes(1);
|
|
304
|
+
expect(events.emit).toHaveBeenCalledWith({
|
|
305
|
+
kind: 'agent-connected',
|
|
306
|
+
forUserId: 'user-7',
|
|
307
|
+
client: 'Laptop',
|
|
308
|
+
agentKind: 'key',
|
|
309
|
+
at: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/),
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// A racing request lost the claim: the ordinary stamp, and no announcement.
|
|
313
|
+
const lost = makeFakeDb([[row], [], undefined]);
|
|
314
|
+
const lostEvents = { emit: vi.fn() };
|
|
315
|
+
await new ExternalApiKeyService(lost.db, 'bevel_', {}, lostEvents).verifyAndLoadUser('bevel_abc');
|
|
316
|
+
await flushMicrotasks();
|
|
317
|
+
expect(lost.calls.update).toHaveLength(2);
|
|
318
|
+
expect(lostEvents.emit).not.toHaveBeenCalled();
|
|
319
|
+
|
|
320
|
+
// A key stamped before: touched, and nobody is told.
|
|
321
|
+
const again = makeFakeDb([[{ ...row, lastUsedAt: new Date('2026-10-07T12:00:00Z') }], undefined]);
|
|
322
|
+
const laterEvents = { emit: vi.fn() };
|
|
323
|
+
await new ExternalApiKeyService(again.db, 'bevel_', {}, laterEvents).verifyAndLoadUser('bevel_abc');
|
|
324
|
+
await flushMicrotasks();
|
|
325
|
+
expect(again.calls.update).toHaveLength(1);
|
|
326
|
+
expect(laterEvents.emit).not.toHaveBeenCalled();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('announces nothing when the first stamp could not be written', async () => {
|
|
330
|
+
const row = { tokenId: 'tok-1', label: 'Laptop', lastUsedAt: null, userId: 'user-7', email: 'a@x.io', name: 'Alice', avatarUrl: null };
|
|
331
|
+
let fail!: (err: Error) => void;
|
|
332
|
+
const claim = new Promise<{ id: string }[]>((_resolve, reject) => (fail = reject));
|
|
333
|
+
const { db } = makeFakeDb([[row], claim]);
|
|
334
|
+
const events = { emit: vi.fn() };
|
|
335
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
336
|
+
const user = await new ExternalApiKeyService(db, 'bevel_', {}, events).verifyAndLoadUser('bevel_abc');
|
|
337
|
+
expect(user?.id).toBe('user-7');
|
|
338
|
+
fail(new Error('connection lost'));
|
|
339
|
+
await flushMicrotasks();
|
|
340
|
+
expect(events.emit).not.toHaveBeenCalled();
|
|
341
|
+
expect(warn).toHaveBeenCalled();
|
|
342
|
+
warn.mockRestore();
|
|
343
|
+
});
|
|
279
344
|
});
|
|
280
345
|
|
|
281
346
|
describe('listForUser', () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Response } from 'express';
|
|
2
|
+
import { ACCOUNT_DEACTIVATED_MESSAGE } from '../auth/account-admission.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* How every key-authenticated endpoint answers a bearer that LOOKS like a
|
|
@@ -19,7 +20,16 @@ export const INVALID_CONNECTION_KEY_CHALLENGE =
|
|
|
19
20
|
export const INVALID_CONNECTION_KEY_MESSAGE =
|
|
20
21
|
'Invalid or revoked connection key. Mint a new one in External agent access.';
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* A live key whose account an admin switched off: still a plain
|
|
25
|
+
* `invalid_token` (no sign-in invitation, see above), but told the real
|
|
26
|
+
* reason — telling them to mint a new key would send them to something
|
|
27
|
+
* they cannot do.
|
|
28
|
+
*/
|
|
29
|
+
export const SWITCHED_OFF_CONNECTION_KEY_CHALLENGE =
|
|
30
|
+
'Bearer error="invalid_token", error_description="The account of this connection key is switched off"';
|
|
31
|
+
|
|
32
|
+
export function rejectConnectionKey(res: Response, opts: { switchedOff?: boolean } = {}): void {
|
|
33
|
+
res.setHeader('WWW-Authenticate', opts.switchedOff ? SWITCHED_OFF_CONNECTION_KEY_CHALLENGE : INVALID_CONNECTION_KEY_CHALLENGE);
|
|
34
|
+
res.status(401).json({ error: opts.switchedOff ? ACCOUNT_DEACTIVATED_MESSAGE : INVALID_CONNECTION_KEY_MESSAGE });
|
|
25
35
|
}
|
|
@@ -140,6 +140,15 @@ export interface IExternalApiKeyService {
|
|
|
140
140
|
plaintext: string,
|
|
141
141
|
): Promise<{ tokenId: string; user: AuthUser } | null>;
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* For a key {@link verifyAndLoadToken} refused: whether it is a live key
|
|
145
|
+
* whose account an admin switched off — the one refusal its holder must
|
|
146
|
+
* not be told to mint a new key over (they cannot; the account is off).
|
|
147
|
+
* Asked only on the refusal path, so a working key costs nothing more.
|
|
148
|
+
* Optional: without it every refused key is reported as invalid.
|
|
149
|
+
*/
|
|
150
|
+
isKeyOfSwitchedOffAccount?(plaintext: string): Promise<boolean>;
|
|
151
|
+
|
|
143
152
|
/** Active + revoked tokens for the user, newest-first. */
|
|
144
153
|
listForUser(userId: string): Promise<ExternalApiKeySummary[]>;
|
|
145
154
|
|
|
@@ -3,7 +3,7 @@ import { logger } from '../../shared/logging.js';
|
|
|
3
3
|
|
|
4
4
|
const log = logger('external-api-key');
|
|
5
5
|
import { and, desc, eq, isNotNull, isNull, type SQL } from 'drizzle-orm';
|
|
6
|
-
import type { AuthUser } from '@bevel-software/platform-shared';
|
|
6
|
+
import type { AgentConnectedEvent, AuthUser } from '@bevel-software/platform-shared';
|
|
7
7
|
import type { Database } from '../database/connection.js';
|
|
8
8
|
import { externalApiKeys, users } from '../database/schema.js';
|
|
9
9
|
import {
|
|
@@ -51,6 +51,12 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
|
|
|
51
51
|
private readonly db: Database,
|
|
52
52
|
private readonly keyPrefix: string,
|
|
53
53
|
private readonly kinds: Readonly<Record<string, KeyKindSpec>> = {},
|
|
54
|
+
/**
|
|
55
|
+
* Where a key's FIRST use is announced to its owner (`agent-connected`,
|
|
56
|
+
* see `verifyAndLoadToken`). Optional: without it the stamp is written
|
|
57
|
+
* and nobody is told.
|
|
58
|
+
*/
|
|
59
|
+
private readonly events?: { emit(event: AgentConnectedEvent): void },
|
|
54
60
|
) {
|
|
55
61
|
this.prefixes = [keyPrefix, ...Object.values(kinds).map((k) => k.prefix)];
|
|
56
62
|
}
|
|
@@ -97,6 +103,19 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
|
|
|
97
103
|
return resolved ? resolved.user : null;
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
async isKeyOfSwitchedOffAccount(plaintext: string): Promise<boolean> {
|
|
107
|
+
if (!this.looksLikeExternalApiKey(plaintext)) return false;
|
|
108
|
+
// The same key `verifyAndLoadToken` would accept (known, not revoked),
|
|
109
|
+
// but on an account that is off.
|
|
110
|
+
const [row] = await this.db
|
|
111
|
+
.select({ id: externalApiKeys.id })
|
|
112
|
+
.from(externalApiKeys)
|
|
113
|
+
.innerJoin(users, eq(externalApiKeys.userId, users.id))
|
|
114
|
+
.where(and(eq(externalApiKeys.tokenHash, hashToken(plaintext)), isNull(externalApiKeys.revokedAt), isNotNull(users.deactivatedAt)))
|
|
115
|
+
.limit(1);
|
|
116
|
+
return row !== undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
100
119
|
async verifyAndLoadToken(
|
|
101
120
|
plaintext: string,
|
|
102
121
|
): Promise<{ tokenId: string; user: AuthUser } | null> {
|
|
@@ -114,6 +133,8 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
|
|
|
114
133
|
const [row] = await this.db
|
|
115
134
|
.select({
|
|
116
135
|
tokenId: externalApiKeys.id,
|
|
136
|
+
label: externalApiKeys.label,
|
|
137
|
+
lastUsedAt: externalApiKeys.lastUsedAt,
|
|
117
138
|
userId: users.id,
|
|
118
139
|
email: users.email,
|
|
119
140
|
name: users.name,
|
|
@@ -130,9 +151,28 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
|
|
|
130
151
|
// the UPDATE. A failed touch is logged but never blocks the caller —
|
|
131
152
|
// the worst case is a slightly stale `last_used_at`, which is fine for
|
|
132
153
|
// a "when was this key last used" audit view.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
154
|
+
//
|
|
155
|
+
// A key's FIRST use is an agent that has just reached the platform —
|
|
156
|
+
// what the onboarding waits for — so its owner is told once the stamp
|
|
157
|
+
// has landed. A key never stamped is CLAIMED (`claimFirstUse`), so of
|
|
158
|
+
// two requests racing its first use exactly one announces it; a key
|
|
159
|
+
// stamped before is touched and nobody is told.
|
|
160
|
+
const stamped = row.lastUsedAt === null ? this.claimFirstUse(row.tokenId) : this.touchLastUsed(row.tokenId).then(() => false);
|
|
161
|
+
stamped.then(
|
|
162
|
+
(first) => {
|
|
163
|
+
if (!first) return;
|
|
164
|
+
this.events?.emit({
|
|
165
|
+
kind: 'agent-connected',
|
|
166
|
+
forUserId: row.userId,
|
|
167
|
+
client: row.label,
|
|
168
|
+
agentKind: 'key',
|
|
169
|
+
at: new Date().toISOString(),
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
(err) => {
|
|
173
|
+
log.warn('touchLastUsed failed:', { err });
|
|
174
|
+
},
|
|
175
|
+
);
|
|
136
176
|
|
|
137
177
|
return {
|
|
138
178
|
tokenId: row.tokenId,
|
|
@@ -280,6 +320,23 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
|
|
|
280
320
|
.set({ lastUsedAt: new Date() })
|
|
281
321
|
.where(eq(externalApiKeys.id, tokenId));
|
|
282
322
|
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* The first stamp as ONE statement: the row is taken only while
|
|
326
|
+
* `last_used_at` is still null, so of two requests (or two replicas)
|
|
327
|
+
* racing a key's first use the database hands it to exactly one, which
|
|
328
|
+
* answers true. The loser stamps the ordinary way and answers false.
|
|
329
|
+
*/
|
|
330
|
+
private async claimFirstUse(tokenId: string): Promise<boolean> {
|
|
331
|
+
const won = await this.db
|
|
332
|
+
.update(externalApiKeys)
|
|
333
|
+
.set({ lastUsedAt: new Date() })
|
|
334
|
+
.where(and(eq(externalApiKeys.id, tokenId), isNull(externalApiKeys.lastUsedAt)))
|
|
335
|
+
.returning({ id: externalApiKeys.id });
|
|
336
|
+
if (won.length > 0) return true;
|
|
337
|
+
await this.touchLastUsed(tokenId);
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
283
340
|
}
|
|
284
341
|
|
|
285
342
|
function hashToken(plaintext: string): string {
|
|
@@ -6,7 +6,11 @@ import type { IExternalApiKeyService } from './external-api-key.interface.js';
|
|
|
6
6
|
import type { AuthService } from '../auth/auth.service.js';
|
|
7
7
|
import { ACCOUNT_DEACTIVATED_MESSAGE, AuthBackendError } from '../auth/account-admission.js';
|
|
8
8
|
import { InternalTokenService } from './internal-token.service.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
INVALID_CONNECTION_KEY_CHALLENGE,
|
|
11
|
+
INVALID_CONNECTION_KEY_MESSAGE,
|
|
12
|
+
SWITCHED_OFF_CONNECTION_KEY_CHALLENGE,
|
|
13
|
+
} from './connection-key-rejection.js';
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* What the `toolAuth` middleware resolves a bearer to, before the per-call
|
|
@@ -165,9 +169,14 @@ export function createTokenVerifier(
|
|
|
165
169
|
// External API key → external caller. (verifyAndLoadToken may throw → propagates → caller maps to 500.)
|
|
166
170
|
if (externalApiKeyService.looksLikeExternalApiKey(token)) {
|
|
167
171
|
const resolved = await externalApiKeyService.verifyAndLoadToken(token);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
172
|
+
if (resolved) {
|
|
173
|
+
return { ok: true, auth: { source: 'external', userId: resolved.user.id, tokenId: resolved.tokenId, scope: 'write' } };
|
|
174
|
+
}
|
|
175
|
+
// A live key of a switched-off account is told so, not sent to mint another.
|
|
176
|
+
if (await externalApiKeyService.isKeyOfSwitchedOffAccount?.(token)) {
|
|
177
|
+
return { ok: false, status: 401, message: ACCOUNT_DEACTIVATED_MESSAGE, challenge: SWITCHED_OFF_CONNECTION_KEY_CHALLENGE };
|
|
178
|
+
}
|
|
179
|
+
return { ok: false, status: 401, message: INVALID_CONNECTION_KEY_MESSAGE, challenge: INVALID_CONNECTION_KEY_CHALLENGE };
|
|
171
180
|
}
|
|
172
181
|
|
|
173
182
|
return missing;
|