@bevel-software/platform-core-backend 0.25.2 → 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 +251 -0
- package/agent-guide/conventions.md +35 -0
- package/agent-guide/directory-structure.md +145 -0
- package/agent-guide/finding-things.md +7 -0
- package/agent-guide/html-views.md +83 -0
- package/agent-guide/introduction.md +27 -0
- package/agent-guide/new-knowledge-base.md +24 -0
- package/agent-guide/skills.md +47 -0
- package/agent-guide/tool-manuals.md +227 -0
- package/agent-guide/where-a-new-file-goes.md +36 -0
- package/dist/assets.d.ts +26 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +35 -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/core-ports.d.ts +11 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.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 +65 -3
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +35 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +114 -12
- 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 +17 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +2 -0
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +27 -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 +145 -0
- package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -0
- package/dist/modules/agent-guide/agent-guide.js +199 -0
- package/dist/modules/agent-guide/agent-guide.js.map +1 -0
- package/dist/modules/agent-guide/agent-guide.tools.d.ts +24 -0
- package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -0
- package/dist/modules/agent-guide/agent-guide.tools.js +100 -0
- package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -0
- package/dist/modules/agent-guide/index.d.ts +4 -0
- package/dist/modules/agent-guide/index.d.ts.map +1 -0
- package/dist/modules/agent-guide/index.js +4 -0
- package/dist/modules/agent-guide/index.js.map +1 -0
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +3 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.js +3 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +14 -6
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +28 -8
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/index.d.ts +1 -1
- package/dist/modules/agent-instructions/index.d.ts.map +1 -1
- package/dist/modules/agent-instructions/index.js +1 -1
- package/dist/modules/agent-instructions/index.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +10 -50
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +51 -104
- 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 +57 -3
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +177 -24
- 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/mcp/tool-schema-guard.d.ts +105 -0
- package/dist/modules/mcp/tool-schema-guard.d.ts.map +1 -0
- package/dist/modules/mcp/tool-schema-guard.js +171 -0
- package/dist/modules/mcp/tool-schema-guard.js.map +1 -0
- 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/plugins/plugins.tools.d.ts +36 -2
- package/dist/modules/plugins/plugins.tools.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.tools.js +71 -14
- package/dist/modules/plugins/plugins.tools.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +34 -7
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +148 -54
- 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/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +3 -6
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +103 -43
- 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.contract.d.ts +23 -4
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +4 -0
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +14 -0
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +7 -0
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +77 -38
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +20 -16
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
- package/dist/modules/tool-registry/description-length.js +50 -30
- package/dist/modules/tool-registry/description-length.js.map +1 -1
- package/dist/modules/tool-registry/guide-first.d.ts +27 -0
- package/dist/modules/tool-registry/guide-first.d.ts.map +1 -0
- package/dist/modules/tool-registry/guide-first.js +40 -0
- package/dist/modules/tool-registry/guide-first.js.map +1 -0
- package/dist/modules/tool-registry/tool-registry.d.ts +6 -0
- package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -1
- package/dist/modules/tool-registry/tool-registry.js +9 -2
- package/dist/modules/tool-registry/tool-registry.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts +449 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.js +481 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +126 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js +611 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +12 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.js +5 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.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 +133 -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 +282 -14
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +627 -126
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/merge-commit.d.ts +73 -0
- package/dist/modules/workflow/git/merge-commit.d.ts.map +1 -0
- package/dist/modules/workflow/git/merge-commit.js +89 -0
- package/dist/modules/workflow/git/merge-commit.js.map +1 -0
- 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/git/pull-request.service.d.ts +94 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +332 -37
- package/dist/modules/workflow/git/pull-request.service.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 +35 -0
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +180 -13
- 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.routes.d.ts +6 -2
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +7 -2
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +187 -15
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +808 -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 +24 -39
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts +58 -52
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +209 -223
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.d.ts +5 -3
- package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.js +5 -3
- package/dist/modules/workspace/startup/steps/template-source.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 +68 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +642 -71
- 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 +84 -1
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +100 -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/hidden-tools.d.ts +44 -0
- package/dist/shared/hidden-tools.d.ts.map +1 -0
- package/dist/shared/hidden-tools.js +13 -0
- package/dist/shared/hidden-tools.js.map +1 -0
- 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/kb-template/.bevelignore +0 -5
- 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 +6 -3
- package/src/__tests__/kb-layout-config.test.ts +10 -100
- package/src/__tests__/packaged-assets-ship.test.ts +66 -0
- package/src/assets.ts +38 -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/core-ports.ts +11 -0
- package/src/core/create-core-server.ts +78 -3
- package/src/core/create-core-services.ts +168 -10
- 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.atref-batch.test.ts +58 -0
- package/src/modules/access/__tests__/access-control.platform-restore.test.ts +8 -7
- package/src/modules/access/__tests__/access-control.service.test.ts +19 -0
- package/src/modules/access/__tests__/access-personal-plugin.test.ts +1 -18
- package/src/modules/access/access-control.interface.ts +28 -0
- package/src/modules/access/access-control.service.ts +37 -0
- package/src/modules/access/directory-sync-bot.ts +1 -1
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +386 -0
- package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +207 -0
- package/src/modules/agent-guide/agent-guide.tools.ts +122 -0
- package/src/modules/agent-guide/agent-guide.ts +300 -0
- package/src/modules/agent-guide/index.ts +22 -0
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +39 -118
- package/src/modules/agent-instructions/agent-instructions.routes.ts +3 -2
- package/src/modules/agent-instructions/compose.ts +26 -8
- package/src/modules/agent-instructions/index.ts +0 -3
- package/src/modules/agent-instructions/shared-file-rules.ts +50 -112
- 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__/fake-downstream-mcp-server.ts +14 -3
- 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 +252 -0
- package/src/modules/mcp/__tests__/mcp.service.test.ts +42 -24
- package/src/modules/mcp/__tests__/tool-call-interface.e2e.test.ts +499 -0
- package/src/modules/mcp/__tests__/tool-schema-guard.test.ts +266 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +2 -1
- package/src/modules/mcp/mcp.service.ts +218 -31
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +42 -4
- package/src/modules/mcp/tool-schema-guard.ts +196 -0
- 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 +177 -4
- 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/plugins/plugins.tools.ts +75 -15
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +176 -56
- package/src/modules/settings/__tests__/repository-source.test.ts +9 -0
- package/src/modules/settings/deployment-settings.service.ts +144 -55
- package/src/modules/settings/repository-source.ts +4 -4
- package/src/modules/settings/setup.routes.ts +3 -6
- package/src/modules/skills/__tests__/skills.tools.description.test.ts +95 -0
- package/src/modules/skills/skills.tools.ts +112 -46
- 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/__tests__/tool-manuals.detail.route.test.ts +57 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +73 -4
- package/src/modules/tool-manuals/tool-manuals.contract.ts +24 -4
- package/src/modules/tool-manuals/tool-manuals.service.ts +17 -0
- package/src/modules/tool-manuals/tool-manuals.tools.ts +86 -38
- package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +173 -0
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +81 -66
- package/src/modules/tool-registry/description-length.ts +57 -32
- package/src/modules/tool-registry/guide-first.ts +41 -0
- package/src/modules/tool-registry/tool-registry.ts +9 -2
- package/src/modules/workflow/__tests__/apply-failure.test.ts +6 -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-shape.test.ts +705 -0
- package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +1554 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +150 -4
- package/src/modules/workflow/agent-tools/change-request-read-shape.ts +712 -0
- package/src/modules/workflow/agent-tools/change-request-read.tools.ts +759 -0
- package/src/modules/workflow/agent-tools/change-request-summary.ts +5 -1
- package/src/modules/workflow/agent-tools/workflow.tools.ts +137 -35
- package/src/modules/workflow/file-lock.service.ts +67 -5
- package/src/modules/workflow/git/__tests__/git.service.appliedChange.test.ts +285 -0
- package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +164 -0
- package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +334 -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/__tests__/pull-request.service.list-fetch.test.ts +72 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +24 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +620 -1
- package/src/modules/workflow/git/git.service.ts +714 -133
- package/src/modules/workflow/git/merge-commit.ts +88 -0
- package/src/modules/workflow/git/node-git-runner.ts +82 -7
- package/src/modules/workflow/git/pull-request.service.ts +380 -54
- 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/__tests__/approval-states.test.ts +7 -1
- package/src/modules/workflow/review-workflow/__tests__/merge-records-own-commit.test.ts +407 -0
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +191 -12
- package/src/modules/workflow/sanitize-error.ts +50 -0
- package/src/modules/workflow/workflow.routes.ts +7 -2
- package/src/modules/workflow/workflow.service.ts +886 -112
- package/src/modules/workspace/__tests__/agent-downloads.test.ts +823 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +4 -3
- 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.routes.move-platform-files.test.ts +21 -10
- 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.agents-file.test.ts +33 -55
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +44 -1
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +664 -34
- 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/__tests__/steps.test.ts +191 -489
- package/src/modules/workspace/startup/steps/seed-tree.ts +23 -38
- package/src/modules/workspace/startup/steps/template-files.step.ts +217 -249
- package/src/modules/workspace/startup/steps/template-source.ts +5 -3
- 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 +707 -68
- 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 +123 -11
- package/src/shared/git.contract.ts +8 -1
- package/src/shared/hidden-tools.ts +45 -0
- 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
- package/kb-template/AGENTS.md +0 -730
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
import type { Server as HttpServer } from 'node:http';
|
|
2
|
+
import type { AddressInfo } from 'node:net';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { mkdir, mkdtemp, readdir, rm, truncate, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import AdmZip from 'adm-zip';
|
|
8
|
+
import express from 'express';
|
|
9
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { LocalFilesystem } from '@mastra/core/workspace';
|
|
11
|
+
import { DEFAULT_KB_LAYOUT } from '@bevel-software/platform-shared';
|
|
12
|
+
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
13
|
+
import { NodeFs } from '../../kb-fs/node-fs.js';
|
|
14
|
+
import { AccessControlService } from '../../access/access-control.service.js';
|
|
15
|
+
import { ToolRegistry } from '../../tool-registry/tool-registry.js';
|
|
16
|
+
import { createToolHandlerFactory, type ToolBranchPort } from '../../tool-helpers/tool-handler.js';
|
|
17
|
+
import { ToolError, type ToolContext } from '../../tool-helpers/tool.contract.js';
|
|
18
|
+
import type { ToolAuth } from '../../tool-auth/tool-auth.middleware.js';
|
|
19
|
+
import { registerWorkspaceTools } from '../workspace.tools.js';
|
|
20
|
+
import { RoutineWritePolicyService } from '../routine-write-policy.js';
|
|
21
|
+
import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
|
|
22
|
+
import { ToolDescriptionNotes } from '../agent-access.gate.js';
|
|
23
|
+
import { SpillStore } from '../spill-store.js';
|
|
24
|
+
import { DocExtractService } from '../file-readers/doc-extract.service.js';
|
|
25
|
+
import { WorkspaceService } from '../workspace.service.js';
|
|
26
|
+
import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
|
|
27
|
+
import { AgentDownloadStore, DOWNLOAD_TOKEN_REFUSAL } from '../agent-download.store.js';
|
|
28
|
+
import { attachmentDisposition, createAgentDownloadRoutes, createDownloadFetcherIdentifier } from '../agent-download.routes.js';
|
|
29
|
+
import { readAuthCookie } from '../../auth/auth.middleware.js';
|
|
30
|
+
import jwt from 'jsonwebtoken';
|
|
31
|
+
import { buildDownload, DOWNLOAD_PERMISSION_REQUIRED, NOT_FOUND } from '../agent-download.builder.js';
|
|
32
|
+
import { READ_ONLY_CODE, type IWriteAccess } from '../../write-access/write-access.js';
|
|
33
|
+
import { sharedFileRules } from '../../agent-instructions/shared-file-rules.js';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `request_file_download` end to end, over the REAL access resolver on a
|
|
37
|
+
* temporary knowledge base: every file judged on its own, a link per file and
|
|
38
|
+
* a zip per folder, each link answering once.
|
|
39
|
+
*
|
|
40
|
+
* The four files under `Shared/` are the cases verified leaking from the app's
|
|
41
|
+
* folder zip (2026-10-07): a file denying `download`, one denying `read`, a
|
|
42
|
+
* nested `access.md` denying both, and a plain file. Ana holds `download` on
|
|
43
|
+
* the folder; only the plain file (and the grant file itself) may go out.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
const KB = 'knowledge-base';
|
|
47
|
+
const BRANCH = 'draft';
|
|
48
|
+
const PUBLIC_BASE = 'https://kb.example.com';
|
|
49
|
+
const ANA = { id: 'user-ana', email: 'ana@x.io', name: 'Ana' };
|
|
50
|
+
const JWT_SECRET = 'test-secret';
|
|
51
|
+
/** An app session, signed as `/auth/login` signs one. */
|
|
52
|
+
const sessionOf = (userId: string): string => jwt.sign({ userId, email: `${userId}@x.io` }, JWT_SECRET, { expiresIn: '1h' });
|
|
53
|
+
|
|
54
|
+
/** Not a real PNG, but bytes no text reader would take for text. */
|
|
55
|
+
const PNG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0xff, 0x10, 0x80, 0x00, 0x01]);
|
|
56
|
+
|
|
57
|
+
const FILES: Record<string, string | Buffer> = {
|
|
58
|
+
'roles.yaml': 'roles:\n Admin:\n - admin@x.io\n',
|
|
59
|
+
'Shared/access.md': '---\n---\ndownload:\n - Ana <ana@x.io>\n',
|
|
60
|
+
'Shared/Open.md': '# open\n',
|
|
61
|
+
'Shared/Node-Deny-Download.md': '---\ndownload:\n - deny Ana <ana@x.io>\n---\n# no save\n',
|
|
62
|
+
'Shared/Node-Deny-Read.md': '---\nread:\n - deny Ana <ana@x.io>\n---\n# hidden\n',
|
|
63
|
+
'Shared/Inner/access.md': '---\n---\nread:\n - deny Ana <ana@x.io>\ndownload:\n - deny Ana <ana@x.io>\n',
|
|
64
|
+
'Shared/Inner/Plan.md': '# hidden plan\n',
|
|
65
|
+
// Ana may read here, and download nothing.
|
|
66
|
+
'Readable/access.md': '---\n---\nread:\n - Ana <ana@x.io>\n',
|
|
67
|
+
'Readable/Top.md': '# top\n',
|
|
68
|
+
// A folder Ana may read (the rule above) whose one file she may not: the explorer keeps it, empty.
|
|
69
|
+
'Readable/Deep/Secret.md': '---\nread:\n - deny Ana <ana@x.io>\n---\n# deep secret\n',
|
|
70
|
+
// A folder whose rules deny Ana `download`, holding a file whose own
|
|
71
|
+
// frontmatter grants it: the file alone may go out, the folder's zip not.
|
|
72
|
+
// A folder nobody granted Ana (read is default-deny, and no rule reaches it) whose one
|
|
73
|
+
// file she may not read either: the explorer drops it, so for her it is not there.
|
|
74
|
+
'Veiled/Secret.md': '---\nread:\n - deny Ana <ana@x.io>\n---\n# secret\n',
|
|
75
|
+
'Gated/access.md': '---\n---\nread:\n - Ana <ana@x.io>\ndownload:\n - deny Ana <ana@x.io>\n',
|
|
76
|
+
'Gated/Open.md': '---\ndownload:\n - Ana <ana@x.io>\n---\n# open inside a gated folder\n',
|
|
77
|
+
'Pictures/access.md': '---\n---\ndownload:\n - Ana <ana@x.io>\n',
|
|
78
|
+
'Pictures/logo.png': PNG,
|
|
79
|
+
'Pictures/sub/.gitkeep': '',
|
|
80
|
+
'Pictures/.bevelignore': 'Ignored.md\n',
|
|
81
|
+
'Pictures/Ignored.md': '# ignored\n',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const sha = (b: Buffer | string): string => createHash('sha256').update(b).digest('hex');
|
|
85
|
+
|
|
86
|
+
interface Started {
|
|
87
|
+
base: string;
|
|
88
|
+
repo: string;
|
|
89
|
+
downloadsRoot: string;
|
|
90
|
+
store: AgentDownloadStore;
|
|
91
|
+
readsHooked: string[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface StartOptions {
|
|
95
|
+
files?: Record<string, string | Buffer>;
|
|
96
|
+
scope?: 'read' | 'write';
|
|
97
|
+
writeAccess?: IWriteAccess;
|
|
98
|
+
ttlMs?: number;
|
|
99
|
+
maxOpenPerUser?: number;
|
|
100
|
+
/** Paths the deployment's read hook refuses. */
|
|
101
|
+
hookRefuses?: (wsPath: string) => boolean;
|
|
102
|
+
/** The branch port the composition root wires; absent, no existence check. */
|
|
103
|
+
branches?: ToolBranchPort;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let servers: HttpServer[] = [];
|
|
107
|
+
let dirs: string[] = [];
|
|
108
|
+
let stores: AgentDownloadStore[] = [];
|
|
109
|
+
|
|
110
|
+
afterEach(async () => {
|
|
111
|
+
for (const s of servers) await new Promise<void>((r) => s.close(() => r()));
|
|
112
|
+
servers = [];
|
|
113
|
+
// Before their roots go: a store left sweeping would keep sweeping a deleted root.
|
|
114
|
+
for (const store of stores) {
|
|
115
|
+
store.stopSweeping();
|
|
116
|
+
await store.drainSweep();
|
|
117
|
+
}
|
|
118
|
+
stores = [];
|
|
119
|
+
for (const d of dirs) await rm(d, { recursive: true, force: true });
|
|
120
|
+
dirs = [];
|
|
121
|
+
vi.useRealTimers();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
async function start(options: StartOptions = {}): Promise<Started> {
|
|
125
|
+
const root = await mkdtemp(join(tmpdir(), 'agent-dl-ws-'));
|
|
126
|
+
const downloadsRoot = await mkdtemp(join(tmpdir(), 'agent-dl-store-'));
|
|
127
|
+
const docCache = await mkdtemp(join(tmpdir(), 'agent-dl-doc-'));
|
|
128
|
+
dirs.push(root, downloadsRoot, docCache);
|
|
129
|
+
const workspaceDir = join(root, workspaceIdForBranch(BRANCH));
|
|
130
|
+
const repo = join(workspaceDir, KB);
|
|
131
|
+
// A `.git` folder is what makes the service adopt the clone without cloning.
|
|
132
|
+
await mkdir(join(repo, '.git'), { recursive: true });
|
|
133
|
+
await writeFile(join(repo, '.git', 'config'), '[core]\n');
|
|
134
|
+
for (const [rel, contents] of Object.entries(options.files ?? FILES)) {
|
|
135
|
+
const abs = join(repo, rel);
|
|
136
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
137
|
+
await writeFile(abs, contents);
|
|
138
|
+
}
|
|
139
|
+
const workspaceService = new WorkspaceService(
|
|
140
|
+
root,
|
|
141
|
+
'https://example.invalid/kb.git',
|
|
142
|
+
testKbContext({ kbDirName: KB }),
|
|
143
|
+
new NodeFs(),
|
|
144
|
+
);
|
|
145
|
+
const accessControl = new AccessControlService(workspaceService, KB, new NodeFs());
|
|
146
|
+
const store = new AgentDownloadStore({
|
|
147
|
+
root: downloadsRoot,
|
|
148
|
+
publicBaseUrl: PUBLIC_BASE,
|
|
149
|
+
tokenPrefix: 'bevel-down_',
|
|
150
|
+
...(options.ttlMs !== undefined ? { ttlMs: options.ttlMs } : {}),
|
|
151
|
+
...(options.maxOpenPerUser !== undefined ? { maxOpenPerUser: options.maxOpenPerUser } : {}),
|
|
152
|
+
});
|
|
153
|
+
stores.push(store);
|
|
154
|
+
const hooks = new WorkflowHooks();
|
|
155
|
+
const readsHooked: string[] = [];
|
|
156
|
+
hooks.onAgentRead(async (op) => {
|
|
157
|
+
readsHooked.push(op.wsPath ?? '');
|
|
158
|
+
if (op.wsPath && options.hookRefuses?.(op.wsPath)) {
|
|
159
|
+
throw new ToolError(`This session may not read ${op.wsPath}.`, 403);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
const fs = new LocalFilesystem({ basePath: workspaceDir, contained: true });
|
|
163
|
+
const resolve = async (auth: ToolAuth, signal: AbortSignal, sessionId?: string): Promise<ToolContext> => ({
|
|
164
|
+
user: ANA,
|
|
165
|
+
scope: auth.scope,
|
|
166
|
+
source: auth.source,
|
|
167
|
+
sessionId,
|
|
168
|
+
abortSignal: signal,
|
|
169
|
+
workspaceService,
|
|
170
|
+
workflowService: {} as never,
|
|
171
|
+
events: {} as never,
|
|
172
|
+
getFilesystem: async () => fs,
|
|
173
|
+
});
|
|
174
|
+
const toolHandler = createToolHandlerFactory(resolve, options.writeAccess, options.branches);
|
|
175
|
+
const fakeAuth = (req: express.Request, _res: express.Response, next: express.NextFunction) => {
|
|
176
|
+
req.toolAuth = { source: 'external', userId: ANA.id, scope: options.scope ?? 'write' };
|
|
177
|
+
next();
|
|
178
|
+
};
|
|
179
|
+
const app = express();
|
|
180
|
+
app.use(express.json());
|
|
181
|
+
const router = express.Router();
|
|
182
|
+
registerWorkspaceTools(
|
|
183
|
+
new ToolRegistry(),
|
|
184
|
+
router,
|
|
185
|
+
fakeAuth,
|
|
186
|
+
toolHandler,
|
|
187
|
+
new SpillStore(join(tmpdir(), 'bevel-test-spills')),
|
|
188
|
+
new DocExtractService(docCache),
|
|
189
|
+
accessControl,
|
|
190
|
+
testKbContext({ kbDirName: KB }),
|
|
191
|
+
{ recoveryBotEmail: 'recovery-bot@bevel.local', hooks, notes: new ToolDescriptionNotes() },
|
|
192
|
+
new RoutineWritePolicyService(),
|
|
193
|
+
{} as never,
|
|
194
|
+
undefined,
|
|
195
|
+
undefined,
|
|
196
|
+
undefined,
|
|
197
|
+
undefined,
|
|
198
|
+
store,
|
|
199
|
+
);
|
|
200
|
+
// The identifier the composition root wires, over REAL signed sessions and
|
|
201
|
+
// a stand-in key store (`key_<user id>` is a connection key of that user).
|
|
202
|
+
const identify = createDownloadFetcherIdentifier(
|
|
203
|
+
{
|
|
204
|
+
verifyToolToken: async (token) =>
|
|
205
|
+
token.startsWith('key_') ? { ok: true, auth: { userId: token.slice(4) } } : { ok: false },
|
|
206
|
+
verifySession: (token) => jwt.verify(token, JWT_SECRET) as { userId: string },
|
|
207
|
+
},
|
|
208
|
+
readAuthCookie,
|
|
209
|
+
);
|
|
210
|
+
router.use(createAgentDownloadRoutes({ downloads: store, identify }));
|
|
211
|
+
app.use('/api', router);
|
|
212
|
+
const server = await new Promise<HttpServer>((r) => {
|
|
213
|
+
const s = app.listen(0, () => r(s));
|
|
214
|
+
});
|
|
215
|
+
servers.push(server);
|
|
216
|
+
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
217
|
+
return { base, repo, downloadsRoot, store, readsHooked };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
interface Answer {
|
|
221
|
+
expiresAt: string | null;
|
|
222
|
+
expiresInSeconds: number;
|
|
223
|
+
files: { path: string; bytes: number; sha256: string; downloadUrl: string }[];
|
|
224
|
+
folders: { path: string; bytes: number; downloadUrl: string; files: string[] }[];
|
|
225
|
+
refused: { path: string; reason: string }[];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function request(base: string, paths: string[]): Promise<{ status: number; body: Answer & { error?: string; code?: string } }> {
|
|
229
|
+
const res = await fetch(`${base}/api/agent/tools/request_file_download`, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: { 'content-type': 'application/json' },
|
|
232
|
+
body: JSON.stringify({ branch: BRANCH, paths }),
|
|
233
|
+
});
|
|
234
|
+
return { status: res.status, body: (await res.json()) as never };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The deployment's address for a link the store answered with its public base URL. */
|
|
238
|
+
const local = (base: string, url: string): string => url.replace(PUBLIC_BASE, base);
|
|
239
|
+
const fetchLink = (base: string, url: string, init?: RequestInit) => fetch(local(base, url), init);
|
|
240
|
+
|
|
241
|
+
describe('request_file_download: every file judged on its own', () => {
|
|
242
|
+
it('answers a single image with one link and no zip, serving its bytes as image/png', async () => {
|
|
243
|
+
const h = await start();
|
|
244
|
+
const { status, body } = await request(h.base, [`${KB}/Pictures/logo.png`]);
|
|
245
|
+
|
|
246
|
+
expect(status).toBe(200);
|
|
247
|
+
expect(body.folders).toEqual([]);
|
|
248
|
+
expect(body.refused).toEqual([]);
|
|
249
|
+
expect(body.files).toHaveLength(1);
|
|
250
|
+
expect(body.files[0]).toMatchObject({ path: `${KB}/Pictures/logo.png`, bytes: PNG.byteLength, sha256: sha(PNG) });
|
|
251
|
+
expect(body.files[0]!.downloadUrl.startsWith(`${PUBLIC_BASE}/api/agent/downloads/bevel-down_`)).toBe(true);
|
|
252
|
+
expect(body.expiresInSeconds).toBe(900);
|
|
253
|
+
expect(Date.parse(body.expiresAt!)).toBeGreaterThan(Date.now());
|
|
254
|
+
|
|
255
|
+
const res = await fetchLink(h.base, body.files[0]!.downloadUrl);
|
|
256
|
+
expect(res.status).toBe(200);
|
|
257
|
+
expect(res.headers.get('content-type')).toBe('image/png');
|
|
258
|
+
expect(res.headers.get('content-disposition')).toBe('attachment; filename="logo.png"; filename*=UTF-8\'\'logo.png');
|
|
259
|
+
expect(res.headers.get('cache-control')).toBe('no-store');
|
|
260
|
+
expect(res.headers.get('x-content-type-options')).toBe('nosniff');
|
|
261
|
+
const got = Buffer.from(await res.arrayBuffer());
|
|
262
|
+
expect(got.equals(PNG)).toBe(true);
|
|
263
|
+
expect(sha(got)).toBe(body.files[0]!.sha256);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('includes the readable, downloadable files from the folder-zip leak cases, names the one it may not download, and leaves the unreadable ones out unnamed', async () => {
|
|
267
|
+
const h = await start();
|
|
268
|
+
const { status, body } = await request(h.base, [`${KB}/Shared`]);
|
|
269
|
+
|
|
270
|
+
expect(status).toBe(200);
|
|
271
|
+
expect(body.files.map((f) => f.path).sort()).toEqual([`${KB}/Shared/Open.md`, `${KB}/Shared/access.md`]);
|
|
272
|
+
// A readable file the caller may not download is named: the explorer
|
|
273
|
+
// shows it. The files they may not read are not — their names are what
|
|
274
|
+
// the read model hides, and a folder answer must not list them.
|
|
275
|
+
expect(body.refused).toEqual([{ path: `${KB}/Shared/Node-Deny-Download.md`, reason: DOWNLOAD_PERMISSION_REQUIRED }]);
|
|
276
|
+
for (const hidden of ['Inner/Plan.md', 'Inner/access.md', 'Node-Deny-Read.md']) {
|
|
277
|
+
expect(JSON.stringify(body)).not.toContain(hidden);
|
|
278
|
+
}
|
|
279
|
+
// A hidden file or FOLDER the caller NAMES is answered exactly as a missing one.
|
|
280
|
+
const missing = await request(h.base, [`${KB}/Shared/Nope.md`, `${KB}/Shared/Node-Deny-Read.md`, `${KB}/Shared/Inner`, `${KB}/Shared/Nowhere`]);
|
|
281
|
+
expect([...missing.body.refused].sort((a, b) => (a.path < b.path ? -1 : 1))).toEqual([
|
|
282
|
+
{ path: `${KB}/Shared/Inner`, reason: NOT_FOUND },
|
|
283
|
+
{ path: `${KB}/Shared/Node-Deny-Read.md`, reason: NOT_FOUND },
|
|
284
|
+
{ path: `${KB}/Shared/Nope.md`, reason: NOT_FOUND },
|
|
285
|
+
{ path: `${KB}/Shared/Nowhere`, reason: NOT_FOUND },
|
|
286
|
+
]);
|
|
287
|
+
expect(JSON.stringify(missing.body)).not.toContain('Plan');
|
|
288
|
+
// As the explorer shows them: a folder the caller may not read with
|
|
289
|
+
// nothing readable beneath it is dropped, so it is `not found`; a folder
|
|
290
|
+
// they may read stays even when every file in it is kept from them, and
|
|
291
|
+
// "holds no files", exactly as a folder with nothing in it at all.
|
|
292
|
+
const veiled = await request(h.base, [`${KB}/Veiled`, `${KB}/Readable/Deep`, `${KB}/Pictures/sub`]);
|
|
293
|
+
expect([...veiled.body.refused].sort((a, b) => (a.path < b.path ? -1 : 1))).toEqual([
|
|
294
|
+
{ path: `${KB}/Pictures/sub`, reason: 'the folder holds no files' },
|
|
295
|
+
{ path: `${KB}/Readable/Deep`, reason: 'the folder holds no files' },
|
|
296
|
+
{ path: `${KB}/Veiled`, reason: NOT_FOUND },
|
|
297
|
+
]);
|
|
298
|
+
expect(JSON.stringify(veiled.body)).not.toContain('Secret');
|
|
299
|
+
|
|
300
|
+
// One zip for the folder, its entries at their full repository paths.
|
|
301
|
+
expect(body.folders).toHaveLength(1);
|
|
302
|
+
const folder = body.folders[0]!;
|
|
303
|
+
expect(folder.path).toBe(`${KB}/Shared`);
|
|
304
|
+
expect([...folder.files].sort()).toEqual([`${KB}/Shared/Open.md`, `${KB}/Shared/access.md`]);
|
|
305
|
+
const res = await fetchLink(h.base, folder.downloadUrl);
|
|
306
|
+
expect(res.status).toBe(200);
|
|
307
|
+
expect(res.headers.get('content-type')).toBe('application/zip');
|
|
308
|
+
expect(res.headers.get('content-disposition')).toBe('attachment; filename="Shared.zip"; filename*=UTF-8\'\'Shared.zip');
|
|
309
|
+
expect(res.headers.get('cache-control')).toBe('no-store');
|
|
310
|
+
const zip = new AdmZip(Buffer.from(await res.arrayBuffer()));
|
|
311
|
+
const entries = zip.getEntries().map((e) => e.entryName).sort();
|
|
312
|
+
expect(entries).toEqual(['Shared/Open.md', 'Shared/access.md']);
|
|
313
|
+
// Every entry hashes as the manifest says.
|
|
314
|
+
for (const f of body.files) {
|
|
315
|
+
expect(sha(zip.getEntry(f.path.slice(KB.length + 1))!.getData())).toBe(f.sha256);
|
|
316
|
+
}
|
|
317
|
+
expect(folder.bytes).toBe(body.files.reduce((n, f) => n + f.bytes, 0));
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('refuses a readable file the caller may not download, and issues no link when nothing can be included', async () => {
|
|
321
|
+
const h = await start();
|
|
322
|
+
const { status, body } = await request(h.base, [`${KB}/Readable/Top.md`, `${KB}/Shared/Node-Deny-Read.md`, `${KB}/Gone.md`]);
|
|
323
|
+
|
|
324
|
+
expect(status).toBe(200);
|
|
325
|
+
expect(body).toEqual({
|
|
326
|
+
expiresAt: null,
|
|
327
|
+
expiresInSeconds: 0,
|
|
328
|
+
files: [],
|
|
329
|
+
folders: [],
|
|
330
|
+
refused: [
|
|
331
|
+
{ path: `${KB}/Gone.md`, reason: NOT_FOUND },
|
|
332
|
+
{ path: `${KB}/Readable/Top.md`, reason: DOWNLOAD_PERMISSION_REQUIRED },
|
|
333
|
+
{ path: `${KB}/Shared/Node-Deny-Read.md`, reason: NOT_FOUND },
|
|
334
|
+
],
|
|
335
|
+
});
|
|
336
|
+
expect(await readdir(h.downloadsRoot)).toEqual([]);
|
|
337
|
+
// Nothing issued, so nothing is held open against the cap.
|
|
338
|
+
expect(h.store.openRequestsOf(ANA.id)).toBe(0);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('serves the other paths when one does not exist, and takes paths without the prefix', async () => {
|
|
342
|
+
const h = await start();
|
|
343
|
+
const { body } = await request(h.base, ['Missing/Thing.md', 'Shared/Open.md']);
|
|
344
|
+
|
|
345
|
+
expect(body.refused).toEqual([{ path: `${KB}/Missing/Thing.md`, reason: NOT_FOUND }]);
|
|
346
|
+
expect(body.files.map((f) => f.path)).toEqual([`${KB}/Shared/Open.md`]);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('names a file once when it is asked for directly and inside a folder', async () => {
|
|
350
|
+
const h = await start();
|
|
351
|
+
const { body } = await request(h.base, [`${KB}/Shared/Open.md`, `${KB}/Shared`, `${KB}/Shared/Open.md`]);
|
|
352
|
+
|
|
353
|
+
expect(body.files.filter((f) => f.path === `${KB}/Shared/Open.md`)).toHaveLength(1);
|
|
354
|
+
expect(body.folders).toHaveLength(1);
|
|
355
|
+
const zip = new AdmZip(Buffer.from(await (await fetchLink(h.base, body.folders[0]!.downloadUrl)).arrayBuffer()));
|
|
356
|
+
expect(zip.getEntries().filter((e) => e.entryName === 'Shared/Open.md')).toHaveLength(1);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('skips what the file explorer skips, and never reaches the git folder', async () => {
|
|
360
|
+
const h = await start();
|
|
361
|
+
const { body } = await request(h.base, [`${KB}/Pictures`, `${KB}/.git/config`]);
|
|
362
|
+
|
|
363
|
+
const paths = body.files.map((f) => f.path);
|
|
364
|
+
expect(paths).toContain(`${KB}/Pictures/logo.png`);
|
|
365
|
+
expect(paths).not.toContain(`${KB}/Pictures/Ignored.md`);
|
|
366
|
+
expect(paths.some((p) => p.endsWith('.gitkeep'))).toBe(false);
|
|
367
|
+
expect(paths.some((p) => p.includes('.git/'))).toBe(false);
|
|
368
|
+
expect(body.refused).toEqual([{ path: `${KB}/.git/config`, reason: NOT_FOUND }]);
|
|
369
|
+
expect(body.folders[0]!.files).toEqual(paths);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('honours the .bevelignore rules of the folders above a requested folder', async () => {
|
|
373
|
+
const h = await start({
|
|
374
|
+
files: {
|
|
375
|
+
...FILES,
|
|
376
|
+
'.bevelignore': 'Pictures/Deep/Secret.md\nPictures/Gone/\n',
|
|
377
|
+
'Pictures/Deep/Open.md': '# open\n',
|
|
378
|
+
'Pictures/Deep/Secret.md': '# ignored from the root\n',
|
|
379
|
+
'Pictures/Gone/Inside.md': '# in an ignored folder\n',
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
const { body } = await request(h.base, [`${KB}/Pictures/Deep`, `${KB}/Pictures/Gone`]);
|
|
383
|
+
|
|
384
|
+
expect(body.files.map((f) => f.path)).toEqual([`${KB}/Pictures/Deep/Open.md`]);
|
|
385
|
+
expect(body.folders.map((f) => f.files)).toEqual([[`${KB}/Pictures/Deep/Open.md`]]);
|
|
386
|
+
expect(body.refused).toEqual([{ path: `${KB}/Pictures/Gone`, reason: 'the folder holds no files' }]);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('takes a path as written, without trimming the spaces around it', async () => {
|
|
390
|
+
const h = await start();
|
|
391
|
+
const { body } = await request(h.base, [`${KB}/Shared/Open.md `]);
|
|
392
|
+
|
|
393
|
+
expect(body.files).toEqual([]);
|
|
394
|
+
expect(body.refused).toHaveLength(1);
|
|
395
|
+
expect(body.refused[0]!.path).not.toBe(`${KB}/Shared/Open.md`);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('serves the bytes captured at request time, not a later save', async () => {
|
|
399
|
+
const h = await start();
|
|
400
|
+
const { body } = await request(h.base, [`${KB}/Shared/Open.md`]);
|
|
401
|
+
await writeFile(join(h.repo, 'Shared/Open.md'), '# changed after the request\n');
|
|
402
|
+
|
|
403
|
+
const got = await (await fetchLink(h.base, body.files[0]!.downloadUrl)).text();
|
|
404
|
+
expect(got).toBe('# open\n');
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe('a download link answers once', () => {
|
|
409
|
+
it('refuses a second fetch, an unknown token, and another user\'s fetch with one and the same answer', async () => {
|
|
410
|
+
const h = await start();
|
|
411
|
+
const { body } = await request(h.base, [`${KB}/Shared/Open.md`, `${KB}/Shared/access.md`]);
|
|
412
|
+
const [first, second] = body.files;
|
|
413
|
+
|
|
414
|
+
expect((await fetchLink(h.base, first!.downloadUrl)).status).toBe(200);
|
|
415
|
+
const again = await fetchLink(h.base, first!.downloadUrl);
|
|
416
|
+
const unknown = await fetch(`${h.base}/api/agent/downloads/bevel-down_not-a-token`);
|
|
417
|
+
const foreign = await fetchLink(h.base, second!.downloadUrl, { headers: { authorization: 'Bearer key_user-mallory' } });
|
|
418
|
+
for (const res of [again, unknown, foreign]) {
|
|
419
|
+
expect(res.status).toBe(404);
|
|
420
|
+
expect(await res.json()).toEqual({ error: DOWNLOAD_TOKEN_REFUSAL });
|
|
421
|
+
}
|
|
422
|
+
expect(DOWNLOAD_TOKEN_REFUSAL).toContain('request_file_download');
|
|
423
|
+
// The issuer's own credential is no obstacle — and a refused foreign fetch did not spend the link.
|
|
424
|
+
const own = await fetchLink(h.base, second!.downloadUrl, { headers: { authorization: `Bearer key_${ANA.id}` } });
|
|
425
|
+
expect(own.status).toBe(200);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('refuses another user\'s signed-in session in every spelling, without spending the link', async () => {
|
|
429
|
+
const h = await start();
|
|
430
|
+
const { body } = await request(h.base, [`${KB}/Shared/Open.md`]);
|
|
431
|
+
const url = local(h.base, body.files[0]!.downloadUrl);
|
|
432
|
+
const token = decodeURIComponent(url.slice(url.lastIndexOf('/') + 1));
|
|
433
|
+
const mallory = sessionOf('user-mallory');
|
|
434
|
+
|
|
435
|
+
const attempts = [
|
|
436
|
+
// A session JWT as a bearer, on the path form and on the header form.
|
|
437
|
+
await fetch(url, { headers: { authorization: `Bearer ${mallory}` } }),
|
|
438
|
+
await fetch(`${h.base}/api/agent/downloads`, {
|
|
439
|
+
headers: { authorization: `Bearer ${mallory}`, 'x-download-token': token },
|
|
440
|
+
}),
|
|
441
|
+
// The cookie a signed-in browser sends on its own.
|
|
442
|
+
await fetch(url, { headers: { cookie: `bevel_token=${encodeURIComponent(mallory)}` } }),
|
|
443
|
+
];
|
|
444
|
+
for (const res of attempts) {
|
|
445
|
+
expect(res.status).toBe(404);
|
|
446
|
+
expect(await res.json()).toEqual({ error: DOWNLOAD_TOKEN_REFUSAL });
|
|
447
|
+
}
|
|
448
|
+
// None of those spent it: the owner's own session still gets the bytes.
|
|
449
|
+
const own = await fetch(url, { headers: { authorization: `Bearer ${sessionOf(ANA.id)}` } });
|
|
450
|
+
expect(own.status).toBe(200);
|
|
451
|
+
expect(await own.text()).toBe('# open\n');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('refuses a fetch whose bearer is the owner but whose cookie is someone else, without spending the link', async () => {
|
|
455
|
+
const h = await start();
|
|
456
|
+
const { body } = await request(h.base, [`${KB}/Shared/Open.md`]);
|
|
457
|
+
const url = local(h.base, body.files[0]!.downloadUrl);
|
|
458
|
+
|
|
459
|
+
const mixed = await fetch(url, {
|
|
460
|
+
headers: {
|
|
461
|
+
authorization: `Bearer key_${ANA.id}`,
|
|
462
|
+
cookie: `bevel_token=${encodeURIComponent(sessionOf('user-mallory'))}`,
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
expect(mixed.status).toBe(404);
|
|
466
|
+
expect(await mixed.json()).toEqual({ error: DOWNLOAD_TOKEN_REFUSAL });
|
|
467
|
+
const own = await fetch(url, {
|
|
468
|
+
headers: { authorization: `Bearer key_${ANA.id}`, cookie: `bevel_token=${encodeURIComponent(sessionOf(ANA.id))}` },
|
|
469
|
+
});
|
|
470
|
+
expect(own.status).toBe(200);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('names the attachment in both forms, encoding what RFC 5987 requires', () => {
|
|
474
|
+
expect(attachmentDisposition("it's (a)*.md")).toBe(
|
|
475
|
+
'attachment; filename="it\'s (a)*.md"; filename*=UTF-8\'\'it%27s%20%28a%29%2A.md',
|
|
476
|
+
);
|
|
477
|
+
expect(attachmentDisposition('Plän "x"\r\n.md')).toBe(
|
|
478
|
+
'attachment; filename="Pl_n _x_.md"; filename*=UTF-8\'\'Pl%C3%A4n%20%22x%22.md',
|
|
479
|
+
);
|
|
480
|
+
expect(attachmentDisposition('')).toBe('attachment; filename="download"; filename*=UTF-8\'\'download');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('serves a fetch whose credential identifies nobody, since the link is the credential', async () => {
|
|
484
|
+
const h = await start();
|
|
485
|
+
const { body } = await request(h.base, [`${KB}/Shared/Open.md`, `${KB}/Shared/access.md`]);
|
|
486
|
+
|
|
487
|
+
const forged = jwt.sign({ userId: 'user-mallory' }, 'not-our-secret');
|
|
488
|
+
expect((await fetchLink(h.base, body.files[0]!.downloadUrl, { headers: { authorization: `Bearer ${forged}` } })).status).toBe(200);
|
|
489
|
+
expect((await fetchLink(h.base, body.files[1]!.downloadUrl, { headers: { authorization: 'Bearer junk' } })).status).toBe(200);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('takes the token in an x-download-token header on the address without its last segment', async () => {
|
|
493
|
+
const h = await start();
|
|
494
|
+
const { body } = await request(h.base, [`${KB}/Shared/Open.md`]);
|
|
495
|
+
const url = local(h.base, body.files[0]!.downloadUrl);
|
|
496
|
+
const token = decodeURIComponent(url.slice(url.lastIndexOf('/') + 1));
|
|
497
|
+
|
|
498
|
+
const res = await fetch(`${h.base}/api/agent/downloads`, { headers: { 'x-download-token': token } });
|
|
499
|
+
expect(res.status).toBe(200);
|
|
500
|
+
expect(await res.text()).toBe('# open\n');
|
|
501
|
+
expect((await fetch(url)).status).toBe(404);
|
|
502
|
+
expect((await fetch(`${h.base}/api/agent/downloads`)).status).toBe(404);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('does not spend a link on a HEAD probe', async () => {
|
|
506
|
+
const h = await start();
|
|
507
|
+
const { body } = await request(h.base, [`${KB}/Shared/Open.md`]);
|
|
508
|
+
const url = body.files[0]!.downloadUrl;
|
|
509
|
+
|
|
510
|
+
expect((await fetchLink(h.base, url, { method: 'HEAD' })).status).toBe(405);
|
|
511
|
+
const res = await fetchLink(h.base, url);
|
|
512
|
+
expect(res.status).toBe(200);
|
|
513
|
+
expect(await res.text()).toBe('# open\n');
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('refuses an expired link the same way, and the sweep deletes bytes nobody fetched', async () => {
|
|
517
|
+
const h = await start({ ttlMs: 50 });
|
|
518
|
+
const { body } = await request(h.base, [`${KB}/Shared`]);
|
|
519
|
+
expect((await readdir(h.downloadsRoot)).length).toBe(1);
|
|
520
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
521
|
+
|
|
522
|
+
const res = await fetchLink(h.base, body.files[0]!.downloadUrl);
|
|
523
|
+
expect(res.status).toBe(404);
|
|
524
|
+
expect(await res.json()).toEqual({ error: DOWNLOAD_TOKEN_REFUSAL });
|
|
525
|
+
await h.store.sweepNow();
|
|
526
|
+
expect(await readdir(h.downloadsRoot)).toEqual([]);
|
|
527
|
+
h.store.stopSweeping();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('a request issued after the store was stopped does not start the sweeper again', async () => {
|
|
531
|
+
const h = await start({ ttlMs: 50 });
|
|
532
|
+
await request(h.base, [`${KB}/Shared`]);
|
|
533
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
534
|
+
// Stopped for good, as shutdown stops it. A request that still lands
|
|
535
|
+
// afterwards used to re-arm the sweeper, whose first sweep runs at once
|
|
536
|
+
// and would have deleted the expired request's bytes here.
|
|
537
|
+
h.store.stopSweeping();
|
|
538
|
+
await h.store.drainSweep();
|
|
539
|
+
await request(h.base, [`${KB}/Shared`]);
|
|
540
|
+
await h.store.drainSweep();
|
|
541
|
+
expect((await readdir(h.downloadsRoot)).length).toBe(2);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('deletes the bytes of a request once every link of it has been fetched', async () => {
|
|
545
|
+
const h = await start();
|
|
546
|
+
const { body } = await request(h.base, [`${KB}/Shared`]);
|
|
547
|
+
for (const url of [...body.files.map((f) => f.downloadUrl), body.folders[0]!.downloadUrl]) {
|
|
548
|
+
await (await fetchLink(h.base, url)).arrayBuffer();
|
|
549
|
+
}
|
|
550
|
+
// `finish` runs as the response closes: wait for the state, not a fixed
|
|
551
|
+
// number of milliseconds a loaded machine may need more of.
|
|
552
|
+
const deadline = Date.now() + 5000;
|
|
553
|
+
while ((await readdir(h.downloadsRoot)).length > 0 && Date.now() < deadline) {
|
|
554
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
555
|
+
}
|
|
556
|
+
expect(await readdir(h.downloadsRoot)).toEqual([]);
|
|
557
|
+
expect(h.store.openRequestsOf(ANA.id)).toBe(0);
|
|
558
|
+
h.store.stopSweeping();
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
describe('a folder is gated as a folder', () => {
|
|
563
|
+
it("refuses a folder's zip when the folder denies download, though a file inside grants it; the file alone goes out", async () => {
|
|
564
|
+
const h = await start();
|
|
565
|
+
const folder = await request(h.base, [`${KB}/Gated`]);
|
|
566
|
+
expect(folder.status).toBe(200);
|
|
567
|
+
expect(folder.body.folders).toEqual([]);
|
|
568
|
+
expect(folder.body.files).toEqual([]);
|
|
569
|
+
expect(folder.body.refused).toEqual([{ path: `${KB}/Gated`, reason: DOWNLOAD_PERMISSION_REQUIRED }]);
|
|
570
|
+
|
|
571
|
+
const file = await request(h.base, [`${KB}/Gated/Open.md`]);
|
|
572
|
+
expect(file.status).toBe(200);
|
|
573
|
+
expect(file.body.files.map((f) => f.path)).toEqual([`${KB}/Gated/Open.md`]);
|
|
574
|
+
h.store.stopSweeping();
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
describe('limits', () => {
|
|
579
|
+
it('refuses the whole request over 500 MB, naming the total and the limit, before reading a byte', async () => {
|
|
580
|
+
const h = await start();
|
|
581
|
+
// Sparse: 501 MB on paper, nothing on disk — the refusal is decided by size alone.
|
|
582
|
+
await truncate(join(h.repo, 'Pictures/logo.png'), 501 * 1024 * 1024);
|
|
583
|
+
const { status, body } = await request(h.base, [`${KB}/Pictures/logo.png`, `${KB}/Shared/Open.md`]);
|
|
584
|
+
|
|
585
|
+
expect(status).toBe(413);
|
|
586
|
+
const total = 501 * 1024 * 1024 + '# open\n'.length;
|
|
587
|
+
expect(body.error).toContain(`${total} bytes`);
|
|
588
|
+
expect(body.error).toContain(`${500 * 1024 * 1024} byte download limit`);
|
|
589
|
+
expect(await readdir(h.downloadsRoot)).toEqual([]);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('checks the size before any file is read', async () => {
|
|
593
|
+
const readFile = vi.fn(async () => Buffer.from('x'));
|
|
594
|
+
await expect(
|
|
595
|
+
buildDownload([`${KB}/a.md`, `${KB}/b.md`], {
|
|
596
|
+
kbDirName: KB,
|
|
597
|
+
maxBytes: 10,
|
|
598
|
+
maxFiles: 100,
|
|
599
|
+
candidatesAt: async (p) => ({ kind: 'file', files: [{ path: p.slice(KB.length + 1), bytes: 6 }] }),
|
|
600
|
+
canReadBatch: async (ps) => new Map(ps.map((p) => [p, true])),
|
|
601
|
+
canDownloadBatch: async (ps) => new Map(ps.map((p) => [p, true])),
|
|
602
|
+
notifyRead: async () => undefined,
|
|
603
|
+
readFile,
|
|
604
|
+
contentTypeOf: () => 'text/markdown',
|
|
605
|
+
}),
|
|
606
|
+
).rejects.toMatchObject({ status: 413, details: { totalBytes: 12, maxBytes: 10 } });
|
|
607
|
+
expect(readFile).not.toHaveBeenCalled();
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
/** Build deps over an in-memory tree: `tree` maps each requested path to the files under it. */
|
|
611
|
+
const fakeDeps = (tree: Record<string, { path: string; bytes: number }[]>, maxBytes: number, maxFiles: number) => ({
|
|
612
|
+
kbDirName: KB,
|
|
613
|
+
maxBytes,
|
|
614
|
+
maxFiles,
|
|
615
|
+
candidatesAt: async (p: string) => ({ kind: 'folder' as const, files: tree[p] ?? [] }),
|
|
616
|
+
canReadBatch: async (ps: string[]) => new Map(ps.map((p) => [p, true])),
|
|
617
|
+
canDownloadBatch: async (ps: string[]) => new Map(ps.map((p) => [p, true])),
|
|
618
|
+
notifyRead: async () => undefined,
|
|
619
|
+
readFile: vi.fn(async () => Buffer.from('xxxx')),
|
|
620
|
+
contentTypeOf: () => 'text/markdown',
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('counts a file in every requested folder that zips it, so overlapping folders cannot multiply the limit', async () => {
|
|
624
|
+
const x = { path: 'a/x.md', bytes: 4 };
|
|
625
|
+
const y = { path: 'a/b/y.md', bytes: 4 };
|
|
626
|
+
const deps = fakeDeps({ [`${KB}/a`]: [x, y], [`${KB}/a/b`]: [y] }, 10, 100);
|
|
627
|
+
// The files total 8, under the limit; the two zips together carry 12.
|
|
628
|
+
await expect(buildDownload([`${KB}/a`, `${KB}/a/b`], deps)).rejects.toMatchObject({
|
|
629
|
+
status: 413,
|
|
630
|
+
details: { totalBytes: 12, maxBytes: 10 },
|
|
631
|
+
});
|
|
632
|
+
expect(deps.readFile).not.toHaveBeenCalled();
|
|
633
|
+
// Either folder alone fits.
|
|
634
|
+
const one = await buildDownload([`${KB}/a`], fakeDeps({ [`${KB}/a`]: [x, y] }, 10, 100));
|
|
635
|
+
expect(one.folders).toHaveLength(1);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('refuses a request holding more readable files than one request may carry, before reading any', async () => {
|
|
639
|
+
const two = [{ path: 'a/1.md', bytes: 0 }, { path: 'a/2.md', bytes: 0 }];
|
|
640
|
+
const deps = fakeDeps({ [`${KB}/a`]: two }, 10, 1);
|
|
641
|
+
await expect(buildDownload([`${KB}/a`], deps)).rejects.toMatchObject({
|
|
642
|
+
status: 413,
|
|
643
|
+
details: { kind: 'download-too-many-files', maxFiles: 1 },
|
|
644
|
+
});
|
|
645
|
+
expect(deps.readFile).not.toHaveBeenCalled();
|
|
646
|
+
// Only what the caller may read counts: the refusal must not tell them
|
|
647
|
+
// how many files they cannot see.
|
|
648
|
+
const oneHidden = { ...fakeDeps({ [`${KB}/a`]: two }, 10, 1), canReadBatch: async (ps: string[]) => new Map(ps.map((p) => [p, p === 'a/1.md'])) };
|
|
649
|
+
const built = await buildDownload([`${KB}/a`], oneHidden);
|
|
650
|
+
expect(built.files.map((f) => f.path)).toEqual([`${KB}/a/1.md`]);
|
|
651
|
+
expect(built.refused).toEqual([]);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('counts calls still building against the cap, so parallel calls cannot all pass it', async () => {
|
|
655
|
+
const h = await start({ maxOpenPerUser: 2 });
|
|
656
|
+
const answers = await Promise.all([1, 2, 3, 4].map(() => request(h.base, [`${KB}/Shared/Open.md`])));
|
|
657
|
+
|
|
658
|
+
expect(answers.map((a) => a.status).sort()).toEqual([200, 200, 429, 429]);
|
|
659
|
+
expect(await readdir(h.downloadsRoot)).toHaveLength(2);
|
|
660
|
+
expect(h.store.openRequestsOf(ANA.id)).toBe(2);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('refuses a nested slot for the same user instead of waiting on its own turn, and issues once per slot', async () => {
|
|
664
|
+
const root = await mkdtemp(join(tmpdir(), 'agent-dl-slot-'));
|
|
665
|
+
dirs.push(root);
|
|
666
|
+
const store = new AgentDownloadStore({ root, publicBaseUrl: PUBLIC_BASE });
|
|
667
|
+
stores.push(store);
|
|
668
|
+
const artifact = { data: Buffer.from('x'), contentType: 'text/plain', filename: 'x.txt' };
|
|
669
|
+
|
|
670
|
+
// Would deadlock if the inner call queued behind the outer turn.
|
|
671
|
+
await expect(
|
|
672
|
+
store.withRequestSlot(ANA, () => store.withRequestSlot(ANA, async () => 'never')),
|
|
673
|
+
).rejects.toThrow('already held for the same user');
|
|
674
|
+
// Another user's slot inside is fine: different users never wait on each other.
|
|
675
|
+
await expect(
|
|
676
|
+
store.withRequestSlot(ANA, () => store.withRequestSlot({ id: 'user-bo' }, async () => 'ok')),
|
|
677
|
+
).resolves.toBe('ok');
|
|
678
|
+
|
|
679
|
+
await expect(
|
|
680
|
+
store.withRequestSlot(ANA, async (issue) => {
|
|
681
|
+
await issue([artifact]);
|
|
682
|
+
return issue([artifact]);
|
|
683
|
+
}),
|
|
684
|
+
).rejects.toThrow('issues once');
|
|
685
|
+
expect(store.openRequestsOf(ANA.id)).toBe(1);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('gives the slot back when a call issues nothing', async () => {
|
|
689
|
+
const h = await start({ maxOpenPerUser: 1 });
|
|
690
|
+
expect((await request(h.base, [`${KB}/Gone.md`])).body.files).toEqual([]);
|
|
691
|
+
expect(h.store.openRequestsOf(ANA.id)).toBe(0);
|
|
692
|
+
expect((await request(h.base, [`${KB}/Shared/Open.md`])).status).toBe(200);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('holds a user to 10 open download requests, however many links each carries', async () => {
|
|
696
|
+
const h = await start();
|
|
697
|
+
const answers: Answer[] = [];
|
|
698
|
+
for (let i = 0; i < 10; i++) {
|
|
699
|
+
const ok = await request(h.base, [`${KB}/Shared`]);
|
|
700
|
+
expect(ok.status).toBe(200);
|
|
701
|
+
answers.push(ok.body);
|
|
702
|
+
}
|
|
703
|
+
const over = await request(h.base, [`${KB}/Shared/Open.md`]);
|
|
704
|
+
expect(over.status).toBe(429);
|
|
705
|
+
expect(over.body.error).toContain('10 open download requests');
|
|
706
|
+
// A request whose links have all been fetched is no longer open.
|
|
707
|
+
const first = answers[0]!;
|
|
708
|
+
for (const url of [...first.files.map((f) => f.downloadUrl), first.folders[0]!.downloadUrl]) {
|
|
709
|
+
await (await fetchLink(h.base, url)).arrayBuffer();
|
|
710
|
+
}
|
|
711
|
+
// Wait for the request to drop (the response's close runs `finish`),
|
|
712
|
+
// not a fixed number of milliseconds.
|
|
713
|
+
const deadline = Date.now() + 5000;
|
|
714
|
+
while (h.store.openRequestsOf(ANA.id) > 9 && Date.now() < deadline) {
|
|
715
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
716
|
+
}
|
|
717
|
+
expect((await request(h.base, [`${KB}/Shared/Open.md`])).status).toBe(200);
|
|
718
|
+
h.store.stopSweeping();
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
describe('credentials, deployments and the read hook', () => {
|
|
723
|
+
it('refuses a read-only credential before anything is built', async () => {
|
|
724
|
+
const h = await start({ scope: 'read' });
|
|
725
|
+
const { status, body } = await request(h.base, [`${KB}/Shared/Open.md`]);
|
|
726
|
+
expect(status).toBe(403);
|
|
727
|
+
// About the credential, not the deployment: the tool itself is a read.
|
|
728
|
+
expect(body.error).toContain('read-only credential');
|
|
729
|
+
expect(await readdir(h.downloadsRoot)).toEqual([]);
|
|
730
|
+
expect(h.readsHooked).toEqual([]);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('still serves a download on a read-only deployment: it is a read', async () => {
|
|
734
|
+
const readOnly: IWriteAccess = { canWrite: async () => ({ ok: false, message: 'This deployment is read-only.' }) };
|
|
735
|
+
const h = await start({ writeAccess: readOnly });
|
|
736
|
+
const { status, body } = await request(h.base, [`${KB}/Shared/Open.md`]);
|
|
737
|
+
expect(status).toBe(200);
|
|
738
|
+
expect(body.code).not.toBe(READ_ONLY_CODE);
|
|
739
|
+
expect(body.files).toHaveLength(1);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it('refuses a call that names no branch, or one that does not exist, before anything is captured', async () => {
|
|
743
|
+
const h = await start({
|
|
744
|
+
branches: { defaultBranch: () => BRANCH, isMissing: async (b) => b !== BRANCH },
|
|
745
|
+
});
|
|
746
|
+
const call = (body: Record<string, unknown>) =>
|
|
747
|
+
fetch(`${h.base}/api/agent/tools/request_file_download`, {
|
|
748
|
+
method: 'POST',
|
|
749
|
+
headers: { 'content-type': 'application/json' },
|
|
750
|
+
body: JSON.stringify(body),
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// `branch` is required in the tool's inputs: no default-branch fallback.
|
|
754
|
+
const none = await call({ paths: [`${KB}/Shared/Open.md`] });
|
|
755
|
+
expect(none.status).toBe(400);
|
|
756
|
+
const missing = await call({ branch: 'nobody/never-made-this', paths: [`${KB}/Shared/Open.md`] });
|
|
757
|
+
expect(missing.status).toBe(404);
|
|
758
|
+
expect(await missing.json()).toMatchObject({ kind: 'branch-not-found', branch: 'nobody/never-made-this' });
|
|
759
|
+
expect(await readdir(h.downloadsRoot)).toEqual([]);
|
|
760
|
+
expect(h.readsHooked).toEqual([]);
|
|
761
|
+
|
|
762
|
+
const { status, body } = await request(h.base, [`${KB}/Shared/Open.md`]);
|
|
763
|
+
expect(status).toBe(200);
|
|
764
|
+
expect(body.files).toHaveLength(1);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('takes every included file to the read hook once, and lists a path the hook refuses', async () => {
|
|
768
|
+
const h = await start({ hookRefuses: (p) => p.endsWith('/access.md') });
|
|
769
|
+
const { status, body } = await request(h.base, [`${KB}/Shared`, `${KB}/Shared/Open.md`]);
|
|
770
|
+
|
|
771
|
+
expect(status).toBe(200);
|
|
772
|
+
// Only what passed the access rules reaches the hook, each path once.
|
|
773
|
+
expect([...h.readsHooked].sort()).toEqual([`${KB}/Shared/Open.md`, `${KB}/Shared/access.md`]);
|
|
774
|
+
expect(body.files.map((f) => f.path)).toEqual([`${KB}/Shared/Open.md`]);
|
|
775
|
+
expect(body.refused).toContainEqual({
|
|
776
|
+
path: `${KB}/Shared/access.md`,
|
|
777
|
+
reason: `This session may not read ${KB}/Shared/access.md.`,
|
|
778
|
+
});
|
|
779
|
+
expect(body.folders[0]!.files).toEqual([`${KB}/Shared/Open.md`]);
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
describe('what agents are told', () => {
|
|
784
|
+
it('describes download beside upload in the shared rule on large and binary content', () => {
|
|
785
|
+
const rule = sharedFileRules(DEFAULT_KB_LAYOUT).find((r) => r.id === 'upload-route')!;
|
|
786
|
+
expect(rule.heading).toMatch(/download out/);
|
|
787
|
+
expect(rule.body).toContain('`request_file_upload`');
|
|
788
|
+
expect(rule.body).toContain('`request_file_download`');
|
|
789
|
+
expect(rule.body).toContain('one-time link');
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it('mounts the tool with a description that names the links, the refusals and the way back up', async () => {
|
|
793
|
+
const registry = new ToolRegistry();
|
|
794
|
+
registerWorkspaceTools(
|
|
795
|
+
registry,
|
|
796
|
+
express.Router(),
|
|
797
|
+
(_q, _s, n) => n(),
|
|
798
|
+
createToolHandlerFactory(async () => ({}) as never),
|
|
799
|
+
{} as never,
|
|
800
|
+
{} as never,
|
|
801
|
+
{} as never,
|
|
802
|
+
testKbContext({ kbDirName: KB }),
|
|
803
|
+
{ recoveryBotEmail: 'r@x', hooks: new WorkflowHooks(), notes: new ToolDescriptionNotes() },
|
|
804
|
+
new RoutineWritePolicyService(),
|
|
805
|
+
{} as never,
|
|
806
|
+
undefined,
|
|
807
|
+
undefined,
|
|
808
|
+
undefined,
|
|
809
|
+
undefined,
|
|
810
|
+
new AgentDownloadStore({ root: tmpdir(), publicBaseUrl: PUBLIC_BASE }),
|
|
811
|
+
);
|
|
812
|
+
const def = (await registry.listExternal()).find((t) => t.name === 'request_file_download');
|
|
813
|
+
expect(def).toBeDefined();
|
|
814
|
+
// The answer with no link carries `expiresAt: null`; the schema says so.
|
|
815
|
+
expect((def!.outputs as { properties: Record<string, { type: unknown }> }).properties.expiresAt!.type).toEqual([
|
|
816
|
+
'string',
|
|
817
|
+
'null',
|
|
818
|
+
]);
|
|
819
|
+
for (const phrase of ['`request_file_upload`', '`apply_file_upload`', 'x-download-token', 'not found', 'download permission required', '500 MB', 'ONCE']) {
|
|
820
|
+
expect(def!.description, phrase).toContain(phrase);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
});
|