@bevel-software/platform-core-backend 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-guide/access-control.md +17 -0
- package/agent-guide/conventions.md +8 -0
- package/agent-guide/html-views.md +83 -0
- package/agent-guide/new-knowledge-base.md +24 -0
- package/agent-guide/tool-manuals.md +10 -0
- package/dist/assets.d.ts +19 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +26 -2
- package/dist/assets.js.map +1 -1
- package/dist/core/catalog-revision.d.ts +19 -1
- package/dist/core/catalog-revision.d.ts.map +1 -1
- package/dist/core/catalog-revision.js +14 -3
- package/dist/core/catalog-revision.js.map +1 -1
- package/dist/core/create-core-server.d.ts +5 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +54 -3
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +26 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +100 -8
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +16 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +18 -1
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +21 -0
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +22 -1
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +8 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -0
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +11 -0
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/directory-sync-bot.js +1 -1
- package/dist/modules/access/directory-sync-bot.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.d.ts +6 -0
- package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
- package/dist/modules/agent-guide/agent-guide.js +8 -0
- package/dist/modules/agent-guide/agent-guide.js.map +1 -1
- package/dist/modules/agent-guide/index.d.ts +1 -1
- package/dist/modules/agent-guide/index.d.ts.map +1 -1
- package/dist/modules/agent-guide/index.js +1 -1
- package/dist/modules/agent-guide/index.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +5 -0
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +19 -2
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +19 -19
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
- package/dist/modules/audit/agent-audit.service.d.ts +21 -2
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +43 -2
- package/dist/modules/audit/agent-audit.service.js.map +1 -1
- package/dist/modules/audit/agent-connection.routes.d.ts +34 -0
- package/dist/modules/audit/agent-connection.routes.d.ts.map +1 -0
- package/dist/modules/audit/agent-connection.routes.js +38 -0
- package/dist/modules/audit/agent-connection.routes.js.map +1 -0
- package/dist/modules/audit/audit.contract.d.ts +24 -0
- package/dist/modules/audit/audit.contract.d.ts.map +1 -1
- package/dist/modules/audit/audit.contract.js.map +1 -1
- package/dist/modules/audit/index.d.ts +1 -0
- package/dist/modules/audit/index.d.ts.map +1 -1
- package/dist/modules/audit/index.js +1 -0
- package/dist/modules/audit/index.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +10 -3
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +7 -0
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +1 -1
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +11 -3
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +96 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +32 -3
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/embed/account-link.service.d.ts +42 -0
- package/dist/modules/embed/account-link.service.d.ts.map +1 -0
- package/dist/modules/embed/account-link.service.js +50 -0
- package/dist/modules/embed/account-link.service.js.map +1 -0
- package/dist/modules/embed/embed-link.d.ts +58 -0
- package/dist/modules/embed/embed-link.d.ts.map +1 -0
- package/dist/modules/embed/embed-link.js +160 -0
- package/dist/modules/embed/embed-link.js.map +1 -0
- package/dist/modules/embed/embed.errors.d.ts +30 -0
- package/dist/modules/embed/embed.errors.d.ts.map +1 -0
- package/dist/modules/embed/embed.errors.js +42 -0
- package/dist/modules/embed/embed.errors.js.map +1 -0
- package/dist/modules/embed/embed.interface.d.ts +204 -0
- package/dist/modules/embed/embed.interface.d.ts.map +1 -0
- package/dist/modules/embed/embed.interface.js +2 -0
- package/dist/modules/embed/embed.interface.js.map +1 -0
- package/dist/modules/embed/embed.routes.d.ts +59 -0
- package/dist/modules/embed/embed.routes.d.ts.map +1 -0
- package/dist/modules/embed/embed.routes.js +343 -0
- package/dist/modules/embed/embed.routes.js.map +1 -0
- package/dist/modules/embed/embed.service.d.ts +169 -0
- package/dist/modules/embed/embed.service.d.ts.map +1 -0
- package/dist/modules/embed/embed.service.js +627 -0
- package/dist/modules/embed/embed.service.js.map +1 -0
- package/dist/modules/embed/embed.tools.d.ts +51 -0
- package/dist/modules/embed/embed.tools.d.ts.map +1 -0
- package/dist/modules/embed/embed.tools.js +170 -0
- package/dist/modules/embed/embed.tools.js.map +1 -0
- package/dist/modules/embed/mcp-app.d.ts +64 -0
- package/dist/modules/embed/mcp-app.d.ts.map +1 -0
- package/dist/modules/embed/mcp-app.js +147 -0
- package/dist/modules/embed/mcp-app.js.map +1 -0
- package/dist/modules/embed/mcp-app.routes.d.ts +21 -0
- package/dist/modules/embed/mcp-app.routes.d.ts.map +1 -0
- package/dist/modules/embed/mcp-app.routes.js +36 -0
- package/dist/modules/embed/mcp-app.routes.js.map +1 -0
- package/dist/modules/github-app/github-app.connection.d.ts +15 -0
- package/dist/modules/github-app/github-app.connection.d.ts.map +1 -1
- package/dist/modules/github-app/github-app.connection.js +34 -3
- package/dist/modules/github-app/github-app.connection.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts +65 -0
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +300 -0
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.interface.d.ts +5 -0
- package/dist/modules/kb-sync/kb-sync.interface.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.service.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.service.js +6 -0
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +2 -1
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +28 -1
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +64 -8
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -0
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +32 -4
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/onboarding/onboarding.contract.d.ts +80 -0
- package/dist/modules/onboarding/onboarding.contract.d.ts.map +1 -0
- package/dist/modules/onboarding/onboarding.contract.js +7 -0
- package/dist/modules/onboarding/onboarding.contract.js.map +1 -0
- package/dist/modules/onboarding/onboarding.routes.d.ts +18 -0
- package/dist/modules/onboarding/onboarding.routes.d.ts.map +1 -0
- package/dist/modules/onboarding/onboarding.routes.js +81 -0
- package/dist/modules/onboarding/onboarding.routes.js.map +1 -0
- package/dist/modules/onboarding/starter-pack.service.d.ts +213 -0
- package/dist/modules/onboarding/starter-pack.service.d.ts.map +1 -0
- package/dist/modules/onboarding/starter-pack.service.js +492 -0
- package/dist/modules/onboarding/starter-pack.service.js.map +1 -0
- package/dist/modules/onboarding/starter-packs.d.ts +78 -0
- package/dist/modules/onboarding/starter-packs.d.ts.map +1 -0
- package/dist/modules/onboarding/starter-packs.js +156 -0
- package/dist/modules/onboarding/starter-packs.js.map +1 -0
- package/dist/modules/plugins/index.d.ts +1 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +1 -1
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.d.ts +19 -0
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +18 -1
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +0 -36
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +31 -0
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +34 -0
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +134 -1
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/repository-source.d.ts +1 -0
- package/dist/modules/settings/repository-source.d.ts.map +1 -1
- package/dist/modules/settings/repository-source.js +3 -3
- package/dist/modules/settings/repository-source.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +50 -32
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/connection-key-rejection.d.ts +10 -1
- package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -1
- package/dist/modules/tool-auth/connection-key-rejection.js +11 -3
- package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +8 -0
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +24 -2
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +59 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +9 -4
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/index.d.ts +28 -1
- package/dist/modules/tool-helpers/index.d.ts.map +1 -1
- package/dist/modules/tool-helpers/index.js +28 -1
- package/dist/modules/tool-helpers/index.js.map +1 -1
- package/dist/modules/tool-helpers/route-argument-check.d.ts +10 -0
- package/dist/modules/tool-helpers/route-argument-check.d.ts.map +1 -0
- package/dist/modules/tool-helpers/route-argument-check.js +132 -0
- package/dist/modules/tool-helpers/route-argument-check.js.map +1 -0
- package/dist/modules/tool-helpers/route-tool-schemas.d.ts +89 -0
- package/dist/modules/tool-helpers/route-tool-schemas.d.ts.map +1 -0
- package/dist/modules/tool-helpers/route-tool-schemas.js +113 -0
- package/dist/modules/tool-helpers/route-tool-schemas.js.map +1 -0
- package/dist/modules/tool-helpers/tool-def.d.ts +71 -0
- package/dist/modules/tool-helpers/tool-def.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-def.js +70 -6
- package/dist/modules/tool-helpers/tool-def.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +46 -6
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +157 -8
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +9 -0
- package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +14 -5
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +6 -2
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
- package/dist/modules/tool-registry/description-length.js +27 -5
- package/dist/modules/tool-registry/description-length.js.map +1 -1
- package/dist/modules/tool-registry/guide-first.d.ts +4 -0
- package/dist/modules/tool-registry/guide-first.d.ts.map +1 -1
- package/dist/modules/tool-registry/guide-first.js +9 -1
- package/dist/modules/tool-registry/guide-first.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +53 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js +97 -68
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +8 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +124 -37
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +29 -5
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +62 -5
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +72 -1
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +172 -36
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.d.ts +15 -0
- package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.js +78 -6
- package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +15 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +25 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts +6 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.js +23 -6
- package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
- package/dist/modules/workflow/recovery-bot.js +1 -1
- package/dist/modules/workflow/recovery-bot.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +2 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/sanitize-error.d.ts +16 -0
- package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
- package/dist/modules/workflow/sanitize-error.js +45 -0
- package/dist/modules/workflow/sanitize-error.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +183 -15
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +805 -106
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-download.builder.d.ts +105 -0
- package/dist/modules/workspace/agent-download.builder.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.builder.js +238 -0
- package/dist/modules/workspace/agent-download.builder.js.map +1 -0
- package/dist/modules/workspace/agent-download.routes.d.ts +77 -0
- package/dist/modules/workspace/agent-download.routes.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.routes.js +156 -0
- package/dist/modules/workspace/agent-download.routes.js.map +1 -0
- package/dist/modules/workspace/agent-download.store.d.ts +225 -0
- package/dist/modules/workspace/agent-download.store.d.ts.map +1 -0
- package/dist/modules/workspace/agent-download.store.js +423 -0
- package/dist/modules/workspace/agent-download.store.js.map +1 -0
- package/dist/modules/workspace/agent-upload.store.d.ts +10 -1
- package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -1
- package/dist/modules/workspace/agent-upload.store.js +16 -6
- package/dist/modules/workspace/agent-upload.store.js.map +1 -1
- package/dist/modules/workspace/file-readers/raw-mime.d.ts +6 -0
- package/dist/modules/workspace/file-readers/raw-mime.d.ts.map +1 -0
- package/dist/modules/workspace/file-readers/raw-mime.js +38 -0
- package/dist/modules/workspace/file-readers/raw-mime.js.map +1 -0
- package/dist/modules/workspace/file-readers/text-reader.d.ts +5 -5
- package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/text-reader.js +5 -8
- package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
- package/dist/modules/workspace/first-run.d.ts +111 -0
- package/dist/modules/workspace/first-run.d.ts.map +1 -0
- package/dist/modules/workspace/first-run.js +181 -0
- package/dist/modules/workspace/first-run.js.map +1 -0
- package/dist/modules/workspace/move-links.d.ts +92 -0
- package/dist/modules/workspace/move-links.d.ts.map +1 -0
- package/dist/modules/workspace/move-links.js +173 -0
- package/dist/modules/workspace/move-links.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +3 -13
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +63 -22
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +75 -2
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +197 -24
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +59 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +443 -65
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/write-access/write-access.d.ts.map +1 -1
- package/dist/modules/write-access/write-access.js +9 -0
- package/dist/modules/write-access/write-access.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +73 -1
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +86 -12
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +10 -1
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/shared/utf8-text.d.ts +15 -0
- package/dist/shared/utf8-text.d.ts.map +1 -0
- package/dist/shared/utf8-text.js +20 -0
- package/dist/shared/utf8-text.js.map +1 -0
- package/dist/tenancy/static-tenant-source.d.ts +2 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +9 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/mcp-app/page.html +349 -0
- package/migrations/0017_atlassian_account_links.sql +37 -0
- package/migrations/meta/0017_snapshot.json +2396 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +5 -3
- package/src/__tests__/packaged-assets-ship.test.ts +16 -4
- package/src/assets.ts +28 -2
- package/src/core/__tests__/catalog-revision.test.ts +40 -0
- package/src/core/__tests__/gated-boot.test.ts +1 -0
- package/src/core/__tests__/lifecycle.test.ts +14 -0
- package/src/core/catalog-revision.ts +15 -2
- package/src/core/create-core-server.ts +67 -3
- package/src/core/create-core-services.ts +140 -6
- package/src/core/lifecycle.ts +28 -2
- package/src/core-config.ts +24 -1
- package/src/index.ts +2 -2
- package/src/modules/access/__tests__/access-control.service.test.ts +19 -0
- package/src/modules/access/access-control.interface.ts +13 -0
- package/src/modules/access/access-control.service.ts +16 -0
- package/src/modules/access/directory-sync-bot.ts +1 -1
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +58 -0
- package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +19 -1
- package/src/modules/agent-guide/agent-guide.ts +9 -0
- package/src/modules/agent-guide/index.ts +1 -0
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +14 -0
- package/src/modules/agent-instructions/compose.ts +17 -2
- package/src/modules/agent-instructions/shared-file-rules.ts +19 -19
- package/src/modules/audit/__tests__/agent-audit.service.test.ts +64 -4
- package/src/modules/audit/__tests__/agent-connection.routes.test.ts +102 -0
- package/src/modules/audit/agent-audit.service.ts +58 -3
- package/src/modules/audit/agent-connection.routes.ts +56 -0
- package/src/modules/audit/audit.contract.ts +26 -0
- package/src/modules/audit/index.ts +1 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +5 -3
- package/src/modules/auth/account.routes.ts +10 -3
- package/src/modules/auth/auth.middleware.ts +1 -1
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +18 -2
- package/src/modules/code-mode/code-mode.tool.ts +13 -2
- package/src/modules/database/core-schema.ts +33 -3
- package/src/modules/embed/__tests__/account-links.migration.test.ts +98 -0
- package/src/modules/embed/__tests__/embed-link.test.ts +122 -0
- package/src/modules/embed/__tests__/embed.routes.test.ts +362 -0
- package/src/modules/embed/__tests__/embed.service.lock.test.ts +193 -0
- package/src/modules/embed/__tests__/embed.service.test.ts +805 -0
- package/src/modules/embed/__tests__/embed.tools.test.ts +304 -0
- package/src/modules/embed/__tests__/mcp-app.test.ts +207 -0
- package/src/modules/embed/account-link.service.ts +76 -0
- package/src/modules/embed/embed-link.ts +180 -0
- package/src/modules/embed/embed.errors.ts +43 -0
- package/src/modules/embed/embed.interface.ts +203 -0
- package/src/modules/embed/embed.routes.ts +363 -0
- package/src/modules/embed/embed.service.ts +705 -0
- package/src/modules/embed/embed.tools.ts +206 -0
- package/src/modules/embed/mcp-app.routes.ts +41 -0
- package/src/modules/embed/mcp-app.ts +173 -0
- package/src/modules/github-app/__tests__/github-app.test.ts +76 -0
- package/src/modules/github-app/github-app.connection.ts +38 -4
- package/src/modules/kb-fs/__tests__/move-with-edits.test.ts +304 -0
- package/src/modules/kb-fs/locking-filesystem.ts +287 -0
- package/src/modules/kb-sync/__tests__/kb-sync.service.test.ts +35 -0
- package/src/modules/kb-sync/kb-sync.interface.ts +2 -0
- package/src/modules/kb-sync/kb-sync.service.ts +6 -0
- package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +68 -11
- package/src/modules/mcp/__tests__/mcp-proxy-helpers.test.ts +37 -2
- package/src/modules/mcp/__tests__/mcp.app.e2e.test.ts +200 -0
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +3 -1
- package/src/modules/mcp/__tests__/mcp.service.test.ts +23 -13
- package/src/modules/mcp/__tests__/tool-call-interface.e2e.test.ts +499 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +2 -1
- package/src/modules/mcp/mcp.service.ts +81 -12
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +42 -4
- package/src/modules/onboarding/__tests__/onboarding.routes.test.ts +163 -0
- package/src/modules/onboarding/__tests__/starter-pack.service.test.ts +635 -0
- package/src/modules/onboarding/__tests__/starter-packs.test.ts +169 -0
- package/src/modules/onboarding/onboarding.contract.ts +85 -0
- package/src/modules/onboarding/onboarding.routes.ts +85 -0
- package/src/modules/onboarding/starter-pack.service.ts +580 -0
- package/src/modules/onboarding/starter-packs.ts +182 -0
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +25 -2
- package/src/modules/plugins/index.ts +1 -0
- package/src/modules/plugins/plugin-provision.service.ts +22 -1
- package/src/modules/plugins/plugins.routes.ts +29 -0
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +150 -1
- package/src/modules/settings/__tests__/repository-source.test.ts +9 -0
- package/src/modules/settings/deployment-settings.service.ts +131 -1
- package/src/modules/settings/repository-source.ts +4 -4
- package/src/modules/skills/__tests__/skills.tools.description.test.ts +5 -1
- package/src/modules/skills/skills.tools.ts +55 -35
- package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +32 -0
- package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +65 -0
- package/src/modules/tool-auth/connection-key-rejection.ts +13 -3
- package/src/modules/tool-auth/external-api-key.interface.ts +9 -0
- package/src/modules/tool-auth/external-api-key.service.ts +61 -4
- package/src/modules/tool-auth/tool-auth.middleware.ts +13 -4
- package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +502 -0
- package/src/modules/tool-helpers/__tests__/route-argument-check.test.ts +337 -0
- package/src/modules/tool-helpers/index.ts +34 -1
- package/src/modules/tool-helpers/route-argument-check.ts +149 -0
- package/src/modules/tool-helpers/route-tool-schemas.ts +171 -0
- package/src/modules/tool-helpers/tool-def.ts +132 -7
- package/src/modules/tool-helpers/tool-handler.ts +196 -8
- package/src/modules/tool-helpers/tool.contract.ts +9 -0
- package/src/modules/tool-manuals/tool-manuals.tools.ts +15 -5
- package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +14 -1
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +23 -10
- package/src/modules/tool-registry/description-length.ts +34 -7
- package/src/modules/tool-registry/guide-first.ts +8 -1
- package/src/modules/workflow/__tests__/file-lock.service.deletion-gate.test.ts +104 -0
- package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +66 -0
- package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +31 -2
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +110 -6
- package/src/modules/workflow/__tests__/sanitize-error.test.ts +89 -1
- package/src/modules/workflow/__tests__/workflow.routes.push-refused.test.ts +202 -0
- package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +6 -6
- package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +1026 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +468 -10
- package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +4 -0
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +5 -4
- package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +54 -2
- package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +41 -5
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +127 -2
- package/src/modules/workflow/agent-tools/change-request-read.tools.ts +157 -122
- package/src/modules/workflow/agent-tools/workflow.tools.ts +129 -35
- package/src/modules/workflow/file-lock.service.ts +67 -5
- package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +164 -0
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +48 -1
- package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +231 -2
- package/src/modules/workflow/git/git.service.ts +176 -38
- package/src/modules/workflow/git/node-git-runner.ts +82 -7
- package/src/modules/workflow/pending-commits.service.ts +29 -1
- package/src/modules/workflow/pending-commits.worker.ts +24 -6
- package/src/modules/workflow/recovery-bot.ts +1 -1
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +2 -1
- package/src/modules/workflow/sanitize-error.ts +50 -0
- package/src/modules/workflow/workflow.service.ts +879 -112
- package/src/modules/workspace/__tests__/agent-downloads.test.ts +823 -0
- package/src/modules/workspace/__tests__/first-run.test.ts +234 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +5 -2
- package/src/modules/workspace/__tests__/md-links.test.ts +353 -0
- package/src/modules/workspace/__tests__/move-links.test.ts +261 -0
- package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +219 -0
- package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +3 -0
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +102 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +52 -5
- package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +190 -0
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +8 -4
- package/src/modules/workspace/__tests__/workspace.service.test.ts +50 -8
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +44 -1
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +409 -12
- package/src/modules/workspace/agent-download.builder.ts +308 -0
- package/src/modules/workspace/agent-download.routes.ts +183 -0
- package/src/modules/workspace/agent-download.store.ts +533 -0
- package/src/modules/workspace/agent-upload.store.ts +23 -7
- package/src/modules/workspace/file-readers/raw-mime.ts +40 -0
- package/src/modules/workspace/file-readers/text-reader.ts +5 -8
- package/src/modules/workspace/first-run.ts +222 -0
- package/src/modules/workspace/move-links.ts +237 -0
- package/src/modules/workspace/startup/steps/seed-tree.ts +3 -12
- package/src/modules/workspace/workspace.routes.ts +64 -22
- package/src/modules/workspace/workspace.service.ts +202 -25
- package/src/modules/workspace/workspace.tools.ts +492 -63
- package/src/modules/write-access/__tests__/write-access.test.ts +13 -0
- package/src/modules/write-access/write-access.ts +9 -0
- package/src/shared/domain-errors.ts +108 -11
- package/src/shared/git.contract.ts +8 -1
- package/src/shared/utf8-text.ts +21 -0
- package/src/tenancy/__tests__/static-tenant-source.test.ts +1 -0
- package/src/tenancy/static-tenant-source.ts +10 -2
- package/starter-packs/README.md +103 -0
- package/starter-packs/engineering/KnowledgeBase/About us.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/Architecture.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/engineering/KnowledgeBase/How we ship.md +9 -0
- package/starter-packs/engineering/Plugins/engineering-starter/LICENSE +21 -0
- package/starter-packs/engineering/Plugins/engineering-starter/SOURCE.md +20 -0
- package/starter-packs/engineering/Plugins/engineering-starter/access.md +19 -0
- package/starter-packs/engineering/Plugins/engineering-starter/plugin.json +10 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/SKILL.md +171 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2a-startup-diagnostic.md +157 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/office-hours/sections/phase-2b-builder-brainstorm.md +39 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-ceo-review/SKILL.md +144 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/plan-eng-review/SKILL.md +144 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/review/SKILL.md +80 -0
- package/starter-packs/engineering/Plugins/engineering-starter/skills/review/checklist.md +184 -0
- package/starter-packs/engineering/pack.yaml +11 -0
- package/starter-packs/general/KnowledgeBase/About us.md +9 -0
- package/starter-packs/general/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/general/KnowledgeBase/How we work.md +9 -0
- package/starter-packs/general/Plugins/general-starter/LICENSE +202 -0
- package/starter-packs/general/Plugins/general-starter/SOURCE.md +10 -0
- package/starter-packs/general/Plugins/general-starter/access.md +19 -0
- package/starter-packs/general/Plugins/general-starter/plugin.json +10 -0
- package/starter-packs/general/Plugins/general-starter/skills/memory-management/SKILL.md +332 -0
- package/starter-packs/general/Plugins/general-starter/skills/productivity-start/SKILL.md +163 -0
- package/starter-packs/general/Plugins/general-starter/skills/productivity-update/SKILL.md +167 -0
- package/starter-packs/general/Plugins/general-starter/skills/task-management/SKILL.md +83 -0
- package/starter-packs/general/pack.yaml +10 -0
- package/starter-packs/operations/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/operations/KnowledgeBase/How we work.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Processes.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Tools we use.md +9 -0
- package/starter-packs/operations/KnowledgeBase/Vendors.md +9 -0
- package/starter-packs/operations/Plugins/operations-starter/LICENSE +202 -0
- package/starter-packs/operations/Plugins/operations-starter/SOURCE.md +10 -0
- package/starter-packs/operations/Plugins/operations-starter/access.md +19 -0
- package/starter-packs/operations/Plugins/operations-starter/plugin.json +10 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/capacity-plan/SKILL.md +111 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/change-request/SKILL.md +119 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/compliance-tracking/SKILL.md +44 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/process-doc/SKILL.md +80 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/process-optimization/SKILL.md +39 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/risk-assessment/SKILL.md +40 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/runbook/SKILL.md +85 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/status-report/SKILL.md +75 -0
- package/starter-packs/operations/Plugins/operations-starter/skills/vendor-review/SKILL.md +102 -0
- package/starter-packs/operations/pack.yaml +12 -0
- package/starter-packs/sales/KnowledgeBase/About us.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Customers.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/sales/KnowledgeBase/How we sell.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Objections.md +9 -0
- package/starter-packs/sales/KnowledgeBase/Pricing.md +9 -0
- package/starter-packs/sales/Plugins/sales-starter/LICENSE +202 -0
- package/starter-packs/sales/Plugins/sales-starter/SOURCE.md +10 -0
- package/starter-packs/sales/Plugins/sales-starter/access.md +19 -0
- package/starter-packs/sales/Plugins/sales-starter/plugin.json +10 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-context/SKILL.md +94 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-plan/SKILL.md +95 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-research/SKILL.md +99 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/account-tiering/SKILL.md +101 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/call-prep/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/call-summary/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/close-plan/SKILL.md +106 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/competitive-intelligence/SKILL.md +102 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/create-an-asset/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/crm-hygiene-check/SKILL.md +103 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/customer-health/SKILL.md +108 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/customer-voice/SKILL.md +94 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/daily-briefing/SKILL.md +61 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-advance-gap/SKILL.md +102 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-review/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-signals/SKILL.md +96 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/deal-slip-scenario/SKILL.md +104 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/draft-outreach/SKILL.md +116 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/end-of-day/SKILL.md +114 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/expansion-whitespace/SKILL.md +97 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/forecast/SKILL.md +100 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/handle-objection/SKILL.md +93 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/inbox-sweep/SKILL.md +114 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/lead-triage/SKILL.md +106 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/log-activity/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/pipeline-review/SKILL.md +97 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/renewal-radar/SKILL.md +95 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/rep-context/SKILL.md +90 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/route-lead/SKILL.md +111 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/sales-setup/SKILL.md +200 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/schedule-meeting/SKILL.md +99 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/stakeholder-map/SKILL.md +107 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/team-pipeline/SKILL.md +105 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/update-opportunity/SKILL.md +112 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/weekly-wrap/SKILL.md +90 -0
- package/starter-packs/sales/Plugins/sales-starter/skills/win-loss-review/SKILL.md +88 -0
- package/starter-packs/sales/pack.yaml +13 -0
- package/starter-packs/support/KnowledgeBase/Escalations.md +9 -0
- package/starter-packs/support/KnowledgeBase/Glossary.md +9 -0
- package/starter-packs/support/KnowledgeBase/Known issues.md +9 -0
- package/starter-packs/support/KnowledgeBase/Products.md +9 -0
- package/starter-packs/support/KnowledgeBase/Tone of voice.md +9 -0
- package/starter-packs/support/Plugins/support-starter/LICENSE +202 -0
- package/starter-packs/support/Plugins/support-starter/SOURCE.md +10 -0
- package/starter-packs/support/Plugins/support-starter/access.md +19 -0
- package/starter-packs/support/Plugins/support-starter/plugin.json +10 -0
- package/starter-packs/support/Plugins/support-starter/skills/customer-escalation/SKILL.md +246 -0
- package/starter-packs/support/Plugins/support-starter/skills/customer-research/SKILL.md +250 -0
- package/starter-packs/support/Plugins/support-starter/skills/draft-response/SKILL.md +416 -0
- package/starter-packs/support/Plugins/support-starter/skills/kb-article/SKILL.md +350 -0
- package/starter-packs/support/Plugins/support-starter/skills/ticket-triage/SKILL.md +273 -0
- package/starter-packs/support/pack.yaml +12 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { logger } from '../../shared/logging.js';
|
|
6
|
+
|
|
7
|
+
const log = logger('agent-downloads');
|
|
8
|
+
|
|
9
|
+
/** How long an issued link, and the bytes behind it, stay usable. */
|
|
10
|
+
export const DOWNLOAD_TOKEN_TTL_MS = 15 * 60 * 1000; // 15 minutes
|
|
11
|
+
|
|
12
|
+
/** How often the store looks for links that have expired. */
|
|
13
|
+
const SWEEP_INTERVAL_MS = 60 * 1000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* How long past its expiry a fetch that is still SENDING keeps its bytes.
|
|
17
|
+
*
|
|
18
|
+
* A link fetched a moment before it expires may take longer than the moment
|
|
19
|
+
* to send a large file. Its bytes are being read right now, so the sweep
|
|
20
|
+
* leaves them alone — but only for this long: a response that has neither
|
|
21
|
+
* finished nor failed within it belongs to a process that died, and the
|
|
22
|
+
* record is in memory, so there is no third possibility.
|
|
23
|
+
*/
|
|
24
|
+
const SEND_GRACE_MS = 10 * 60 * 1000; // 10 minutes
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* THE one refusal for every way a download link can fail to be usable: it was
|
|
28
|
+
* never issued, it has already been fetched, it has expired, or it belongs to
|
|
29
|
+
* somebody else. One sentence for all four, for the reason the upload token
|
|
30
|
+
* gives (`UPLOAD_TOKEN_REFUSAL`): the token is the whole credential the route
|
|
31
|
+
* has, and an answer that told the cases apart would let a guesser learn which
|
|
32
|
+
* guesses exist.
|
|
33
|
+
*/
|
|
34
|
+
export const DOWNLOAD_TOKEN_REFUSAL =
|
|
35
|
+
'That download link cannot be used: it is unknown, already fetched, expired, or was issued to someone else. ' +
|
|
36
|
+
'Call `request_file_download` for a new one.';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How many download REQUESTS one user may hold open at once — however many
|
|
40
|
+
* links each carries. A request with forty files and two folders is one.
|
|
41
|
+
*
|
|
42
|
+
* A request is permission to keep up to the whole download limit on the
|
|
43
|
+
* deployment's disk for a TTL; without a bound, one caller asks for a hundred.
|
|
44
|
+
*/
|
|
45
|
+
export const MAX_OPEN_DOWNLOADS_PER_USER = 10;
|
|
46
|
+
|
|
47
|
+
/** A refusal with the HTTP status the download route and the tool both answer. */
|
|
48
|
+
export class DownloadTokenError extends Error {
|
|
49
|
+
constructor(
|
|
50
|
+
message: string,
|
|
51
|
+
readonly status: number,
|
|
52
|
+
) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = 'DownloadTokenError';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One thing a link serves: a file's bytes, or a folder's zip. */
|
|
59
|
+
export interface DownloadArtifact {
|
|
60
|
+
/** The bytes, captured when the request was made. */
|
|
61
|
+
data: Buffer;
|
|
62
|
+
/** The `Content-Type` the link answers with. */
|
|
63
|
+
contentType: string;
|
|
64
|
+
/** The name the attachment is saved under. */
|
|
65
|
+
filename: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** What the `issue` {@link AgentDownloadStore.withRequestSlot} hands its work answers: one link per artifact, in order, and the terms. */
|
|
69
|
+
export interface IssuedDownload {
|
|
70
|
+
/** One absolute URL per artifact, in the order the artifacts were given. */
|
|
71
|
+
downloadUrls: string[];
|
|
72
|
+
/** ISO-8601 instant after which every link of the request, and its bytes, are gone. */
|
|
73
|
+
expiresAt: string;
|
|
74
|
+
/** Seconds from now until `expiresAt`, so a caller need not parse the date. */
|
|
75
|
+
expiresInSeconds: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A link being fetched: what to send, and how. */
|
|
79
|
+
export interface ClaimedDownload {
|
|
80
|
+
/** Absolute path of the stored bytes — OUTSIDE every workspace. */
|
|
81
|
+
absolutePath: string;
|
|
82
|
+
contentType: string;
|
|
83
|
+
filename: string;
|
|
84
|
+
bytes: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface ArtifactRecord {
|
|
88
|
+
requestId: string;
|
|
89
|
+
/** The file name under the request's directory. Never derived from a path or a name. */
|
|
90
|
+
file: string;
|
|
91
|
+
contentType: string;
|
|
92
|
+
filename: string;
|
|
93
|
+
bytes: number;
|
|
94
|
+
/** Set by the fetch that took it: a link answers once. */
|
|
95
|
+
claimedAt?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface RequestRecord {
|
|
99
|
+
id: string;
|
|
100
|
+
userId: string;
|
|
101
|
+
expiresAt: number;
|
|
102
|
+
/** Token hashes of the artifacts not yet fetched to the end. */
|
|
103
|
+
pending: Set<string>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface AgentDownloadStoreOptions {
|
|
107
|
+
/** Directory the bytes are written to. A sibling of `workspacesRoot`, never inside one. */
|
|
108
|
+
root: string;
|
|
109
|
+
/** Absolute base URL of this deployment's API, e.g. `https://core.example.com`. */
|
|
110
|
+
publicBaseUrl: string;
|
|
111
|
+
/** Token prefix, so a leaked credential can be recognised by shape. */
|
|
112
|
+
tokenPrefix?: string;
|
|
113
|
+
ttlMs?: number;
|
|
114
|
+
/** How many requests one user may hold open at once. Defaults to {@link MAX_OPEN_DOWNLOADS_PER_USER}. */
|
|
115
|
+
maxOpenPerUser?: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The download links an agent takes files OUT of the knowledge base by — the
|
|
120
|
+
* twin of the upload store (`agent-upload.store.ts`), and built on the same
|
|
121
|
+
* three properties, because the download route is likewise authenticated by
|
|
122
|
+
* a token alone:
|
|
123
|
+
*
|
|
124
|
+
* - **Unguessable.** 32 random bytes, base64url, per link.
|
|
125
|
+
* - **Never stored in the clear.** Records are keyed by the token's SHA-256.
|
|
126
|
+
* - **Bound and single-use.** A link answers ONE fetch; a second, an expired
|
|
127
|
+
* one and an unknown one all meet {@link DOWNLOAD_TOKEN_REFUSAL}.
|
|
128
|
+
*
|
|
129
|
+
* The bytes are captured at request time (by `request_file_download`, which
|
|
130
|
+
* judges every file before it gets here) and written under `root/<request>/` —
|
|
131
|
+
* a directory beside the workspaces root, which no file tool can name. A link
|
|
132
|
+
* fetched has its bytes deleted once the response ends, however it ends; a
|
|
133
|
+
* link nobody fetched is deleted when it expires.
|
|
134
|
+
*/
|
|
135
|
+
/**
|
|
136
|
+
* The download store as its consumers see it — the port the tool, the
|
|
137
|
+
* download routes and the lifecycle depend on, so a deployment may substitute
|
|
138
|
+
* its own keeper of one-time links.
|
|
139
|
+
*/
|
|
140
|
+
export interface IAgentDownloadStore {
|
|
141
|
+
/** The absolute address a token's link is fetched at. */
|
|
142
|
+
downloadUrlFor(token: string): string;
|
|
143
|
+
/** Refuse, with the cap's sentence, when `user` holds the most requests allowed. */
|
|
144
|
+
assertCanIssue(user: { id: string }): void;
|
|
145
|
+
/** Take one of `user`'s request slots and run `work` in their turn — see the class. */
|
|
146
|
+
withRequestSlot<T>(
|
|
147
|
+
user: { id: string },
|
|
148
|
+
work: (issue: (items: DownloadArtifact[]) => Promise<IssuedDownload>) => Promise<T>,
|
|
149
|
+
): Promise<T>;
|
|
150
|
+
/** Take `token`'s link for ONE fetch and answer what to send. */
|
|
151
|
+
claim(token: string, fetcherIds?: readonly string[]): ClaimedDownload;
|
|
152
|
+
/** A fetch is over: the link's bytes are gone. Idempotent. */
|
|
153
|
+
finish(token: string): Promise<void>;
|
|
154
|
+
/** How many requests `userId` holds open. */
|
|
155
|
+
openRequestsOf(userId: string): number;
|
|
156
|
+
/** Delete every expired request and every orphaned directory, once. */
|
|
157
|
+
sweepNow(): Promise<void>;
|
|
158
|
+
/** Sweep now, and keep sweeping. */
|
|
159
|
+
startSweeping(intervalMs?: number): void;
|
|
160
|
+
/** Stop the sweeps. */
|
|
161
|
+
stopSweeping(): void;
|
|
162
|
+
/** Wait for a sweep in flight, so a root can be torn down after it. */
|
|
163
|
+
drainSweep(): Promise<void>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export class AgentDownloadStore implements IAgentDownloadStore {
|
|
167
|
+
/** Token hash → the one artifact that link serves. */
|
|
168
|
+
private readonly artifacts = new Map<string, ArtifactRecord>();
|
|
169
|
+
/** Request id → the request its links belong to. */
|
|
170
|
+
private readonly requests = new Map<string, RequestRecord>();
|
|
171
|
+
/** Per user, the request in flight — see {@link withRequestSlot}. */
|
|
172
|
+
private readonly building = new Map<string, Promise<unknown>>();
|
|
173
|
+
/** Per user, slots held by calls still waiting, building or writing — counted as open. */
|
|
174
|
+
private readonly reserved = new Map<string, number>();
|
|
175
|
+
/**
|
|
176
|
+
* Request ids whose bytes are being written right now: named in no map yet,
|
|
177
|
+
* and not the sweep's to reclaim. Entered before the first write and left
|
|
178
|
+
* once the request is recorded or its directory removed.
|
|
179
|
+
*/
|
|
180
|
+
private readonly writing = new Set<string>();
|
|
181
|
+
/** The users whose turn the current async context is running in — see {@link withRequestSlot}. */
|
|
182
|
+
private readonly inTurn = new AsyncLocalStorage<ReadonlySet<string>>();
|
|
183
|
+
private readonly root: string;
|
|
184
|
+
private readonly publicBaseUrl: string;
|
|
185
|
+
private readonly tokenPrefix: string;
|
|
186
|
+
private readonly ttlMs: number;
|
|
187
|
+
private readonly maxOpenPerUser: number;
|
|
188
|
+
private sweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
189
|
+
private sweeping: Promise<void> | null = null;
|
|
190
|
+
private stopped = false;
|
|
191
|
+
|
|
192
|
+
constructor(options: AgentDownloadStoreOptions) {
|
|
193
|
+
this.root = options.root;
|
|
194
|
+
this.publicBaseUrl = options.publicBaseUrl.replace(/\/+$/, '');
|
|
195
|
+
this.tokenPrefix = options.tokenPrefix ?? '';
|
|
196
|
+
this.ttlMs = options.ttlMs ?? DOWNLOAD_TOKEN_TTL_MS;
|
|
197
|
+
this.maxOpenPerUser = options.maxOpenPerUser ?? MAX_OPEN_DOWNLOADS_PER_USER;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The URL a link's bytes are fetched from. The one spelling of this route's address. */
|
|
201
|
+
downloadUrlFor(token: string): string {
|
|
202
|
+
return `${this.publicBaseUrl}/api/agent/downloads/${encodeURIComponent(token)}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Refuse `user` a new request when they already hold the most one user may
|
|
207
|
+
* — issued requests and slots still being filled alike.
|
|
208
|
+
*/
|
|
209
|
+
assertCanIssue(user: { id: string }): void {
|
|
210
|
+
const open = this.openRequestsOf(user.id);
|
|
211
|
+
if (open >= this.maxOpenPerUser) {
|
|
212
|
+
throw new DownloadTokenError(
|
|
213
|
+
`You already hold ${open} open download requests, the most one user may have at once. Fetch their links, ` +
|
|
214
|
+
'or wait for them to expire, then ask again. One request carries any number of files and folders.',
|
|
215
|
+
429,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Take one of `user`'s request slots, then run `work` after any request of
|
|
222
|
+
* theirs already running; `work` is handed the `issue` that fills the slot.
|
|
223
|
+
*
|
|
224
|
+
* The slot is taken AT ONCE, before the turn is waited for, and counts as an
|
|
225
|
+
* open request until `work` ends: a caller at the cap is refused before the
|
|
226
|
+
* server spends a zip's worth of work on it, and ten calls in parallel cannot
|
|
227
|
+
* all pass the count and then all build. A call that ends without issuing
|
|
228
|
+
* (nothing included, a refusal, a failure) gives its slot back.
|
|
229
|
+
*
|
|
230
|
+
* The build AND the write of its bytes run in the turn: a build reads every
|
|
231
|
+
* included file into memory and may zip up to the download limit, so one
|
|
232
|
+
* request at a time per user bounds what one caller can make the process
|
|
233
|
+
* hold or the disk stage. Different users do not wait on each other.
|
|
234
|
+
*
|
|
235
|
+
* The ONE way a request is issued: there is no public `issue` to call on
|
|
236
|
+
* the side. `work`'s `issue` fills the slot once — a second call would be
|
|
237
|
+
* a second request on one slot, past the cap — and a `withRequestSlot` for
|
|
238
|
+
* the same user from inside `work` throws rather than wait for the turn it
|
|
239
|
+
* is itself holding, which would never come.
|
|
240
|
+
*/
|
|
241
|
+
async withRequestSlot<T>(
|
|
242
|
+
user: { id: string },
|
|
243
|
+
work: (issue: (items: DownloadArtifact[]) => Promise<IssuedDownload>) => Promise<T>,
|
|
244
|
+
): Promise<T> {
|
|
245
|
+
const held = this.inTurn.getStore();
|
|
246
|
+
if (held?.has(user.id)) {
|
|
247
|
+
throw new Error('A download request slot was asked for inside one already held for the same user.');
|
|
248
|
+
}
|
|
249
|
+
this.assertCanIssue(user);
|
|
250
|
+
this.reserved.set(user.id, (this.reserved.get(user.id) ?? 0) + 1);
|
|
251
|
+
// Once the request is recorded, the record is what counts as open and the
|
|
252
|
+
// reservation is given back by `write` — not here as well, which would
|
|
253
|
+
// count the request twice while `work` finishes.
|
|
254
|
+
let issued = false;
|
|
255
|
+
try {
|
|
256
|
+
const issue = (items: DownloadArtifact[]): Promise<IssuedDownload> => {
|
|
257
|
+
if (issued) return Promise.reject(new Error('A download request slot issues once.'));
|
|
258
|
+
issued = true;
|
|
259
|
+
return this.write(user, items);
|
|
260
|
+
};
|
|
261
|
+
const turn = new Set(held ?? []).add(user.id);
|
|
262
|
+
const before = this.building.get(user.id) ?? Promise.resolve();
|
|
263
|
+
const mine = before.catch(() => undefined).then(() => this.inTurn.run(turn, () => work(issue)));
|
|
264
|
+
this.building.set(user.id, mine);
|
|
265
|
+
try {
|
|
266
|
+
return await mine;
|
|
267
|
+
} finally {
|
|
268
|
+
if (this.building.get(user.id) === mine) this.building.delete(user.id);
|
|
269
|
+
}
|
|
270
|
+
} finally {
|
|
271
|
+
if (!issued) this.releaseReservation(user.id);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Give back one of `userId`'s reserved slots. */
|
|
276
|
+
private releaseReservation(userId: string): void {
|
|
277
|
+
const left = (this.reserved.get(userId) ?? 1) - 1;
|
|
278
|
+
if (left > 0) this.reserved.set(userId, left);
|
|
279
|
+
else this.reserved.delete(userId);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Store every artifact of one request and answer a link per artifact, all
|
|
284
|
+
* sharing the request's expiry, within a slot already held: the slot is the
|
|
285
|
+
* capacity check. Nothing is issued unless every artifact is on disk: a
|
|
286
|
+
* request whose bytes could not all be written leaves nothing.
|
|
287
|
+
*/
|
|
288
|
+
private async write(user: { id: string }, items: DownloadArtifact[]): Promise<IssuedDownload> {
|
|
289
|
+
if (items.length === 0) throw new Error('A download request must carry at least one artifact.');
|
|
290
|
+
const requestId = `download-${Date.now()}-${randomBytes(8).toString('hex')}`;
|
|
291
|
+
const dir = path.join(this.root, requestId);
|
|
292
|
+
const minted: { key: string; token: string; record: ArtifactRecord }[] = [];
|
|
293
|
+
// Reserved BEFORE the first filesystem await: a sweep running while a slow
|
|
294
|
+
// disk takes these writes would otherwise find a directory no request
|
|
295
|
+
// names and, past the age bound, reclaim it under the write.
|
|
296
|
+
this.writing.add(requestId);
|
|
297
|
+
try {
|
|
298
|
+
await fs.mkdir(dir, { recursive: true });
|
|
299
|
+
for (let i = 0; i < items.length; i++) {
|
|
300
|
+
const item = items[i]!;
|
|
301
|
+
const file = String(i);
|
|
302
|
+
await fs.writeFile(path.join(dir, file), item.data);
|
|
303
|
+
const token = this.tokenPrefix + randomBytes(32).toString('base64url');
|
|
304
|
+
minted.push({
|
|
305
|
+
key: hash(token),
|
|
306
|
+
token,
|
|
307
|
+
record: {
|
|
308
|
+
requestId,
|
|
309
|
+
file,
|
|
310
|
+
contentType: item.contentType,
|
|
311
|
+
filename: item.filename,
|
|
312
|
+
bytes: item.data.byteLength,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
try {
|
|
318
|
+
await this.removeDir(requestId);
|
|
319
|
+
} finally {
|
|
320
|
+
this.writing.delete(requestId);
|
|
321
|
+
// Nothing issued: the slot goes back here, since `withRequestSlot`
|
|
322
|
+
// treats an `issue` that was called as one that filled its slot.
|
|
323
|
+
this.releaseReservation(user.id);
|
|
324
|
+
}
|
|
325
|
+
throw err;
|
|
326
|
+
}
|
|
327
|
+
// The TTL runs from the moment the links exist, not from when the build
|
|
328
|
+
// started: a 500 MB build must not hand out links with minutes already gone.
|
|
329
|
+
const expiresAt = Date.now() + this.ttlMs;
|
|
330
|
+
const request: RequestRecord = { id: requestId, userId: user.id, expiresAt, pending: new Set() };
|
|
331
|
+
for (const { key, record } of minted) {
|
|
332
|
+
this.artifacts.set(key, record);
|
|
333
|
+
request.pending.add(key);
|
|
334
|
+
}
|
|
335
|
+
this.requests.set(requestId, request);
|
|
336
|
+
this.writing.delete(requestId);
|
|
337
|
+
// The record counts as open from here; the reservation that held the
|
|
338
|
+
// place until now is given back, so the request is counted once.
|
|
339
|
+
this.releaseReservation(user.id);
|
|
340
|
+
// AFTER the records exist: the first sweep runs at once and would
|
|
341
|
+
// otherwise take this request's directory for an orphan. And never once
|
|
342
|
+
// the store is stopped: a request finishing after shutdown's
|
|
343
|
+
// `stopSweeping` must not re-arm the sweeper that was stopped for good.
|
|
344
|
+
if (!this.stopped) this.startSweeping();
|
|
345
|
+
return {
|
|
346
|
+
downloadUrls: minted.map((m) => this.downloadUrlFor(m.token)),
|
|
347
|
+
expiresAt: new Date(expiresAt).toISOString(),
|
|
348
|
+
expiresInSeconds: Math.round(this.ttlMs / 1000),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Take `token`'s link for ONE fetch and answer what to send. Spent the
|
|
354
|
+
* moment it is claimed, before a byte goes out: a second fetch arriving
|
|
355
|
+
* while the first is still sending is refused, not served twice.
|
|
356
|
+
*
|
|
357
|
+
* `fetcherIds` are the users the fetch identified itself as, by every
|
|
358
|
+
* credential it carried — usually none. The link works for anyone holding
|
|
359
|
+
* it — an agent's `curl` carries no session — but a fetch that says it is
|
|
360
|
+
* SOMEONE ELSE, by any of its credentials, is refused: that user holds a
|
|
361
|
+
* link issued to another.
|
|
362
|
+
*/
|
|
363
|
+
claim(token: string, fetcherIds: readonly string[] = []): ClaimedDownload {
|
|
364
|
+
const key = hash(token);
|
|
365
|
+
const record = this.artifacts.get(key);
|
|
366
|
+
if (!record || record.claimedAt !== undefined) throw refusal();
|
|
367
|
+
const request = this.requests.get(record.requestId);
|
|
368
|
+
if (!request) throw refusal();
|
|
369
|
+
if (request.expiresAt <= Date.now()) {
|
|
370
|
+
void this.forget(key);
|
|
371
|
+
throw refusal();
|
|
372
|
+
}
|
|
373
|
+
if (fetcherIds.some((id) => id !== request.userId)) throw refusal();
|
|
374
|
+
record.claimedAt = Date.now();
|
|
375
|
+
return {
|
|
376
|
+
absolutePath: path.join(this.root, record.requestId, record.file),
|
|
377
|
+
contentType: record.contentType,
|
|
378
|
+
filename: record.filename,
|
|
379
|
+
bytes: record.bytes,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The fetch of `token` has ended — sent in full, failed, or the client went
|
|
385
|
+
* away. The bytes go either way: a link answers once, so nothing will ever
|
|
386
|
+
* read them again. Idempotent.
|
|
387
|
+
*/
|
|
388
|
+
async finish(token: string): Promise<void> {
|
|
389
|
+
const key = hash(token);
|
|
390
|
+
const record = this.artifacts.get(key);
|
|
391
|
+
if (!record) return;
|
|
392
|
+
await this.forget(key);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* How many requests `userId` holds open: unexpired with a link unfetched, or
|
|
397
|
+
* with a fetch still sending — and the slots of calls still under way.
|
|
398
|
+
*/
|
|
399
|
+
openRequestsOf(userId: string): number {
|
|
400
|
+
const now = Date.now();
|
|
401
|
+
let open = this.reserved.get(userId) ?? 0;
|
|
402
|
+
for (const request of this.requests.values()) {
|
|
403
|
+
if (request.userId !== userId) continue;
|
|
404
|
+
if (request.expiresAt > now || this.sending(request, now)) open += 1;
|
|
405
|
+
}
|
|
406
|
+
return open;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Delete every request whose links have expired, with its bytes — then any
|
|
411
|
+
* directory in the root that no live request names and that has outlived
|
|
412
|
+
* every link that could name it (a process killed between issue and fetch
|
|
413
|
+
* leaves one no map will ever mention again; by AGE, because during a
|
|
414
|
+
* restart two processes share the root, as the upload store explains).
|
|
415
|
+
*/
|
|
416
|
+
async sweepNow(): Promise<void> {
|
|
417
|
+
if (this.stopped) return;
|
|
418
|
+
const now = Date.now();
|
|
419
|
+
for (const request of [...this.requests.values()]) {
|
|
420
|
+
if (request.expiresAt > now) continue;
|
|
421
|
+
if (this.sending(request, now)) continue;
|
|
422
|
+
await this.dropRequest(request);
|
|
423
|
+
}
|
|
424
|
+
let names: string[];
|
|
425
|
+
try {
|
|
426
|
+
names = await fs.readdir(this.root);
|
|
427
|
+
} catch {
|
|
428
|
+
return; // root not created yet — nothing to reclaim
|
|
429
|
+
}
|
|
430
|
+
// Read AFTER the listing, so a request issued while it was read is named.
|
|
431
|
+
const live = new Set(this.requests.keys());
|
|
432
|
+
for (const name of names) {
|
|
433
|
+
if (this.stopped) return;
|
|
434
|
+
if (live.has(name) || this.writing.has(name)) continue;
|
|
435
|
+
if (await this.outlivedEveryLink(name, now)) await this.removeDir(name);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Sweep now, and keep sweeping. Started by the first request issued; idempotent; `unref`'d. */
|
|
440
|
+
startSweeping(intervalMs: number = SWEEP_INTERVAL_MS): void {
|
|
441
|
+
if (this.sweepTimer) return;
|
|
442
|
+
this.stopped = false;
|
|
443
|
+
this.sweep();
|
|
444
|
+
this.sweepTimer = setInterval(() => this.sweep(), intervalMs);
|
|
445
|
+
this.sweepTimer.unref?.();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Stop sweeping for good — see `AgentUploadStore.stopSweeping` for why a stopped graph must. */
|
|
449
|
+
stopSweeping(): void {
|
|
450
|
+
this.stopped = true;
|
|
451
|
+
if (this.sweepTimer) clearInterval(this.sweepTimer);
|
|
452
|
+
this.sweepTimer = null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Wait for the sweep in flight, if any. Pairs with {@link stopSweeping} on shutdown. */
|
|
456
|
+
async drainSweep(): Promise<void> {
|
|
457
|
+
await this.sweeping;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private sweep(): void {
|
|
461
|
+
// One sweep at a time: a second starting while the first still deletes
|
|
462
|
+
// would make `drainSweep` wait for the newer and miss the older.
|
|
463
|
+
if (this.sweeping) return;
|
|
464
|
+
const running = this.sweepNow()
|
|
465
|
+
.catch((err: unknown) => {
|
|
466
|
+
log.warn('could not sweep expired downloads:', { err });
|
|
467
|
+
})
|
|
468
|
+
.finally(() => {
|
|
469
|
+
if (this.sweeping === running) this.sweeping = null;
|
|
470
|
+
});
|
|
471
|
+
this.sweeping = running;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Whether a fetch of one of `request`'s links is still sending, within its grace. */
|
|
475
|
+
private sending(request: RequestRecord, now: number): boolean {
|
|
476
|
+
for (const key of request.pending) {
|
|
477
|
+
const claimedAt = this.artifacts.get(key)?.claimedAt;
|
|
478
|
+
if (claimedAt !== undefined && now - claimedAt < SEND_GRACE_MS) return true;
|
|
479
|
+
}
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Drop one artifact's link and bytes; the request goes with its last one. */
|
|
484
|
+
private async forget(key: string): Promise<void> {
|
|
485
|
+
const record = this.artifacts.get(key);
|
|
486
|
+
if (!record) return;
|
|
487
|
+
this.artifacts.delete(key);
|
|
488
|
+
const request = this.requests.get(record.requestId);
|
|
489
|
+
request?.pending.delete(key);
|
|
490
|
+
if (request && request.pending.size === 0) {
|
|
491
|
+
this.requests.delete(request.id);
|
|
492
|
+
await this.removeDir(request.id);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
await fs.rm(path.join(this.root, record.requestId, record.file), { force: true });
|
|
497
|
+
} catch (err) {
|
|
498
|
+
log.warn(`could not delete a fetched download of "${record.requestId}":`, { err });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private async dropRequest(request: RequestRecord): Promise<void> {
|
|
503
|
+
for (const key of request.pending) this.artifacts.delete(key);
|
|
504
|
+
this.requests.delete(request.id);
|
|
505
|
+
await this.removeDir(request.id);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private async outlivedEveryLink(name: string, now: number): Promise<boolean> {
|
|
509
|
+
try {
|
|
510
|
+
const { mtimeMs } = await fs.stat(path.join(this.root, name));
|
|
511
|
+
return now - mtimeMs > this.ttlMs + SEND_GRACE_MS;
|
|
512
|
+
} catch {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Best-effort delete of one request's directory. Never throws. */
|
|
518
|
+
private async removeDir(id: string): Promise<void> {
|
|
519
|
+
try {
|
|
520
|
+
await fs.rm(path.join(this.root, id), { recursive: true, force: true });
|
|
521
|
+
} catch (err) {
|
|
522
|
+
log.warn(`could not delete the stored download "${id}":`, { err });
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function refusal(): DownloadTokenError {
|
|
528
|
+
return new DownloadTokenError(DOWNLOAD_TOKEN_REFUSAL, 404);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function hash(token: string): string {
|
|
532
|
+
return createHash('sha256').update(token).digest('hex');
|
|
533
|
+
}
|
|
@@ -260,8 +260,10 @@ export class AgentUploadStore {
|
|
|
260
260
|
});
|
|
261
261
|
// AFTER the record exists, not before. The first sweep runs the moment the
|
|
262
262
|
// timer starts, and started from an empty map it would be a sweep that
|
|
263
|
-
// believes nothing is live — this token's own id included.
|
|
264
|
-
|
|
263
|
+
// believes nothing is live — this token's own id included. And never once
|
|
264
|
+
// the store is stopped: a token issued after shutdown's `stopSweeping`
|
|
265
|
+
// must not re-arm the sweeper that was stopped for good.
|
|
266
|
+
if (!this.stopped) this.startSweeping();
|
|
265
267
|
return {
|
|
266
268
|
uploadUrl: this.uploadUrlFor(token),
|
|
267
269
|
token,
|
|
@@ -621,25 +623,39 @@ function hash(token: string): string {
|
|
|
621
623
|
* Both directions and both spellings: equal paths, either containing the
|
|
622
624
|
* other, and the real paths, so a root that is a LINK into the workspaces tree
|
|
623
625
|
* is refused too.
|
|
626
|
+
*
|
|
627
|
+
* `what` names the root being checked, for a boot failure that sends the
|
|
628
|
+
* operator to the right setting: the upload root by default; the download
|
|
629
|
+
* root (derived from it) says so itself.
|
|
624
630
|
*/
|
|
625
631
|
export async function assertUploadsRootOutsideWorkspaces(
|
|
626
632
|
uploadsRoot: string,
|
|
627
633
|
workspacesRoot: string,
|
|
634
|
+
what: StagingRootDescription = UPLOADS_ROOT_DESCRIPTION,
|
|
628
635
|
): Promise<void> {
|
|
629
636
|
for (const [uploads, workspaces] of [
|
|
630
637
|
[path.resolve(uploadsRoot), path.resolve(workspacesRoot)],
|
|
631
638
|
[await realBase(uploadsRoot), await realBase(workspacesRoot)],
|
|
632
639
|
]) {
|
|
633
640
|
if (uploads === workspaces || contains(workspaces, uploads) || contains(uploads, workspaces)) {
|
|
634
|
-
throw new Error(
|
|
635
|
-
`AGENT_UPLOADS_ROOT ("${uploadsRoot}") must be outside WORKSPACES_ROOT ("${workspacesRoot}"): uploaded ` +
|
|
636
|
-
'bytes are staged there before any access or platform-file rule has judged them, so a root inside a ' +
|
|
637
|
-
'workspace would let the file tools read them. Point it at a sibling directory.',
|
|
638
|
-
);
|
|
641
|
+
throw new Error(`${what.name} ("${uploadsRoot}") must be outside WORKSPACES_ROOT ("${workspacesRoot}"): ${what.why}`);
|
|
639
642
|
}
|
|
640
643
|
}
|
|
641
644
|
}
|
|
642
645
|
|
|
646
|
+
/** A staging root {@link assertUploadsRootOutsideWorkspaces} checks: what it is called, and why and how to fix it. */
|
|
647
|
+
export interface StagingRootDescription {
|
|
648
|
+
name: string;
|
|
649
|
+
why: string;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const UPLOADS_ROOT_DESCRIPTION: StagingRootDescription = {
|
|
653
|
+
name: 'AGENT_UPLOADS_ROOT',
|
|
654
|
+
why:
|
|
655
|
+
'uploaded bytes are staged there before any access or platform-file rule has judged them, so a root inside a ' +
|
|
656
|
+
'workspace would let the file tools read them. Point it at a sibling directory.',
|
|
657
|
+
};
|
|
658
|
+
|
|
643
659
|
/** Whether `child` is inside `parent`. Paths already resolved. */
|
|
644
660
|
function contains(parent: string, child: string): boolean {
|
|
645
661
|
return child.startsWith(parent.endsWith(path.sep) ? parent : parent + path.sep);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The content type a raw route declares for bytes a renderer fetches to
|
|
3
|
+
* draw INLINE — the app's `/raw` file route and the embed's raw route read
|
|
4
|
+
* it from here, so a picture or a document the app shows is the same type
|
|
5
|
+
* inside a chat. Anything outside this table is `application/octet-stream`:
|
|
6
|
+
* bytes the browser is told nothing about, under `nosniff`, so a file the
|
|
7
|
+
* table does not name can never be promoted to active content.
|
|
8
|
+
*
|
|
9
|
+
* SVG is listed because the renderers draw it through `<img>`, where its
|
|
10
|
+
* scripts never run; a route that serves it as a DOCUMENT sandboxes it (see
|
|
11
|
+
* the routes), and a download of it is forced to octet-stream there.
|
|
12
|
+
*/
|
|
13
|
+
const RAW_INLINE_MIME: Record<string, string> = {
|
|
14
|
+
'.png': 'image/png',
|
|
15
|
+
'.jpg': 'image/jpeg',
|
|
16
|
+
'.jpeg': 'image/jpeg',
|
|
17
|
+
'.gif': 'image/gif',
|
|
18
|
+
'.webp': 'image/webp',
|
|
19
|
+
'.svg': 'image/svg+xml',
|
|
20
|
+
'.bmp': 'image/bmp',
|
|
21
|
+
'.ico': 'image/x-icon',
|
|
22
|
+
'.avif': 'image/avif',
|
|
23
|
+
'.pdf': 'application/pdf',
|
|
24
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
25
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const OCTET_STREAM = 'application/octet-stream';
|
|
29
|
+
|
|
30
|
+
/** The lower-cased extension of `path`, dot included; `''` when it has none. */
|
|
31
|
+
export function rawExtensionOf(path: string): string {
|
|
32
|
+
const dot = path.lastIndexOf('.');
|
|
33
|
+
const slash = path.lastIndexOf('/');
|
|
34
|
+
return dot > slash ? path.slice(dot).toLowerCase() : '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The inline content type for `path`'s extension, `application/octet-stream` when the table has none. */
|
|
38
|
+
export function rawMimeFor(path: string): string {
|
|
39
|
+
return RAW_INLINE_MIME[rawExtensionOf(path)] ?? OCTET_STREAM;
|
|
40
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isTextBytes } from '../../../shared/utf8-text.js';
|
|
2
2
|
import { fileExtension } from './doc-extract.types.js';
|
|
3
3
|
import { displayPath, type FileKind, type FileReader, type ReadResult } from './file-reader.js';
|
|
4
4
|
|
|
@@ -64,14 +64,11 @@ export function extensionMime(path: string): string | undefined {
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Text is what the fallback reader may hand to the text tools: no NUL byte
|
|
67
|
-
* AND valid UTF-8
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* is refused without allocating its full decoded string first.
|
|
67
|
+
* AND valid UTF-8 — the one policy in `shared/utf8-text.ts`, which the
|
|
68
|
+
* seeder and the starter packs read as well. Re-exported here for the
|
|
69
|
+
* readers' callers.
|
|
71
70
|
*/
|
|
72
|
-
export
|
|
73
|
-
return !bytes.includes(0) && isUtf8(bytes);
|
|
74
|
-
}
|
|
71
|
+
export { isTextBytes };
|
|
75
72
|
|
|
76
73
|
/**
|
|
77
74
|
* The DEFAULT reader — the registry's fallback for every extension no other
|