@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
|
@@ -114,6 +114,20 @@ describe('the shared file rules are one text, in two places', () => {
|
|
|
114
114
|
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
describe('the delete rule for platform files', () => {
|
|
118
|
+
const managedItems = (): string => sharedFileRules(DEFAULT_KB_LAYOUT).find((r) => r.id === 'managed-items')!.body;
|
|
119
|
+
|
|
120
|
+
it('says a nested access.md is deleted by whoever may write it, and the root\'s platform files by nobody', async () => {
|
|
121
|
+
const body = managedItems();
|
|
122
|
+
expect(body).toContain('delete_file deletes a nested `access.md` for whoever may write it');
|
|
123
|
+
expect(body).toContain("its folder then follows its parent's rules");
|
|
124
|
+
expect(body).toContain("the repository root's `access.md` and `roles.yaml` are deleted by nobody");
|
|
125
|
+
// The sentence the refusal actually carries, as the tools and the app say it.
|
|
126
|
+
expect(body).toContain("\"<name> is the repository's own file and cannot be deleted.\"");
|
|
127
|
+
expect(await composeAgentGuide(DEFAULT_KB_LAYOUT)).toContain(body);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
117
131
|
describe('the handshake text stays inside the length it pins', () => {
|
|
118
132
|
it('holds the shared section under its own cap', () => {
|
|
119
133
|
const section = sharedFileRulesSection(DEFAULT_KB_LAYOUT);
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
stripHtmlComments,
|
|
31
31
|
type KbLayout,
|
|
32
32
|
} from '@bevel-software/platform-shared';
|
|
33
|
+
import { splitCallLine } from '@bevel-software/platform-mcp-core';
|
|
33
34
|
import { sharedFileRulesSection } from './shared-file-rules.js';
|
|
34
35
|
|
|
35
36
|
export { PREAMBLE_CAP, PREAMBLE_FILE, TOOL_PREFIX_CAP };
|
|
@@ -48,7 +49,13 @@ export const PLATFORM_HEADER =
|
|
|
48
49
|
'Before answering a question about the organisation, its people, customers, products, processes, projects or internal terms, ' +
|
|
49
50
|
'search the knowledge base: call `start_session` once, then `grep` for the key terms, `list_files` to orient, and `read_file` what matches. ' +
|
|
50
51
|
'Prefer what you find there over memory or the web, and say so when the knowledge base is silent on something the organisation should have documented. ' +
|
|
51
|
-
'Skills are available as prompts and through `list_skills` and `get_skill`.'
|
|
52
|
+
'Skills are available as prompts and through `list_skills` and `get_skill`. ' +
|
|
53
|
+
// There is no one calling shape to state: the knowledge-base tools take
|
|
54
|
+
// their arguments under `body`, a tool that calls another service takes them
|
|
55
|
+
// flat, and an agent told one rule for all of them sends a GET with a body.
|
|
56
|
+
// The per-tool `Call:` line is generated from each tool's own input schema,
|
|
57
|
+
// so pointing at it is both shorter and always right.
|
|
58
|
+
'Tools do not share one calling shape: call each tool exactly as the `Call:` line at the top of its description shows.';
|
|
52
59
|
|
|
53
60
|
/**
|
|
54
61
|
* The fixed first line of the tool prefix. It always leads, so an admin's
|
|
@@ -167,9 +174,17 @@ export function composeAgentInstructions(
|
|
|
167
174
|
* A tool description with the prefix ahead of it: the prefix, a blank line,
|
|
168
175
|
* then the original. Purpose line first, because claude.ai cuts descriptions
|
|
169
176
|
* near 500 characters.
|
|
177
|
+
*
|
|
178
|
+
* Except for the `Call:` line, which stays the very first line whatever else
|
|
179
|
+
* is prepended: it is how an agent learns the shape this tool's arguments take,
|
|
180
|
+
* and a description that opens with the purpose prefix instead would bury the
|
|
181
|
+
* one line the refusal for a wrong call points back to.
|
|
170
182
|
*/
|
|
171
183
|
export function prefixToolDescription(toolPrefix: string, description: string | undefined): string {
|
|
172
|
-
|
|
184
|
+
if (!description) return toolPrefix;
|
|
185
|
+
const { call, rest } = splitCallLine(description);
|
|
186
|
+
if (call === null) return `${toolPrefix}\n\n${description}`;
|
|
187
|
+
return rest === '' ? `${call}\n\n${toolPrefix}` : `${call}\n\n${toolPrefix}\n\n${rest}`;
|
|
173
188
|
}
|
|
174
189
|
|
|
175
190
|
/** An ATX heading line: `#` to `######`, then a space or the end. */
|
|
@@ -145,15 +145,15 @@ export function sharedFileRules(layout: KbLayout): readonly SharedFileRule[] {
|
|
|
145
145
|
// that take content as a JSON string names the route in one sentence of
|
|
146
146
|
// its own; WHY a file should go that way, and how, is here.
|
|
147
147
|
id: 'upload-route',
|
|
148
|
-
heading: 'Large, escape-heavy and binary content
|
|
148
|
+
heading: 'Large, escape-heavy and binary content: upload in, download out',
|
|
149
149
|
body:
|
|
150
|
-
'write_file, write_files and edit_file take content as a JSON string you
|
|
151
|
-
'
|
|
152
|
-
'
|
|
153
|
-
'
|
|
154
|
-
'
|
|
155
|
-
'
|
|
156
|
-
'
|
|
150
|
+
'write_file, write_files and edit_file take content as a JSON string you type out in full, so a long file ' +
|
|
151
|
+
'is cut off mid-answer, a file full of backslashes or `\\u` escapes fails to parse as a parameter, and an ' +
|
|
152
|
+
'image, a PDF or a zip cannot be sent at all. For those, call `request_file_upload`, POST the file (or one ' +
|
|
153
|
+
'zip of many) with any HTTP client (`curl -X POST --data-binary @<file> ' +
|
|
154
|
+
'"<uploadUrl>?filename=<name>"`), then `apply_file_upload` lands it in one commit. To take copies OUT, ' +
|
|
155
|
+
'`request_file_download` answers a one-time link per file and a zip per folder (`curl -o <name> ' +
|
|
156
|
+
'"<downloadUrl>"`). Either way no bytes pass through the conversation.',
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
id: 'dry-run-confirm',
|
|
@@ -163,22 +163,22 @@ export function sharedFileRules(layout: KbLayout): readonly SharedFileRule[] {
|
|
|
163
163
|
'what the call would touch, `allowed`, and `reason` when it may not run. A non-empty folder is deleted, and a ' +
|
|
164
164
|
'move that changes your access runs, only with `confirm: true`; without it the call changes nothing and ' +
|
|
165
165
|
'returns the same impact with `confirmationRequired: true`. Do NOT set `confirm: true` on your first call — ' +
|
|
166
|
-
'dry-run, check the impact, then confirm. delete_file takes neither: it
|
|
167
|
-
'
|
|
166
|
+
'dry-run, check the impact, then confirm. delete_file takes neither: check it first with file_stat ' +
|
|
167
|
+
'(`deletable`) if you are unsure. delete_branch has its own `dryRun`.',
|
|
168
168
|
},
|
|
169
169
|
{
|
|
170
170
|
id: 'managed-items',
|
|
171
171
|
heading: 'What these tools never move or delete',
|
|
172
172
|
body:
|
|
173
|
-
`A platform file (${platformFileList(layout)}) is
|
|
174
|
-
'
|
|
175
|
-
|
|
176
|
-
'
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
'
|
|
180
|
-
'able to write
|
|
181
|
-
'file_stat reports `managed`, `movable` and `deletable`
|
|
173
|
+
`A platform file (${platformFileList(layout)}) is never moved: "<name> is a platform file and stays in its folder." ` +
|
|
174
|
+
'A folder moved or deleted takes its own along, in one change. delete_file deletes a nested `access.md` for ' +
|
|
175
|
+
"whoever may write it (its folder then follows its parent's rules) and refuses `.bevelignore`; the repository " +
|
|
176
|
+
"root's `access.md` and `roles.yaml` are deleted by nobody: \"<name> is the repository's own file and cannot be " +
|
|
177
|
+
'deleted." A platform folder (the root, or a reserved one like ' +
|
|
178
|
+
`\`${layout.knowledgeBaseDir}/\`), git metadata, and making a platform file or folder at a destination are ` +
|
|
179
|
+
'refused; so is a path that is or crosses a symlink: links are never followed or removed. On a protected ' +
|
|
180
|
+
'branch you must be able to write all a call touches — for a folder, every file under it, at its old and new ' +
|
|
181
|
+
'path. file_stat reports `managed`, `movable` and `deletable` up front.',
|
|
182
182
|
},
|
|
183
183
|
{
|
|
184
184
|
id: 'refused-for-permissions',
|
|
@@ -14,13 +14,17 @@ import type { IExternalApiKeyService } from '../../tool-auth/external-api-key.in
|
|
|
14
14
|
* update / delete` pops the next queued result. `transaction` runs its
|
|
15
15
|
* callback against the same fake, so queued results are consumed in order.
|
|
16
16
|
* Every `where` predicate and `update` target is captured, so a test can pin
|
|
17
|
-
* WHICH rows a statement addressed (see {@link render}), not just that it ran
|
|
17
|
+
* WHICH rows a statement addressed (see {@link render}), not just that it ran;
|
|
18
|
+
* `orderBy` and `limit` too, since the fake applies neither and a test can
|
|
19
|
+
* only pin WHICH row of several a read keeps by the clauses it sent.
|
|
18
20
|
*/
|
|
19
21
|
function makeFakeDb(queue: unknown[]) {
|
|
20
|
-
const captured: { values: any[]; set: any[]; where: SQL[]; updateTargets: unknown[] } = {
|
|
22
|
+
const captured: { values: any[]; set: any[]; where: SQL[]; orderBy: SQL[][]; limit: number[]; updateTargets: unknown[] } = {
|
|
21
23
|
values: [],
|
|
22
24
|
set: [],
|
|
23
25
|
where: [],
|
|
26
|
+
orderBy: [],
|
|
27
|
+
limit: [],
|
|
24
28
|
updateTargets: [],
|
|
25
29
|
};
|
|
26
30
|
const counts = { insert: 0, select: 0, update: 0, delete: 0 };
|
|
@@ -36,10 +40,10 @@ function makeFakeDb(queue: unknown[]) {
|
|
|
36
40
|
chain.values = passthrough((a) => captured.values.push(a[0]));
|
|
37
41
|
chain.set = passthrough((a) => captured.set.push(a[0]));
|
|
38
42
|
chain.where = passthrough((a) => captured.where.push(a[0]));
|
|
39
|
-
chain.limit = passthrough();
|
|
43
|
+
chain.limit = passthrough((a) => captured.limit.push(a[0]));
|
|
40
44
|
chain.innerJoin = passthrough();
|
|
41
45
|
chain.from = passthrough();
|
|
42
|
-
chain.orderBy = passthrough();
|
|
46
|
+
chain.orderBy = passthrough((a) => captured.orderBy.push(a));
|
|
43
47
|
chain.groupBy = passthrough();
|
|
44
48
|
chain.returning = passthrough();
|
|
45
49
|
chain.then = (onF: any, onR: any) => Promise.resolve(result).then(onF, onR);
|
|
@@ -355,6 +359,62 @@ describe('AgentAuditService.revokeConnection', () => {
|
|
|
355
359
|
});
|
|
356
360
|
});
|
|
357
361
|
|
|
362
|
+
/**
|
|
363
|
+
* The onboarding's "is your agent connected yet?". The page polls it, so the
|
|
364
|
+
* contract worth pinning is what it reads (one person's LIVE, USED rows of
|
|
365
|
+
* each kind — never the event log) and how the two kinds are combined.
|
|
366
|
+
*/
|
|
367
|
+
describe('AgentAuditService.lastAgentUse', () => {
|
|
368
|
+
it('answers null when neither an agent connection nor a key has been used', async () => {
|
|
369
|
+
const { service, counts } = makeService([[] /* connections */, [] /* keys */]);
|
|
370
|
+
await expect(service.lastAgentUse(ALICE.id)).resolves.toBeNull();
|
|
371
|
+
expect(counts.select).toBe(2);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("reads only the caller's live, used rows of each kind", async () => {
|
|
375
|
+
const { service, captured } = makeService([[], []]);
|
|
376
|
+
await service.lastAgentUse(ALICE.id);
|
|
377
|
+
const [connections, keys] = captured.where.map(render);
|
|
378
|
+
expect(connections!.sql).toContain('"agent_connections"."user_id" = $1');
|
|
379
|
+
expect(connections!.sql).toContain('"agent_connections"."revoked_at" is null');
|
|
380
|
+
expect(connections!.sql).toContain('"agent_connections"."last_used_at" is not null');
|
|
381
|
+
expect(connections!.params).toEqual([ALICE.id]);
|
|
382
|
+
expect(keys!.sql).toContain('"api_tokens"."user_id" = $1');
|
|
383
|
+
expect(keys!.sql).toContain('"api_tokens"."revoked_at" is null');
|
|
384
|
+
expect(keys!.sql).toContain('"api_tokens"."last_used_at" is not null');
|
|
385
|
+
expect(keys!.params).toEqual([ALICE.id]);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('keeps the most recently used row of each kind, of however many a person has', async () => {
|
|
389
|
+
// The fake neither sorts nor limits, so what is pinned is what each read
|
|
390
|
+
// asks the database for: newest use first, one row.
|
|
391
|
+
const { service, captured } = makeService([[], []]);
|
|
392
|
+
await service.lastAgentUse(ALICE.id);
|
|
393
|
+
expect(captured.orderBy.map((clauses) => clauses.map((c) => render(c).sql))).toEqual([
|
|
394
|
+
['"agent_connections"."last_used_at" desc'],
|
|
395
|
+
['"api_tokens"."last_used_at" desc'],
|
|
396
|
+
]);
|
|
397
|
+
expect(captured.limit).toEqual([1, 1]);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("names an agent connection by its client, and an unnamed one as such", async () => {
|
|
401
|
+
const at = new Date(NOW - 60_000);
|
|
402
|
+
const named = makeService([[{ client: 'Claude', at }], []]);
|
|
403
|
+
await expect(named.service.lastAgentUse(ALICE.id)).resolves.toEqual({ at, client: 'Claude', kind: 'agent' });
|
|
404
|
+
const unnamed = makeService([[{ client: null, at }], []]);
|
|
405
|
+
await expect(unnamed.service.lastAgentUse(ALICE.id)).resolves.toEqual({ at, client: 'Unnamed agent', kind: 'agent' });
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('answers with the newer of the two when both kinds have been used', async () => {
|
|
409
|
+
const older = new Date(NOW - DAY);
|
|
410
|
+
const newer = new Date(NOW - 60_000);
|
|
411
|
+
const keyNewer = makeService([[{ client: 'Claude', at: older }], [{ client: 'Laptop', at: newer }]]);
|
|
412
|
+
await expect(keyNewer.service.lastAgentUse(ALICE.id)).resolves.toEqual({ at: newer, client: 'Laptop', kind: 'key' });
|
|
413
|
+
const agentNewer = makeService([[{ client: 'Claude', at: newer }], [{ client: 'Laptop', at: older }]]);
|
|
414
|
+
await expect(agentNewer.service.lastAgentUse(ALICE.id)).resolves.toEqual({ at: newer, client: 'Claude', kind: 'agent' });
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
358
418
|
describe('retentionDaysFrom', () => {
|
|
359
419
|
it('reads a positive whole number of days, and blank, zero or negative as forever', () => {
|
|
360
420
|
expect(retentionDaysFrom('30')).toBe(30);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import type { Server } from 'node:http';
|
|
4
|
+
import { createAgentConnectionRoutes } from '../agent-connection.routes.js';
|
|
5
|
+
import type { AgentUse, IAgentConnectionStatus } from '../audit.contract.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /onboarding/agent-connection — what the connect-your-agent page polls.
|
|
9
|
+
* The contract worth pinning:
|
|
10
|
+
*
|
|
11
|
+
* - it answers for the AUTHENTICATED user (req.userId) and nobody else;
|
|
12
|
+
* nothing in the request — query or otherwise — can name another account;
|
|
13
|
+
* - "not yet" and "connected" are the two shapes, the second with when, as
|
|
14
|
+
* what, and whether that name is an agent's or a connection key's label;
|
|
15
|
+
* - the answer is never cached, since the next poll expects it to change;
|
|
16
|
+
* - a failure is a generic 500, never the driver's message.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const ALICE = 'u-alice';
|
|
20
|
+
const BOB = 'u-bob';
|
|
21
|
+
const CARA = 'u-cara';
|
|
22
|
+
|
|
23
|
+
const USES: Record<string, AgentUse> = {
|
|
24
|
+
[BOB]: { at: new Date(Date.UTC(2026, 9, 7, 12, 0, 0)), client: 'Claude', kind: 'agent' },
|
|
25
|
+
[CARA]: { at: new Date(Date.UTC(2026, 9, 7, 13, 0, 0)), client: 'Laptop', kind: 'key' },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const status = {
|
|
29
|
+
lastAgentUse: vi.fn(async (userId: string) => USES[userId] ?? null),
|
|
30
|
+
} satisfies IAgentConnectionStatus;
|
|
31
|
+
|
|
32
|
+
let server: Server | undefined;
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
server?.close();
|
|
35
|
+
server = undefined;
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** Mount the route with a stand-in auth middleware stamping `as` as the caller. */
|
|
41
|
+
async function listen(as: string): Promise<string> {
|
|
42
|
+
const app = express();
|
|
43
|
+
app.use((req, _res, next) => {
|
|
44
|
+
req.userId = as;
|
|
45
|
+
next();
|
|
46
|
+
});
|
|
47
|
+
app.use('/api', createAgentConnectionRoutes(status));
|
|
48
|
+
await new Promise<void>((resolve) => {
|
|
49
|
+
server = app.listen(0, resolve);
|
|
50
|
+
});
|
|
51
|
+
const address = server!.address();
|
|
52
|
+
if (typeof address === 'string' || !address) throw new Error('no port');
|
|
53
|
+
return `http://127.0.0.1:${address.port}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('GET /onboarding/agent-connection', () => {
|
|
57
|
+
it('says "not yet" for a caller whose agents have not been used', async () => {
|
|
58
|
+
const base = await listen(ALICE);
|
|
59
|
+
const res = await fetch(`${base}/api/onboarding/agent-connection`);
|
|
60
|
+
expect(res.status).toBe(200);
|
|
61
|
+
expect(await res.json()).toEqual({ connected: false });
|
|
62
|
+
expect(status.lastAgentUse).toHaveBeenCalledWith(ALICE);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('says connected, with when and as what, once one has', async () => {
|
|
66
|
+
const base = await listen(BOB);
|
|
67
|
+
const res = await fetch(`${base}/api/onboarding/agent-connection`);
|
|
68
|
+
expect(await res.json()).toEqual({ connected: true, at: '2026-10-07T12:00:00.000Z', client: 'Claude', kind: 'agent' });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("keeps a connection key's kind: its label says nothing about which app holds it", async () => {
|
|
72
|
+
const base = await listen(CARA);
|
|
73
|
+
const res = await fetch(`${base}/api/onboarding/agent-connection`);
|
|
74
|
+
expect(await res.json()).toEqual({ connected: true, at: '2026-10-07T13:00:00.000Z', client: 'Laptop', kind: 'key' });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("answers for the caller alone — a named account in the query is not asked about", async () => {
|
|
78
|
+
const base = await listen(ALICE);
|
|
79
|
+
const res = await fetch(`${base}/api/onboarding/agent-connection?userId=${BOB}&user=${BOB}`);
|
|
80
|
+
expect(await res.json()).toEqual({ connected: false });
|
|
81
|
+
expect(status.lastAgentUse).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(status.lastAgentUse).toHaveBeenCalledWith(ALICE);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('is never cached: the next poll expects a different answer', async () => {
|
|
86
|
+
const base = await listen(ALICE);
|
|
87
|
+
const res = await fetch(`${base}/api/onboarding/agent-connection`);
|
|
88
|
+
expect(res.headers.get('cache-control')).toBe('no-store');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('reports a failed read as a generic 500, keeping the detail server-side', async () => {
|
|
92
|
+
const detail = 'connection to 10.0.0.4:5432 refused';
|
|
93
|
+
status.lastAgentUse.mockRejectedValueOnce(new Error(detail));
|
|
94
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
95
|
+
const base = await listen(ALICE);
|
|
96
|
+
const res = await fetch(`${base}/api/onboarding/agent-connection`);
|
|
97
|
+
expect(res.status).toBe(500);
|
|
98
|
+
const body = (await res.json()) as { error: string };
|
|
99
|
+
expect(body.error).toBe('Could not check your agent');
|
|
100
|
+
expect(body.error).not.toContain(detail);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { and, count, desc, eq, isNull, lt, or, type SQL } from 'drizzle-orm';
|
|
1
|
+
import { and, count, desc, eq, isNotNull, isNull, lt, or, type SQL } from 'drizzle-orm';
|
|
2
2
|
import { logger } from '../../shared/logging.js';
|
|
3
3
|
import type { Database } from '../database/connection.js';
|
|
4
|
-
import { agentConnections, agentEvents, oauthTokens, users } from '../database/schema.js';
|
|
4
|
+
import { agentConnections, agentEvents, externalApiKeys, oauthTokens, users } from '../database/schema.js';
|
|
5
5
|
import type { IExternalApiKeyService } from '../tool-auth/external-api-key.interface.js';
|
|
6
6
|
import {
|
|
7
7
|
AuditPrincipalNotFoundError,
|
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
type AgentEventInput,
|
|
12
12
|
type AgentEventPage,
|
|
13
13
|
type AgentEventView,
|
|
14
|
+
type AgentUse,
|
|
14
15
|
type AuditPrincipal,
|
|
15
16
|
type AuditPrincipalRef,
|
|
16
17
|
type AuditScope,
|
|
17
18
|
type IAgentAuditService,
|
|
19
|
+
type IAgentConnectionStatus,
|
|
18
20
|
type IAgentEventRecorder,
|
|
19
21
|
} from './audit.contract.js';
|
|
20
22
|
|
|
@@ -39,7 +41,7 @@ const PRUNE_INTERVAL_MS = 60 * 60_000;
|
|
|
39
41
|
* Connection keys are read through the key service rather than joined here,
|
|
40
42
|
* so their summary shape (kind, revoked-by, …) has exactly one definition.
|
|
41
43
|
*/
|
|
42
|
-
export class AgentAuditService implements IAgentAuditService, IAgentEventRecorder {
|
|
44
|
+
export class AgentAuditService implements IAgentAuditService, IAgentEventRecorder, IAgentConnectionStatus {
|
|
43
45
|
private lastPruneAt = 0;
|
|
44
46
|
|
|
45
47
|
constructor(
|
|
@@ -242,6 +244,59 @@ export class AgentAuditService implements IAgentAuditService, IAgentEventRecorde
|
|
|
242
244
|
};
|
|
243
245
|
}
|
|
244
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Whether one person's agent has reached the platform yet — what the
|
|
249
|
+
* onboarding asks once on arrival (the `agent-connected` event the stamp
|
|
250
|
+
* paths emit tells it the moment it changes). Two single-row reads on the
|
|
251
|
+
* per-user indexes, never a scan of the event log.
|
|
252
|
+
*
|
|
253
|
+
* Read from the `last_used_at` stamps, not from `agent_events`: an agent
|
|
254
|
+
* stamps its connection (or key) on its FIRST authenticated request — the
|
|
255
|
+
* `initialize` / `tools/list` every client sends straight after connecting
|
|
256
|
+
* — while an event is written only once it calls a tool, which may be
|
|
257
|
+
* never. The stamps are also outside the retention sweep, so "connected"
|
|
258
|
+
* cannot quietly turn back into "waiting" when old events are pruned.
|
|
259
|
+
*
|
|
260
|
+
* Live rows only: an agent its owner disconnected, or an admin revoked, is
|
|
261
|
+
* not a connected agent, whatever it did before. The key's columns are read
|
|
262
|
+
* here rather than through the key service because the service has no
|
|
263
|
+
* single-row read and this question needs only two columns of one row.
|
|
264
|
+
*/
|
|
265
|
+
async lastAgentUse(userId: string): Promise<AgentUse | null> {
|
|
266
|
+
const [[agent], [key]] = await Promise.all([
|
|
267
|
+
this.db
|
|
268
|
+
.select({ client: agentConnections.clientName, at: agentConnections.lastUsedAt })
|
|
269
|
+
.from(agentConnections)
|
|
270
|
+
.where(
|
|
271
|
+
and(
|
|
272
|
+
eq(agentConnections.userId, userId),
|
|
273
|
+
isNull(agentConnections.revokedAt),
|
|
274
|
+
isNotNull(agentConnections.lastUsedAt),
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
.orderBy(desc(agentConnections.lastUsedAt))
|
|
278
|
+
.limit(1),
|
|
279
|
+
this.db
|
|
280
|
+
.select({ client: externalApiKeys.label, at: externalApiKeys.lastUsedAt })
|
|
281
|
+
.from(externalApiKeys)
|
|
282
|
+
.where(
|
|
283
|
+
and(
|
|
284
|
+
eq(externalApiKeys.userId, userId),
|
|
285
|
+
isNull(externalApiKeys.revokedAt),
|
|
286
|
+
isNotNull(externalApiKeys.lastUsedAt),
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
.orderBy(desc(externalApiKeys.lastUsedAt))
|
|
290
|
+
.limit(1),
|
|
291
|
+
]);
|
|
292
|
+
const uses = [
|
|
293
|
+
agent?.at ? { at: agent.at, client: agent.client ?? 'Unnamed agent', kind: 'agent' as const } : null,
|
|
294
|
+
key?.at ? { at: key.at, client: key.client, kind: 'key' as const } : null,
|
|
295
|
+
].filter((u): u is AgentUse => u !== null);
|
|
296
|
+
if (uses.length === 0) return null;
|
|
297
|
+
return uses.reduce((newest, u) => (u.at.getTime() > newest.at.getTime() ? u : newest));
|
|
298
|
+
}
|
|
299
|
+
|
|
245
300
|
async revokeConnection(id: string, by: 'owner' | 'admin', ownerUserId?: string): Promise<void> {
|
|
246
301
|
const scope = ownerUserId
|
|
247
302
|
? and(eq(agentConnections.id, id), eq(agentConnections.userId, ownerUserId))!
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { logger } from '../../shared/logging.js';
|
|
3
|
+
import type { IAgentConnectionStatus } from './audit.contract.js';
|
|
4
|
+
import '../auth/auth.middleware.js'; // Express Request augmentation (req.userId)
|
|
5
|
+
|
|
6
|
+
const log = logger('audit');
|
|
7
|
+
|
|
8
|
+
/** What the onboarding reads: connected or not, and — once connected — when, as what, and through which kind of credential. */
|
|
9
|
+
export interface AgentConnectionResponse {
|
|
10
|
+
connected: boolean;
|
|
11
|
+
/** The agent's most recent call, ISO 8601. */
|
|
12
|
+
at?: string;
|
|
13
|
+
/** The agent's registered client name, or the connection key's label. */
|
|
14
|
+
client?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Which of the two `client` is: `agent` for an OAuth connection's
|
|
17
|
+
* registered name, `key` for a connection key's free-text label. Only an
|
|
18
|
+
* `agent` name says which app connected.
|
|
19
|
+
*/
|
|
20
|
+
kind?: 'agent' | 'key';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* GET /api/onboarding/agent-connection — "has an agent of mine reached the
|
|
25
|
+
* platform yet?", JWT-only (mounted behind the session middleware).
|
|
26
|
+
*
|
|
27
|
+
* The connect-your-agent page and the Get set up list ask it once on
|
|
28
|
+
* arrival, and again when the person comes back to the tab; the moment it
|
|
29
|
+
* changes is told by the `agent-connected` event the use-stamp paths emit
|
|
30
|
+
* over the event stream, so nothing asks on a timer. It answers for the
|
|
31
|
+
* CALLER alone: the user is the token's, and nothing in the request can name
|
|
32
|
+
* anyone else — there is no parameter to try.
|
|
33
|
+
*
|
|
34
|
+
* `no-store` because the answer is expected to change between two asks; a
|
|
35
|
+
* cached "not yet" would keep a page waiting on an agent that has already
|
|
36
|
+
* arrived.
|
|
37
|
+
*/
|
|
38
|
+
export function createAgentConnectionRoutes(status: IAgentConnectionStatus): express.Router {
|
|
39
|
+
const router = express.Router();
|
|
40
|
+
|
|
41
|
+
router.get('/onboarding/agent-connection', async (req, res) => {
|
|
42
|
+
res.set('Cache-Control', 'no-store');
|
|
43
|
+
try {
|
|
44
|
+
const use = await status.lastAgentUse(req.userId!);
|
|
45
|
+
const body: AgentConnectionResponse = use
|
|
46
|
+
? { connected: true, at: use.at.toISOString(), client: use.client, kind: use.kind }
|
|
47
|
+
: { connected: false };
|
|
48
|
+
res.json(body);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
log.error('agent connection status failed:', { err });
|
|
51
|
+
res.status(500).json({ error: 'Could not check your agent' });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return router;
|
|
56
|
+
}
|
|
@@ -121,6 +121,32 @@ export interface IAgentAuditService {
|
|
|
121
121
|
revokeConnection(id: string, by: 'owner' | 'admin', ownerUserId?: string): Promise<void>;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* The most recent use of any of one person's live agents: when, what it
|
|
126
|
+
* calls itself (the agent's registered client name, or the key's label), and
|
|
127
|
+
* which of the two that name is.
|
|
128
|
+
*/
|
|
129
|
+
export interface AgentUse {
|
|
130
|
+
at: Date;
|
|
131
|
+
client: string;
|
|
132
|
+
/**
|
|
133
|
+
* `agent`: an OAuth connection, and `client` is the name the agent
|
|
134
|
+
* registered. `key`: a connection key, and `client` is whatever label its
|
|
135
|
+
* owner typed — it says nothing about which app holds the key.
|
|
136
|
+
*/
|
|
137
|
+
kind: 'agent' | 'key';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The onboarding's "is your agent connected yet?" — answered for ONE person,
|
|
142
|
+
* from the use stamps the auth paths already write, so it costs two indexed
|
|
143
|
+
* single-row reads and records nothing new.
|
|
144
|
+
*/
|
|
145
|
+
export interface IAgentConnectionStatus {
|
|
146
|
+
/** The newest use among the person's live agent connections and connection keys; null when none has been used. */
|
|
147
|
+
lastAgentUse(userId: string): Promise<AgentUse | null>;
|
|
148
|
+
}
|
|
149
|
+
|
|
124
150
|
export class AuditPrincipalNotFoundError extends Error {
|
|
125
151
|
constructor() {
|
|
126
152
|
super('No such agent or key');
|
|
@@ -2,6 +2,7 @@ export * from './audit.contract.js';
|
|
|
2
2
|
export { AgentAuditService, MAX_EVENT_PAGE, retentionDaysFrom } from './agent-audit.service.js';
|
|
3
3
|
export { createAgentRestAuditMiddleware } from './agent-rest-audit.middleware.js';
|
|
4
4
|
export { createAuditRoutes } from './audit.routes.js';
|
|
5
|
+
export { createAgentConnectionRoutes, type AgentConnectionResponse } from './agent-connection.routes.js';
|
|
5
6
|
export { RequestAudit } from './request-audit.js';
|
|
6
7
|
export {
|
|
7
8
|
classifyToolCall,
|
|
@@ -67,6 +67,8 @@ describe('account routes — admin gate', () => {
|
|
|
67
67
|
body: JSON.stringify({ email: 'x@example.com', password: 'long-enough-pw' }),
|
|
68
68
|
});
|
|
69
69
|
expect(create.status).toBe(403);
|
|
70
|
+
// Not an admission refusal: nothing here may read as "no seat left".
|
|
71
|
+
expect(await create.json()).toEqual({ error: 'Admins only' });
|
|
70
72
|
expect(authService.createAccount).not.toHaveBeenCalled();
|
|
71
73
|
});
|
|
72
74
|
|
|
@@ -151,7 +153,7 @@ describe('account routes — admin gate', () => {
|
|
|
151
153
|
expect(accountErasure.eraseUser).toHaveBeenLastCalledWith('u2');
|
|
152
154
|
});
|
|
153
155
|
|
|
154
|
-
it('an account the admission port refuses → 403 carrying the port\'s own words', async () => {
|
|
156
|
+
it('an account the admission port refuses → 403 carrying the port\'s own words, marked as an admission refusal', async () => {
|
|
155
157
|
const base = await listen(makeApp({ admin: true }));
|
|
156
158
|
vi.mocked(authService.createAccount).mockRejectedValueOnce(
|
|
157
159
|
new AccountAdmissionRefusedError('No seat left on this plan'),
|
|
@@ -162,7 +164,7 @@ describe('account routes — admin gate', () => {
|
|
|
162
164
|
body: JSON.stringify({ email: 'b@example.com', password: 'long-enough-pw' }),
|
|
163
165
|
});
|
|
164
166
|
expect(refused.status).toBe(403);
|
|
165
|
-
expect(
|
|
167
|
+
expect(await refused.json()).toEqual({ error: 'No seat left on this plan', kind: 'admission' });
|
|
166
168
|
});
|
|
167
169
|
|
|
168
170
|
it('erasure failure → 500 with a generic body (no internal error text)', async () => {
|
|
@@ -370,7 +372,7 @@ describe('account routes — switching an account off and on', () => {
|
|
|
370
372
|
const base = await listen(makeApp({ admin: true }));
|
|
371
373
|
const res = await post(base, 'u2/reactivate');
|
|
372
374
|
expect(res.status).toBe(403);
|
|
373
|
-
expect(
|
|
375
|
+
expect(await res.json()).toEqual({ error: 'All 3 seats are taken', kind: 'admission' });
|
|
374
376
|
});
|
|
375
377
|
|
|
376
378
|
it('answers a refused deactivation (the deployment admin, say) with 400 and the reason', async () => {
|
|
@@ -73,7 +73,13 @@ export function createAccountRoutes(
|
|
|
73
73
|
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
74
74
|
// The deployment has no place for the account (a seat limit, say): the
|
|
75
75
|
// port's own words, as a refusal rather than a malformed request.
|
|
76
|
-
|
|
76
|
+
// `kind` tells it apart from `requireAdmin`'s 403, which a caller must
|
|
77
|
+
// not read as "no seat left".
|
|
78
|
+
if (error instanceof AccountAdmissionRefusedError) {
|
|
79
|
+
res.status(403).json({ error: msg, kind: 'admission' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
res.status(400).json({ error: msg });
|
|
77
83
|
}
|
|
78
84
|
});
|
|
79
85
|
|
|
@@ -107,7 +113,8 @@ export function createAccountRoutes(
|
|
|
107
113
|
|
|
108
114
|
// POST /api/admin/accounts/:userId/reactivate — switch it back on, if the
|
|
109
115
|
// deployment has room: the admission port is asked, as for a new account
|
|
110
|
-
// (a host that sells seats answers from its plan; 403 with its words
|
|
116
|
+
// (a host that sells seats answers from its plan; 403 with its words and
|
|
117
|
+
// `kind: 'admission'`).
|
|
111
118
|
router.post('/admin/accounts/:userId/reactivate', requireAdmin, async (req, res) => {
|
|
112
119
|
const userId = String(req.params.userId);
|
|
113
120
|
try {
|
|
@@ -119,7 +126,7 @@ export function createAccountRoutes(
|
|
|
119
126
|
res.status(204).end();
|
|
120
127
|
} catch (error) {
|
|
121
128
|
if (error instanceof AccountAdmissionRefusedError) {
|
|
122
|
-
res.status(403).json({ error: error.message });
|
|
129
|
+
res.status(403).json({ error: error.message, kind: 'admission' });
|
|
123
130
|
return;
|
|
124
131
|
}
|
|
125
132
|
log.error('switching an account on failed:', { err: error });
|
|
@@ -30,7 +30,7 @@ export const AUTH_COOKIE_NAME = 'bevel_token';
|
|
|
30
30
|
* beats the dep surface of a middleware. Returns `null` if the cookie
|
|
31
31
|
* is absent or empty.
|
|
32
32
|
*/
|
|
33
|
-
function readAuthCookie(req: Request): string | null {
|
|
33
|
+
export function readAuthCookie(req: Request): string | null {
|
|
34
34
|
const header = req.headers.cookie;
|
|
35
35
|
if (!header) return null;
|
|
36
36
|
for (const segment of header.split(';')) {
|
|
@@ -27,6 +27,16 @@ vi.mock('../code-mode-names.js', async (importOriginal) => {
|
|
|
27
27
|
}
|
|
28
28
|
if (name === 'down.run') throw new Error('repository unavailable: ECONNREFUSED');
|
|
29
29
|
if (name === 'solo.run') return { tool: { name: 'solo.run' }, utcpName: 'solo.run' };
|
|
30
|
+
if (name === 'kb.read_file') {
|
|
31
|
+
return {
|
|
32
|
+
tool: {
|
|
33
|
+
name: 'kb.read_file',
|
|
34
|
+
description: 'Read a file.',
|
|
35
|
+
inputs: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
|
|
36
|
+
},
|
|
37
|
+
utcpName: 'kb.read_file',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
30
40
|
return null;
|
|
31
41
|
}),
|
|
32
42
|
};
|
|
@@ -35,7 +45,8 @@ vi.mock('../code-mode-names.js', async (importOriginal) => {
|
|
|
35
45
|
const { createToolsInfoTool } = await import('../code-mode.tool.js');
|
|
36
46
|
|
|
37
47
|
const client = {
|
|
38
|
-
toolToTypeScriptInterface: (tool: { name: string }) =>
|
|
48
|
+
toolToTypeScriptInterface: (tool: { name: string; description?: string }) =>
|
|
49
|
+
tool.description ? `/** ${tool.description} */ interface ${tool.name}` : `interface ${tool.name}`,
|
|
39
50
|
} as unknown as CodeModeUtcpClient;
|
|
40
51
|
|
|
41
52
|
type ToolsInfoResult = { interfaces: string; not_found: string[]; errors?: string[] };
|
|
@@ -58,11 +69,16 @@ describe('tools_info', () => {
|
|
|
58
69
|
|
|
59
70
|
it('omits the errors field entirely when every name resolves or misses cleanly', async () => {
|
|
60
71
|
const result = await run(['solo.run', 'nope']);
|
|
61
|
-
expect(result.interfaces).toBe('interface solo.run');
|
|
72
|
+
expect(result.interfaces).toBe('/** Call: solo.run({}) */ interface solo.run');
|
|
62
73
|
expect(result.not_found).toEqual(['nope']);
|
|
63
74
|
expect(result.errors).toBeUndefined();
|
|
64
75
|
});
|
|
65
76
|
|
|
77
|
+
it('shows each tool with its Call: line, as the chain description promises', async () => {
|
|
78
|
+
const result = await run(['kb.read_file']);
|
|
79
|
+
expect(result.interfaces).toBe('/** Call: kb.read_file({ query: "..." })\n\nRead a file. */ interface kb.read_file');
|
|
80
|
+
});
|
|
81
|
+
|
|
66
82
|
it('contains ONLY ambiguity — an outage rethrows instead of posing as partial success', async () => {
|
|
67
83
|
await expect(run(['solo.run', 'down.run'])).rejects.toThrow(/repository unavailable/);
|
|
68
84
|
});
|