@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
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
chainExample,
|
|
12
12
|
type ChainExampleTool,
|
|
13
13
|
runToolChain,
|
|
14
|
+
withCallExample,
|
|
14
15
|
} from '@bevel-software/platform-mcp-core';
|
|
15
16
|
import type { SpillStore } from '../workspace/spill-store.js';
|
|
16
17
|
import { utcpNameToTsInterfaceName, findToolByName, AmbiguousToolNameError } from './code-mode-names.js';
|
|
@@ -46,7 +47,8 @@ export function createCallToolChainTool(
|
|
|
46
47
|
id: 'call_tool_chain',
|
|
47
48
|
description: [
|
|
48
49
|
CodeModeUtcpClient.AGENT_PROMPT_TEMPLATE,
|
|
49
|
-
`Execute JavaScript code with direct access to all registered UTCP tools as hierarchical functions
|
|
50
|
+
`Execute JavaScript code with direct access to all registered UTCP tools as hierarchical functions, synchronous, no await.${worked} The runtime is plain JavaScript — no type annotations or other TypeScript-only syntax — plus \`atob\`, \`btoa\`, \`TextEncoder\` and \`TextDecoder\` for base64 and UTF-8 bytes, as in a browser; there is no \`Buffer\`, no \`fetch\` and no \`require\`. Return the final value with \`return\`. Use \`list_tools\` and \`tools_info\` first to discover available tools and their interfaces.`,
|
|
51
|
+
`There is NO single calling shape: some tools take their arguments under a \`body\` object, others take them flat. Call each tool as the \`Call:\` line at the top of its own description shows — it is generated from that tool's input schema, and every tool in \`${ns}\` has one. Every argument a tool declares REQUIRED must be present; for the knowledge-base tools that includes \`branch\`. Arguments that do not match the schema are refused before anything is sent or run, and the refusal carries the tool's interface.`,
|
|
50
52
|
'Error handling inside the chain: a failing tool call THROWS, and the thrown error\'s `.message` holds the server\'s actual reason (e.g. a 403 with the explanation, not just a status code). If you catch it, surface `err.message` (and `err.status` / `err.data` when present) — NEVER `return { error: err }` or otherwise return the raw Error object, because an Error serializes to `{}` (its `message` is non-enumerable) and the reason is lost. If you don\'t need to handle it, just let it throw — the runtime already reports `err.message` back to you.',
|
|
51
53
|
`Failures are answered, never dropped: a chain that throws comes back with \`success: false\` and the reason, and one that outlives \`timeout\` (default ${CHAIN_TIMEOUT_DEFAULT_MS} ms, maximum ${CHAIN_TIMEOUT_MAX_MS} ms) comes back saying it timed out — raise \`timeout\` or split the work and run it again. Either way your next tool call works as usual.`,
|
|
52
54
|
'Large return values: if the returned value exceeds `max_output_size`, the full JSON is auto-spilled to a shared spill store (outside any workspace, never committed) and the response contains only a `__tool_chain_spill__/…` ref + a truncated marker. You can read the spill back with the regular `read_file` tool — pass that ref as `path` (its `branch` is ignored) plus `offset` / `limit` to slice it, never read a multi-MB file in full. Order of preference: (1) re-run `call_tool_chain` with a follow-up code chain that filters/maps the data inline and returns just what you need; (2) narrow the API call — shorter `fields`, tighter date window, lower `limit`; (3) last resort — `read_file` against the spill ref with `offset` / `limit`. The spill is read-only context only; do NOT use it as a way to persist KB content — for KB writes use the regular `write_file` / `edit_file` tools, which go through the lock/commit pipeline.',
|
|
@@ -176,7 +178,16 @@ export function createToolsInfoTool(client: CodeModeUtcpClient) {
|
|
|
176
178
|
try {
|
|
177
179
|
const found = await findToolByName(client, name);
|
|
178
180
|
if (found) {
|
|
179
|
-
|
|
181
|
+
// With its `Call:` line, as on the MCP surface (`meta-tools.ts`):
|
|
182
|
+
// the chain's description tells the agent to call each tool as
|
|
183
|
+
// that line shows, and this is where it reads the tool. The
|
|
184
|
+
// repository's tool is not ours to edit, so a copy carries it.
|
|
185
|
+
interfaces.push(
|
|
186
|
+
client.toolToTypeScriptInterface({
|
|
187
|
+
...found.tool,
|
|
188
|
+
description: withCallExample(found.tool.description, found.utcpName, found.tool.inputs),
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
180
191
|
} else {
|
|
181
192
|
notFound.push(name);
|
|
182
193
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CORE platform tables — the git-backed workspace/workflow, auth, access,
|
|
3
3
|
* change requests, locks, the pending-commits queue, connection keys, MCP
|
|
4
|
-
* OAuth,
|
|
5
|
-
* exactly these tables.
|
|
4
|
+
* OAuth, the Secrets Vault, and the embed's Atlassian account links. A
|
|
5
|
+
* core-only deployment migrates and runs on exactly these tables.
|
|
6
6
|
*
|
|
7
7
|
* Enterprise-only tables (chat, routines, watchlist, connectors, LLM config,
|
|
8
|
-
* SharePoint
|
|
8
|
+
* SharePoint links, feedback, upload, kb-revalidation) live in
|
|
9
9
|
* `enterprise-schema.ts`, which imports the FK targets (`users`,
|
|
10
10
|
* `externalApiKeys`) from here. `schema.ts` re-exports both, so existing
|
|
11
11
|
* imports keep working unchanged.
|
|
@@ -829,3 +829,33 @@ export const pluginJoinRequests = pgTable('plugin_join_requests', {
|
|
|
829
829
|
sql`${t.status} IN ('pending', 'opened', 'failed')`,
|
|
830
830
|
),
|
|
831
831
|
}));
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Links an account from an outside system — today an Atlassian (Forge)
|
|
835
|
+
* account id — to a Hexis user, so an embed minted for that account resolves
|
|
836
|
+
* to a person whose read and write access the view obeys.
|
|
837
|
+
*
|
|
838
|
+
* KEPT UNDER ITS ENTERPRISE NAME, deliberately. The table was created by the
|
|
839
|
+
* Bevel Platform's own migration history before the embed moved into Hexis;
|
|
840
|
+
* an upgraded database already holds every link its Jira users made, and
|
|
841
|
+
* nobody is going to re-link. So the core migration creates it only if it is
|
|
842
|
+
* absent and adopts what is there otherwise (see
|
|
843
|
+
* `0017_atlassian_account_links.sql`), and the generic name this table
|
|
844
|
+
* deserves is not worth a data migration for a column nobody reads by name.
|
|
845
|
+
*
|
|
846
|
+
* Not sealed: an Atlassian account id is an opaque identifier from another
|
|
847
|
+
* system, which is the same reason the rest of those are left in the clear.
|
|
848
|
+
*
|
|
849
|
+
* PK is the account id — one Hexis user per Atlassian account — while one
|
|
850
|
+
* user may hold several account ids across sites, which is what `by_user`
|
|
851
|
+
* serves. Links die with the user (`onDelete: 'cascade'`): an erasure request
|
|
852
|
+
* must never be blocked by a leftover embed link.
|
|
853
|
+
*/
|
|
854
|
+
export const atlassianAccountLinks = pgTable('atlassian_account_links', {
|
|
855
|
+
atlassianAccountId: text('atlassian_account_id').primaryKey(),
|
|
856
|
+
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
857
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
858
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
859
|
+
}, (t) => ({
|
|
860
|
+
byUser: index('atlassian_account_links_by_user').on(t.userId),
|
|
861
|
+
}));
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { coreMigrationsDir } from '../../../assets.js';
|
|
5
|
+
|
|
6
|
+
const MIGRATION = '0017_atlassian_account_links.sql';
|
|
7
|
+
|
|
8
|
+
async function sql(): Promise<string> {
|
|
9
|
+
return fs.readFile(path.join(coreMigrationsDir(), MIGRATION), 'utf8');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The account-link table already EXISTS on an enterprise database: the Bevel
|
|
14
|
+
* Platform's own migration history created it, under this exact name and
|
|
15
|
+
* shape, while the embed lived there. Such a database must be ADOPTED as it
|
|
16
|
+
* stands, so every Atlassian link it holds keeps working and nobody re-links
|
|
17
|
+
* after the upgrade.
|
|
18
|
+
*
|
|
19
|
+
* That property lives in how this one file is WRITTEN, and the thing most
|
|
20
|
+
* likely to break it is regenerating it: `drizzle-kit generate` emits a bare
|
|
21
|
+
* `CREATE TABLE` and a bare `ADD CONSTRAINT`, either of which fails on a
|
|
22
|
+
* database that already has them — and a failed migration crash-loops the
|
|
23
|
+
* boot. So the guards are pinned here.
|
|
24
|
+
*/
|
|
25
|
+
describe('the account-link migration adopts a database that already has the table', () => {
|
|
26
|
+
it('creates the table only if it is absent', async () => {
|
|
27
|
+
expect(await sql()).toMatch(/CREATE TABLE IF NOT EXISTS "atlassian_account_links"/i);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('adds the foreign key behind an existence guard, under the name the enterprise used', async () => {
|
|
31
|
+
const text = await sql();
|
|
32
|
+
// The NAME is what the guard recognises. A different one would add a
|
|
33
|
+
// second, duplicate constraint to an upgraded database.
|
|
34
|
+
const name = 'atlassian_account_links_user_id_users_id_fk';
|
|
35
|
+
// The guard looks for THIS name, as a foreign key with the cascade, and
|
|
36
|
+
// the constraint it adds when the guard finds nothing carries the same
|
|
37
|
+
// one — tied together, so a guard naming anything else fails here.
|
|
38
|
+
const guard = /IF NOT EXISTS\s*\(\s*SELECT 1 FROM pg_constraint\s+WHERE([\s\S]*?)\)\s*THEN/i.exec(text);
|
|
39
|
+
expect(guard).not.toBeNull();
|
|
40
|
+
expect(guard![1]).toContain(`conname = '${name}'`);
|
|
41
|
+
expect(guard![1]).toMatch(/contype = 'f'/);
|
|
42
|
+
expect(guard![1]).toMatch(/confdeltype = 'c'/);
|
|
43
|
+
expect(text).toContain(`ADD CONSTRAINT "${name}"`);
|
|
44
|
+
// Links die with the user: an erasure request must never be blocked by a
|
|
45
|
+
// leftover embed link.
|
|
46
|
+
expect(text).toMatch(/ON DELETE cascade/i);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('creates the by-user index only if it is absent', async () => {
|
|
50
|
+
expect(await sql()).toMatch(/CREATE INDEX IF NOT EXISTS "atlassian_account_links_by_user"/i);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Nothing here may remove or rewrite a row or a column somebody is using.
|
|
55
|
+
*
|
|
56
|
+
* Matched on STATEMENTS rather than on words: `ON DELETE cascade` and `ON
|
|
57
|
+
* UPDATE no action` are referential actions on the key being added, not
|
|
58
|
+
* destructive statements, and a word-level rule would forbid the very
|
|
59
|
+
* clause that makes an erasure request work.
|
|
60
|
+
*/
|
|
61
|
+
it('is purely additive — it drops nothing and rewrites nothing', async () => {
|
|
62
|
+
const statements = (await sql())
|
|
63
|
+
.split('\n')
|
|
64
|
+
.filter((line) => !line.trimStart().startsWith('--'))
|
|
65
|
+
.join('\n')
|
|
66
|
+
.split(';');
|
|
67
|
+
// The VERB a statement opens with is what says whether it destroys
|
|
68
|
+
// anything. `ON DELETE cascade` and `ON UPDATE no action` sit mid-clause
|
|
69
|
+
// on the key being added and are the referential actions an erasure
|
|
70
|
+
// request depends on; `ALTER COLUMN` is a rewrite wherever it appears.
|
|
71
|
+
const destructive = statements.filter((statement) => {
|
|
72
|
+
const body = statement.trim();
|
|
73
|
+
// A statement opens a segment — or opens a block's body: inside a
|
|
74
|
+
// `DO $$ BEGIN … IF … THEN … END IF; END $$` the first statement shares
|
|
75
|
+
// its segment with the block's opening, so the verbs are looked for
|
|
76
|
+
// after BEGIN / THEN / ELSE / LOOP as well.
|
|
77
|
+
return (
|
|
78
|
+
/(^|\b(BEGIN|THEN|ELSE|LOOP)\s+)(DROP|DELETE|TRUNCATE|UPDATE)\b/i.test(body) ||
|
|
79
|
+
/ALTER\s+COLUMN/i.test(body)
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
expect(destructive).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('is registered in the journal the runner reads, exactly once', async () => {
|
|
86
|
+
const journal = JSON.parse(
|
|
87
|
+
await fs.readFile(path.join(coreMigrationsDir(), 'meta', '_journal.json'), 'utf8'),
|
|
88
|
+
) as { entries: Array<{ tag: string; idx: number }> };
|
|
89
|
+
const mine = journal.entries.filter((e) => e.tag === MIGRATION.replace(/\.sql$/, ''));
|
|
90
|
+
expect(mine).toHaveLength(1);
|
|
91
|
+
// The runner applies entries in ARRAY order, so the array order is the
|
|
92
|
+
// ledger: every entry sits at the position its idx names, and this one
|
|
93
|
+
// after every migration numbered before it.
|
|
94
|
+
expect(journal.entries.map((e) => e.idx)).toEqual(journal.entries.map((_, i) => i));
|
|
95
|
+
expect(journal.entries.findIndex((e) => e.tag === mine[0].tag)).toBe(mine[0].idx);
|
|
96
|
+
expect(mine[0].tag.startsWith(String(mine[0].idx).padStart(4, '0'))).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
EmbedRefParseError,
|
|
4
|
+
isSafeRepoRelativeEmbedPath,
|
|
5
|
+
parseEmbedRef,
|
|
6
|
+
} from '../embed-link.js';
|
|
7
|
+
|
|
8
|
+
const KB = 'knowledge-base';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The mint's reference grammar — the boundary between Hexis and the Atlassian
|
|
12
|
+
* connector's own repository, which is why its shape may not change. Two
|
|
13
|
+
* consumers hand it very different strings: `open_page` a path from a model,
|
|
14
|
+
* the connector a `content:` marker a person pasted into a ticket.
|
|
15
|
+
*/
|
|
16
|
+
describe('parseEmbedRef', () => {
|
|
17
|
+
it.each([
|
|
18
|
+
['a bare path', 'Data/Thing.md', { repoRelative: 'Data/Thing.md' }],
|
|
19
|
+
['a path under the kb folder', `${KB}/Data/Thing.md`, { repoRelative: 'Data/Thing.md' }],
|
|
20
|
+
['a leading slash', '/Data/Thing.md', { repoRelative: 'Data/Thing.md' }],
|
|
21
|
+
['a dot-slash', './Data/Thing.md', { repoRelative: 'Data/Thing.md' }],
|
|
22
|
+
[
|
|
23
|
+
'a heading anchor',
|
|
24
|
+
'Data/Thing.md#problem-statement',
|
|
25
|
+
{ repoRelative: 'Data/Thing.md', slug: 'problem-statement' },
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
'an app URL',
|
|
29
|
+
`https://hexis.example/workspace/main/${KB}/Data/Thing.md#goal`,
|
|
30
|
+
{ repoRelative: 'Data/Thing.md', slug: 'goal' },
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
'an embed URL (the branch segment is ignored)',
|
|
34
|
+
`https://hexis.example/embed/some-draft/${KB}/Data/Thing.md`,
|
|
35
|
+
{ repoRelative: 'Data/Thing.md' },
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
'an app URL of a deployment served under a path prefix',
|
|
39
|
+
`https://hexis.example/hexis/workspace/main/${KB}/Data/Thing.md#goal`,
|
|
40
|
+
{ repoRelative: 'Data/Thing.md', slug: 'goal' },
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
'an app URL whose route name arrives percent-encoded',
|
|
44
|
+
`https://hexis.example/%77orkspace/main/${KB}/Data/Thing.md`,
|
|
45
|
+
{ repoRelative: 'Data/Thing.md' },
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
'an app URL whose default branch carries a slash, encoded in its segment',
|
|
49
|
+
`https://hexis.example/workspace/team%2Fmain/${KB}/Data/Thing.md`,
|
|
50
|
+
{ repoRelative: 'Data/Thing.md' },
|
|
51
|
+
],
|
|
52
|
+
['a content: marker in angle brackets', '<content: Data/Thing.md>', { repoRelative: 'Data/Thing.md' }],
|
|
53
|
+
['a percent-encoded space', 'Data/Some%20Thing.md', { repoRelative: 'Data/Some Thing.md' }],
|
|
54
|
+
])('reads %s', (_label, raw, expected) => {
|
|
55
|
+
expect(parseEmbedRef(raw, KB)).toEqual(expected);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* EVERY file type the app renders, because the embed renders them all with
|
|
60
|
+
* the app's own renderer. The enterprise panel this grammar came from
|
|
61
|
+
* accepted `.md`, `.html` and `.htm` only — the three its own renderer could
|
|
62
|
+
* draw — and that list would now be a second, narrower answer to "what can
|
|
63
|
+
* be shown".
|
|
64
|
+
*/
|
|
65
|
+
it.each(['Shots/diagram.png', 'Docs/Report.pdf', 'Docs/Spec.docx', 'Data/rows.csv', 'Notes/plain.txt'])(
|
|
66
|
+
'reads a path of any type: %s',
|
|
67
|
+
(path) => {
|
|
68
|
+
expect(parseEmbedRef(path, KB)).toEqual({ repoRelative: path });
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
it('reads a bare node id as an id, not a path', () => {
|
|
73
|
+
expect(parseEmbedRef('hx-some-ticket', KB)).toEqual({ nodeId: 'hx-some-ticket' });
|
|
74
|
+
expect(parseEmbedRef('hx-some-ticket#notes', KB)).toEqual({
|
|
75
|
+
nodeId: 'hx-some-ticket',
|
|
76
|
+
slug: 'notes',
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('reads a single segment WITH an extension as a path, not an id', () => {
|
|
81
|
+
expect(parseEmbedRef('AGENTS.md', KB)).toEqual({ repoRelative: 'AGENTS.md' });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it.each([
|
|
85
|
+
['empty', ''],
|
|
86
|
+
['traversal', '../../etc/passwd'],
|
|
87
|
+
['a traversal inside', 'Data/../../etc/passwd'],
|
|
88
|
+
['a percent-encoded separator', 'Data%2f..%2fsecret.md'],
|
|
89
|
+
['a percent-encoded dot', 'Data/%2e%2e/secret.md'],
|
|
90
|
+
])('refuses %s', (_label, raw) => {
|
|
91
|
+
expect(() => parseEmbedRef(raw, KB)).toThrow(EmbedRefParseError);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A single segment that is not a legal id is a FILE NAME, not a bad id — a
|
|
96
|
+
* space is perfectly legal in one, and the knowledge base is full of them.
|
|
97
|
+
* Only the shape of the trailing segment decides which form a reference is
|
|
98
|
+
* (see `looksLikeNodeId`), and nothing about that is a refusal.
|
|
99
|
+
*/
|
|
100
|
+
it('reads a single segment that is not a legal id as a file name', () => {
|
|
101
|
+
expect(parseEmbedRef('Not An Id', KB)).toEqual({ repoRelative: 'Not An Id' });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('refuses a malformed percent-escape rather than throwing a URIError', () => {
|
|
105
|
+
expect(() => parseEmbedRef('Data/%zz.md', KB)).toThrow(EmbedRefParseError);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('isSafeRepoRelativeEmbedPath', () => {
|
|
110
|
+
it('refuses an absolute path, traversal and a control character', () => {
|
|
111
|
+
expect(isSafeRepoRelativeEmbedPath('/etc/passwd')).toBe(false);
|
|
112
|
+
expect(isSafeRepoRelativeEmbedPath('a/../b.md')).toBe(false);
|
|
113
|
+
expect(isSafeRepoRelativeEmbedPath('a\nb.md')).toBe(false);
|
|
114
|
+
expect(isSafeRepoRelativeEmbedPath('')).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('accepts a path with no extension at all', () => {
|
|
118
|
+
// Not a node id — it has a separator — and the app renders extensionless
|
|
119
|
+
// text files, so nothing here may refuse it.
|
|
120
|
+
expect(isSafeRepoRelativeEmbedPath('Data/LICENSE')).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import type { Server } from 'node:http';
|
|
2
|
+
import type { AddressInfo } from 'node:net';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import cors from 'cors';
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { createEmbedLinkRoutes, createEmbedRoutes, TOKEN_REQUIRED } from '../embed.routes.js';
|
|
7
|
+
import {
|
|
8
|
+
EmbedAccessError,
|
|
9
|
+
EmbedLockedError,
|
|
10
|
+
EmbedNodeNotFoundError,
|
|
11
|
+
EmbedTokenError,
|
|
12
|
+
} from '../embed.errors.js';
|
|
13
|
+
|
|
14
|
+
const TOKEN = 'a-valid-token';
|
|
15
|
+
|
|
16
|
+
let server: Server | null = null;
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
if (server) await new Promise<void>((resolve) => server!.close(() => resolve()));
|
|
19
|
+
server = null;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function stubService(overrides: Record<string, unknown> = {}) {
|
|
23
|
+
return {
|
|
24
|
+
sharedSecretConfigured: vi.fn(() => true),
|
|
25
|
+
verifySharedSecret: vi.fn((s?: string) => s === 'shh'),
|
|
26
|
+
mintForUser: vi.fn(async () => ({ token: TOKEN, embedUrl: 'https://hexis.example/embed?token=' + TOKEN })),
|
|
27
|
+
mintToken: vi.fn(async () => ({ token: TOKEN, embedUrl: 'https://hexis.example/embed?token=' + TOKEN })),
|
|
28
|
+
loadFile: vi.fn(async (token: string) => {
|
|
29
|
+
if (token !== TOKEN) throw new EmbedTokenError();
|
|
30
|
+
return { nodeName: 'Thing', content: '# Thing', canWrite: true };
|
|
31
|
+
}),
|
|
32
|
+
readBytes: vi.fn(async () => ({ bytes: Buffer.from([1, 2, 3]), path: 'Data/x.png' })),
|
|
33
|
+
acquireLock: vi.fn(async () => ({ acquired: true })),
|
|
34
|
+
heartbeat: vi.fn(async () => undefined),
|
|
35
|
+
cancel: vi.fn(async () => undefined),
|
|
36
|
+
save: vi.fn(async () => undefined),
|
|
37
|
+
propose: vi.fn(async () => ({ branch: 'suggestions/a-1/knowledge', number: 7, url: '/change-requests/7' })),
|
|
38
|
+
linkAccount: vi.fn(async () => undefined),
|
|
39
|
+
listLinkedAccounts: vi.fn(async () => [{ atlassianAccountId: 'acc-1', createdAt: 0 }]),
|
|
40
|
+
unlinkAccount: vi.fn(async () => true),
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The app as the server mounts it: the embed routes ahead of the JWT
|
|
47
|
+
* middleware, the link routes behind it, and a static catch-all last — the
|
|
48
|
+
* same order `create-core-server` uses, because the order is what makes the
|
|
49
|
+
* framing headers land before `index.html` is served.
|
|
50
|
+
*/
|
|
51
|
+
async function serve(
|
|
52
|
+
service: ReturnType<typeof stubService>,
|
|
53
|
+
opts: { session?: boolean; restrictFraming?: boolean; appWideCors?: boolean } = {},
|
|
54
|
+
) {
|
|
55
|
+
const app = express();
|
|
56
|
+
// The server's own CORS, as `create-core-server` mounts it ahead of every
|
|
57
|
+
// route: any origin reflected, credentials allowed, preflights answered.
|
|
58
|
+
if (opts.appWideCors) app.use(cors({ origin: true, credentials: true }));
|
|
59
|
+
app.use(express.json());
|
|
60
|
+
// A deployment (or a later change to this app) that stamps a framing
|
|
61
|
+
// restriction on EVERY response, ahead of the embed routes — the case the
|
|
62
|
+
// embed page's own header removal exists for.
|
|
63
|
+
if (opts.restrictFraming) {
|
|
64
|
+
app.use((_req, res, next) => {
|
|
65
|
+
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
66
|
+
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'");
|
|
67
|
+
next();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
app.use(createEmbedRoutes(service as never));
|
|
71
|
+
const authMiddleware: express.RequestHandler = (req, res, next) => {
|
|
72
|
+
if (!opts.session) {
|
|
73
|
+
res.status(401).json({ error: 'Unauthenticated' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
req.userId = 'u-1';
|
|
77
|
+
next();
|
|
78
|
+
};
|
|
79
|
+
app.use('/api', authMiddleware, createEmbedLinkRoutes(service as never));
|
|
80
|
+
app.get('{*path}', (_req, res) => res.type('html').send('<!doctype html><title>SPA</title>'));
|
|
81
|
+
|
|
82
|
+
server = app.listen(0);
|
|
83
|
+
await new Promise<void>((r) => server!.once('listening', () => r()));
|
|
84
|
+
const base = `http://127.0.0.1:${(server!.address() as AddressInfo).port}`;
|
|
85
|
+
return {
|
|
86
|
+
base,
|
|
87
|
+
get: (path: string, headers: Record<string, string> = {}) => fetch(`${base}${path}`, { headers }),
|
|
88
|
+
post: (path: string, body: unknown, headers: Record<string, string> = {}) =>
|
|
89
|
+
fetch(`${base}${path}`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: { 'content-type': 'application/json', ...headers },
|
|
92
|
+
body: JSON.stringify(body),
|
|
93
|
+
}),
|
|
94
|
+
del: (path: string) => fetch(`${base}${path}`, { method: 'DELETE' }),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Every route that serves FILE CONTENT or changes a file. The framing rule
|
|
100
|
+
* makes the embed page reachable from any site, so the token is the whole of
|
|
101
|
+
* the security — a session fallback would let a hostile frame read and write
|
|
102
|
+
* as whoever happened to be signed in.
|
|
103
|
+
*/
|
|
104
|
+
const DATA_ROUTES = [
|
|
105
|
+
['GET', '/api/embed/load'],
|
|
106
|
+
['GET', '/api/embed/raw'],
|
|
107
|
+
['POST', '/api/embed/lock'],
|
|
108
|
+
['POST', '/api/embed/heartbeat'],
|
|
109
|
+
['POST', '/api/embed/cancel'],
|
|
110
|
+
['POST', '/api/embed/save'],
|
|
111
|
+
['POST', '/api/embed/propose'],
|
|
112
|
+
] as const;
|
|
113
|
+
|
|
114
|
+
describe('the embed data routes take the token and nothing else', () => {
|
|
115
|
+
it.each(DATA_ROUTES)('%s %s refuses a request with a valid session and no token', async (method, path) => {
|
|
116
|
+
const client = await serve(stubService(), { session: true });
|
|
117
|
+
const res =
|
|
118
|
+
method === 'GET'
|
|
119
|
+
? await client.get(path, { cookie: 'bevel_token=a-real-session' })
|
|
120
|
+
: await client.post(path, { content: 'x' }, { cookie: 'bevel_token=a-real-session' });
|
|
121
|
+
expect(res.status).toBe(401);
|
|
122
|
+
expect((await res.json()).error).toBe(TOKEN_REQUIRED);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it.each(DATA_ROUTES)('%s %s answers a request with a token and no session', async (method, path) => {
|
|
126
|
+
const client = await serve(stubService());
|
|
127
|
+
const res =
|
|
128
|
+
method === 'GET'
|
|
129
|
+
? await client.get(`${path}?token=${TOKEN}`)
|
|
130
|
+
: await client.post(path, { token: TOKEN, content: 'x' });
|
|
131
|
+
expect(res.status).toBeLessThan(400);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('refuses an expired or rejected token with 401, so the view can say it expired', async () => {
|
|
135
|
+
const client = await serve(stubService());
|
|
136
|
+
const res = await client.get('/api/embed/load?token=stale');
|
|
137
|
+
expect(res.status).toBe(401);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('framing', () => {
|
|
142
|
+
/**
|
|
143
|
+
* Any host may frame the embed page — that is what makes it work in every
|
|
144
|
+
* host's sandbox without an admin entry per host, and the token is what
|
|
145
|
+
* secures it. `X-Frame-Options` is REMOVED rather than merely unset, so a
|
|
146
|
+
* header added globally later cannot silently break every host's embed.
|
|
147
|
+
*/
|
|
148
|
+
it('sends no framing restriction on the embed page, even under a global one', async () => {
|
|
149
|
+
const client = await serve(stubService(), { restrictFraming: true });
|
|
150
|
+
// The restriction is really there on every other page…
|
|
151
|
+
const other = await client.get('/workspace/main');
|
|
152
|
+
expect(other.headers.get('x-frame-options')).toBe('SAMEORIGIN');
|
|
153
|
+
// …and the embed page clears it.
|
|
154
|
+
const res = await client.get('/embed?token=' + TOKEN);
|
|
155
|
+
expect(res.status).toBe(200);
|
|
156
|
+
expect(res.headers.get('x-frame-options')).toBeNull();
|
|
157
|
+
expect(res.headers.get('content-security-policy')).toBeNull();
|
|
158
|
+
// And it still falls through to the SPA.
|
|
159
|
+
expect(await res.text()).toContain('<!doctype html>');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The account-link page acts under the SIGNED-IN SESSION, so a page that
|
|
164
|
+
* could frame it could link a foreign account to a victim.
|
|
165
|
+
*/
|
|
166
|
+
it('refuses every framing ancestor on the account-link page', async () => {
|
|
167
|
+
const client = await serve(stubService(), { restrictFraming: true });
|
|
168
|
+
const res = await client.get('/embed/link?token=' + TOKEN);
|
|
169
|
+
expect(res.status).toBe(200);
|
|
170
|
+
expect(res.headers.get('content-security-policy')).toBe("frame-ancestors 'none'");
|
|
171
|
+
expect(res.headers.get('x-frame-options')).toBe('DENY');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('the connector mint', () => {
|
|
176
|
+
it('mints from the shared secret, an account id and a reference', async () => {
|
|
177
|
+
const service = stubService();
|
|
178
|
+
const client = await serve(service);
|
|
179
|
+
const res = await client.post(
|
|
180
|
+
'/api/embed/token',
|
|
181
|
+
{ accountId: 'acc-1', email: 'a@x.io', reference: 'Data/Thing.md' },
|
|
182
|
+
{ 'x-embed-secret': 'shh' },
|
|
183
|
+
);
|
|
184
|
+
expect(res.status).toBe(200);
|
|
185
|
+
expect(await res.json()).toMatchObject({ token: TOKEN });
|
|
186
|
+
expect(service.mintToken).toHaveBeenCalledWith({
|
|
187
|
+
accountId: 'acc-1',
|
|
188
|
+
email: 'a@x.io',
|
|
189
|
+
reference: 'Data/Thing.md',
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('refuses a wrong secret and a missing one', async () => {
|
|
194
|
+
const client = await serve(stubService());
|
|
195
|
+
expect((await client.post('/api/embed/token', { accountId: 'a', reference: 'b' }, { 'x-embed-secret': 'no' })).status).toBe(401);
|
|
196
|
+
expect((await client.post('/api/embed/token', { accountId: 'a', reference: 'b' })).status).toBe(401);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('refuses an incomplete body', async () => {
|
|
200
|
+
const client = await serve(stubService());
|
|
201
|
+
const res = await client.post('/api/embed/token', { accountId: 'acc-1' }, { 'x-embed-secret': 'shh' });
|
|
202
|
+
expect(res.status).toBe(400);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/** A deployment with no connector pointed at it has no such route at all. */
|
|
206
|
+
it('is absent when no shared secret is configured', async () => {
|
|
207
|
+
const service = stubService({ sharedSecretConfigured: vi.fn(() => false) });
|
|
208
|
+
const client = await serve(service);
|
|
209
|
+
const res = await client.post('/api/embed/token', { accountId: 'a', reference: 'b' }, { 'x-embed-secret': 'shh' });
|
|
210
|
+
expect(res.status).toBe(404);
|
|
211
|
+
expect(service.verifySharedSecret).not.toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('the data routes', () => {
|
|
216
|
+
it('serves the page to render', async () => {
|
|
217
|
+
const client = await serve(stubService());
|
|
218
|
+
const res = await client.get(`/api/embed/load?token=${TOKEN}`);
|
|
219
|
+
expect(await res.json()).toMatchObject({ nodeName: 'Thing' });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('serves bytes inline, never as a download, and never from a shared cache', async () => {
|
|
223
|
+
const client = await serve(stubService());
|
|
224
|
+
const res = await client.get(`/api/embed/raw?token=${TOKEN}&path=assets/x.png`);
|
|
225
|
+
expect(res.status).toBe(200);
|
|
226
|
+
expect(res.headers.get('content-disposition')).toBe('inline');
|
|
227
|
+
expect(res.headers.get('cache-control')).toContain('no-store');
|
|
228
|
+
expect(res.headers.get('x-content-type-options')).toBe('nosniff');
|
|
229
|
+
expect([...new Uint8Array(await res.arrayBuffer())]).toEqual([1, 2, 3]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('saves, and proposes, through the service', async () => {
|
|
233
|
+
const service = stubService();
|
|
234
|
+
const client = await serve(service);
|
|
235
|
+
expect((await client.post('/api/embed/save', { token: TOKEN, content: 'new' })).status).toBe(204);
|
|
236
|
+
expect(service.save).toHaveBeenCalledWith(TOKEN, 'new');
|
|
237
|
+
const proposed = await client.post('/api/embed/propose', { token: TOKEN, content: 'draft' });
|
|
238
|
+
expect(await proposed.json()).toMatchObject({ number: 7 });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('says who holds the lock when a save is refused for it', async () => {
|
|
242
|
+
const service = stubService({
|
|
243
|
+
save: vi.fn(async () => {
|
|
244
|
+
throw new EmbedLockedError('Bob');
|
|
245
|
+
}),
|
|
246
|
+
});
|
|
247
|
+
const client = await serve(service);
|
|
248
|
+
const res = await client.post('/api/embed/save', { token: TOKEN, content: 'x' });
|
|
249
|
+
expect(res.status).toBe(409);
|
|
250
|
+
expect((await res.json()).error).toContain('Bob');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
/** Protected text and a token-bearing link: nothing may keep the answer. */
|
|
254
|
+
it('forbids caching the loaded page, as it does the raw bytes', async () => {
|
|
255
|
+
const client = await serve(stubService());
|
|
256
|
+
const load = await client.get(`/api/embed/load?token=${TOKEN}`);
|
|
257
|
+
expect(load.headers.get('cache-control')).toBe('no-store, private');
|
|
258
|
+
const refused = await client.get('/api/embed/load?token=wrong');
|
|
259
|
+
expect(refused.headers.get('cache-control')).toBe('no-store, private');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('refuses a save with no content', async () => {
|
|
263
|
+
const client = await serve(stubService());
|
|
264
|
+
expect((await client.post('/api/embed/save', { token: TOKEN })).status).toBe(400);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it.each([
|
|
268
|
+
[new EmbedTokenError(), 401],
|
|
269
|
+
[new EmbedAccessError(), 403],
|
|
270
|
+
[new EmbedNodeNotFoundError('gone'), 404],
|
|
271
|
+
[new EmbedLockedError('Bob'), 409],
|
|
272
|
+
])('maps %s to its status', async (error, status) => {
|
|
273
|
+
const service = stubService({
|
|
274
|
+
loadFile: vi.fn(async () => {
|
|
275
|
+
throw error;
|
|
276
|
+
}),
|
|
277
|
+
});
|
|
278
|
+
const client = await serve(service);
|
|
279
|
+
expect((await client.get(`/api/embed/load?token=${TOKEN}`)).status).toBe(status);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('does not leak an unexpected failure message', async () => {
|
|
283
|
+
const service = stubService({
|
|
284
|
+
loadFile: vi.fn(async () => {
|
|
285
|
+
throw new Error('postgres://user:pw@host down');
|
|
286
|
+
}),
|
|
287
|
+
});
|
|
288
|
+
const client = await serve(service);
|
|
289
|
+
const res = await client.get(`/api/embed/load?token=${TOKEN}`);
|
|
290
|
+
expect(res.status).toBe(500);
|
|
291
|
+
expect((await res.json()).error).toBe('Something went wrong.');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe('the account-link routes', () => {
|
|
296
|
+
it('link, list and unlink need a session', async () => {
|
|
297
|
+
const client = await serve(stubService(), { session: false });
|
|
298
|
+
expect((await client.post('/api/embed/link', { token: TOKEN })).status).toBe(401);
|
|
299
|
+
expect((await client.get('/api/embed/links')).status).toBe(401);
|
|
300
|
+
expect((await client.del('/api/embed/links/acc-1')).status).toBe(401);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('links the token account to the signed-in user', async () => {
|
|
304
|
+
const service = stubService();
|
|
305
|
+
const client = await serve(service, { session: true });
|
|
306
|
+
expect((await client.post('/api/embed/link', { token: TOKEN })).status).toBe(204);
|
|
307
|
+
expect(service.linkAccount).toHaveBeenCalledWith(TOKEN, 'u-1');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('answers 404 for a link that is not this user own, so it cannot probe others', async () => {
|
|
311
|
+
const service = stubService({ unlinkAccount: vi.fn(async () => false) });
|
|
312
|
+
const client = await serve(service, { session: true });
|
|
313
|
+
expect((await client.del('/api/embed/links/someone-elses')).status).toBe(404);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The MCP App view runs the embed inside a chat host's SANDBOX — another
|
|
319
|
+
* origin, and one no deployment can know in advance — and calls the token
|
|
320
|
+
* routes from there. The server's app-wide CORS is what lets it: mounted
|
|
321
|
+
* here as `create-core-server` mounts it, so what is pinned is the behaviour
|
|
322
|
+
* a sandbox actually meets, not a header these routes set themselves. Safe
|
|
323
|
+
* on the token routes for the reason the token-only rule exists: CORS guards
|
|
324
|
+
* a browser's ambient credentials, these routes take none, and the token in
|
|
325
|
+
* the request is the whole credential.
|
|
326
|
+
*/
|
|
327
|
+
describe('a cross-origin caller reaches the token routes through the app-wide CORS', () => {
|
|
328
|
+
const SANDBOX = 'https://sandbox.example';
|
|
329
|
+
const FOREIGN = { origin: SANDBOX };
|
|
330
|
+
|
|
331
|
+
it.each(DATA_ROUTES)('%s %s answers the sandbox origin', async (method, path) => {
|
|
332
|
+
const client = await serve(stubService(), { appWideCors: true });
|
|
333
|
+
const res =
|
|
334
|
+
method === 'GET'
|
|
335
|
+
? await client.get(`${path}?token=${TOKEN}`, FOREIGN)
|
|
336
|
+
: await client.post(path, { token: TOKEN, content: 'x' }, FOREIGN);
|
|
337
|
+
expect(res.status).toBeLessThan(400);
|
|
338
|
+
expect(res.headers.get('access-control-allow-origin')).toBe(SANDBOX);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('answers the preflight a JSON POST from the sandbox triggers', async () => {
|
|
342
|
+
const client = await serve(stubService(), { appWideCors: true });
|
|
343
|
+
const res = await fetch(`${client.base}/api/embed/save`, {
|
|
344
|
+
method: 'OPTIONS',
|
|
345
|
+
headers: { ...FOREIGN, 'access-control-request-method': 'POST', 'access-control-request-headers': 'content-type' },
|
|
346
|
+
});
|
|
347
|
+
expect(res.status).toBe(204);
|
|
348
|
+
expect(res.headers.get('access-control-allow-origin')).toBe(SANDBOX);
|
|
349
|
+
expect(res.headers.get('access-control-allow-methods')).toContain('POST');
|
|
350
|
+
expect(res.headers.get('access-control-allow-headers')?.toLowerCase()).toContain('content-type');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('still refuses a sandbox caller that brings no token', async () => {
|
|
354
|
+
// Being reachable from another origin changes nothing about who may
|
|
355
|
+
// read: the token is the credential, and a request without it is 401
|
|
356
|
+
// from the sandbox exactly as from a tab.
|
|
357
|
+
const client = await serve(stubService(), { appWideCors: true });
|
|
358
|
+
const res = await client.get('/api/embed/load', FOREIGN);
|
|
359
|
+
expect(res.status).toBe(401);
|
|
360
|
+
expect((await res.json()).error).toBe(TOKEN_REQUIRED);
|
|
361
|
+
});
|
|
362
|
+
});
|