@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
|
@@ -5,7 +5,7 @@ import AdmZip from 'adm-zip';
|
|
|
5
5
|
import type { Router, RequestHandler } from 'express';
|
|
6
6
|
import type { LocalFilesystem } from '@mastra/core/workspace';
|
|
7
7
|
import type { IToolRegistry, JsonSchema } from '../tool-registry/tool.contract.js';
|
|
8
|
-
import { ToolError, type ToolContext, type ToolHandler } from '../tool-helpers/tool.contract.js';
|
|
8
|
+
import { hasHttpStatus, ToolError, type ToolContext, type ToolHandler } from '../tool-helpers/tool.contract.js';
|
|
9
9
|
import { BRANCH_INPUT, toolDef } from '../tool-helpers/tool-def.js';
|
|
10
10
|
import {
|
|
11
11
|
notifyAgentRead,
|
|
@@ -17,7 +17,7 @@ import type { IRoutineWritePolicy } from './routine-write-policy.js';
|
|
|
17
17
|
import type { ToolHandlerFactory } from '../tool-helpers/tool-handler.js';
|
|
18
18
|
import { requireInternalSource, requireExternalSource } from '../tool-auth/tool-auth.middleware.js';
|
|
19
19
|
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
20
|
-
import {
|
|
20
|
+
import { GitInternalsError, WorkflowValidationError } from '../../shared/domain-errors.js';
|
|
21
21
|
// Leaf-level shared primitive (same exception `workspace.service.ts` already
|
|
22
22
|
// relies on) — not a workflow service, so this stays inside the module boundary.
|
|
23
23
|
import { assertValidBranchName } from '../kb-fs/branch-name.js';
|
|
@@ -27,19 +27,20 @@ import {
|
|
|
27
27
|
assertRepoRootNameFreeArgs,
|
|
28
28
|
isInsideRepo,
|
|
29
29
|
normalizePathArgs,
|
|
30
|
+
normalizeWorkspacePath,
|
|
30
31
|
} from '../kb-fs/repo-path.js';
|
|
31
32
|
import { GitGuardedFilesystem } from '../kb-fs/git-guarded-filesystem.js';
|
|
32
33
|
import { assertNoGitInternalsSegment, assertNotGitInternals, hasGitInternalsSegment } from '../../shared/git-internals.js';
|
|
33
34
|
import { isRolesYamlPath } from '../access-model/roles-yaml-guard.js';
|
|
34
35
|
import type { ISessionSink } from './session-sink.js';
|
|
35
|
-
import { isAbsence } from '../../shared/fs.contract.js';
|
|
36
|
+
import { isAbsence, type ITreeWalker } from '../../shared/fs.contract.js';
|
|
36
37
|
import type { AccessDecisionSource, AccessTargetKind, IAccessControl } from '../access/access-control.interface.js';
|
|
37
38
|
import { accessRoster, resolveAccessView } from '../access/access-view.js';
|
|
38
39
|
import { accessMdPathForFolder, fileCarriesAccessRules, governingFolderOf } from '../access/access-mutation.service.js';
|
|
39
40
|
import { toKbRelative, resolveReadableMap } from '../access-model/kb-read-filter.js';
|
|
40
41
|
import type { SpillStore } from './spill-store.js';
|
|
41
42
|
import type { DocExtractService } from './file-readers/doc-extract.service.js';
|
|
42
|
-
import { displayPath, type FileKind, type FileReaderRegistry } from './file-readers/file-reader.js';
|
|
43
|
+
import { displayPath, type FileKind, type FileReaderRegistry, type ReadResult } from './file-readers/file-reader.js';
|
|
43
44
|
import { fileTypeOf, needsContent } from './file-readers/content-mode.js';
|
|
44
45
|
import { createFileReaderRegistry } from './file-readers/file-reader.registry.js';
|
|
45
46
|
import { DocumentReader } from './file-readers/document-reader.js';
|
|
@@ -53,6 +54,8 @@ import {
|
|
|
53
54
|
platformFileRefusal,
|
|
54
55
|
platformFileUploadRefusal,
|
|
55
56
|
platformFolderRefusal,
|
|
57
|
+
isRepositoryOwnFile,
|
|
58
|
+
repositoryOwnFileDeleteRefusal,
|
|
56
59
|
entryExistsMessage,
|
|
57
60
|
type ExistingEntryKind,
|
|
58
61
|
} from '@bevel-software/platform-shared';
|
|
@@ -66,6 +69,9 @@ import {
|
|
|
66
69
|
type AgentGuideReader,
|
|
67
70
|
} from '../agent-guide/agent-guide.js';
|
|
68
71
|
import { removeEmptyDirs } from './empty-dirs.js';
|
|
72
|
+
import { FIRST_RUN_SECTION_ID, firstRunNote, knowledgeFolderIsNew, type FirstRunStarterSource } from './first-run.js';
|
|
73
|
+
import { planMoveLinks } from './move-links.js';
|
|
74
|
+
import { MoveLockedError, MoveRacedError, type LockingFilesystem } from '../kb-fs/locking-filesystem.js';
|
|
69
75
|
import { rethrowAsWriteDenial } from './write-denial.js';
|
|
70
76
|
import type { IChangeReadGate } from '../access-model/change-gate.js';
|
|
71
77
|
import { notFound, orDeclaredNotFound, orNotFound } from './not-found.js';
|
|
@@ -73,6 +79,9 @@ import { logger } from '../../shared/logging.js';
|
|
|
73
79
|
import { printable } from '../../shared/printable.js';
|
|
74
80
|
import { DestinationTakenError, inspectDestination } from '../../shared/rename-no-replace.js';
|
|
75
81
|
import { AgentUploadStore, type ClaimedUpload } from './agent-upload.store.js';
|
|
82
|
+
import type { IAgentDownloadStore } from './agent-download.store.js';
|
|
83
|
+
import { buildDownload } from './agent-download.builder.js';
|
|
84
|
+
import { DOWNLOAD_MAX_FILES, ZIP_DOWNLOAD_MAX_BYTES } from './workspace.service.js';
|
|
76
85
|
import {
|
|
77
86
|
isSymlinkZipEntry,
|
|
78
87
|
isZipNoiseEntry,
|
|
@@ -84,6 +93,9 @@ import {
|
|
|
84
93
|
|
|
85
94
|
const log = logger('workspace-tools');
|
|
86
95
|
|
|
96
|
+
/** Types that run scripts wherever they are opened, sent by `request_file_download` as plain bytes. */
|
|
97
|
+
const ACTIVE_CONTENT_TYPES = new Set(['image/svg+xml', 'text/html', 'application/xhtml+xml']);
|
|
98
|
+
|
|
87
99
|
/** The caller's verdict per access verb on one path. */
|
|
88
100
|
interface AccessVerbs {
|
|
89
101
|
read: boolean;
|
|
@@ -817,13 +829,40 @@ const BATCH_SAVE_WARNINGS_OUTPUT: JsonSchema = {
|
|
|
817
829
|
* the tools were built has to be written here rather than onto the shared
|
|
818
830
|
* `SESSION_ID_INPUT` constant.
|
|
819
831
|
*/
|
|
820
|
-
function sessionIdInputOf(def: { inputs?: unknown }): { description?: string } | undefined {
|
|
832
|
+
export function sessionIdInputOf(def: { inputs?: unknown }): { description?: string } | undefined {
|
|
821
833
|
const inputs = def.inputs as
|
|
822
834
|
| { properties?: { body?: { properties?: Record<string, { description?: string }> } } }
|
|
823
835
|
| undefined;
|
|
824
836
|
return inputs?.properties?.body?.properties?.sessionId;
|
|
825
837
|
}
|
|
826
838
|
|
|
839
|
+
/**
|
|
840
|
+
* A read of ONE workspace file, exactly as `read_file` performs it: the read
|
|
841
|
+
* hook, the access gate, the not-found refusal and the per-extension reader,
|
|
842
|
+
* in that order. Rejects with the same `ToolError`s `read_file` rejects with.
|
|
843
|
+
*
|
|
844
|
+
* `offset`/`limit` and the `__tool_chain_spill__/…` ref are deliberately NOT
|
|
845
|
+
* here: they are `read_file`'s own arguments, not part of what reading a file
|
|
846
|
+
* means.
|
|
847
|
+
*/
|
|
848
|
+
export type ReadForTool = (
|
|
849
|
+
branch: string,
|
|
850
|
+
path: string,
|
|
851
|
+
ctx: ToolContext,
|
|
852
|
+
) => Promise<ReadResult>;
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* What the workspace tool registration hands back for another module to build
|
|
856
|
+
* on, rather than re-deriving.
|
|
857
|
+
*
|
|
858
|
+
* One member today, and it is the only kind of thing that belongs here: a
|
|
859
|
+
* behaviour the platform promises TWICE in the same words (`open_page`
|
|
860
|
+
* answers "the way `read_file` does") and must therefore implement once.
|
|
861
|
+
*/
|
|
862
|
+
export interface WorkspaceToolsPorts {
|
|
863
|
+
readForTool: ReadForTool;
|
|
864
|
+
}
|
|
865
|
+
|
|
827
866
|
/**
|
|
828
867
|
* Workspace domain tools: the file primitives (replacing Mastra's auto-injected
|
|
829
868
|
* Workspace tools) + unzip. Most just re-expose the SAME `LocalFilesystem`
|
|
@@ -875,7 +914,27 @@ export function registerWorkspaceTools(
|
|
|
875
914
|
* tools read the disk and nothing else.
|
|
876
915
|
*/
|
|
877
916
|
agentGuide?: AgentGuideReader,
|
|
878
|
-
|
|
917
|
+
/**
|
|
918
|
+
* The download-link store behind `request_file_download` — the way an agent
|
|
919
|
+
* takes files OUT without their content passing through the model. Optional
|
|
920
|
+
* for the harnesses about the file primitives; without it the tool is not
|
|
921
|
+
* mounted.
|
|
922
|
+
*/
|
|
923
|
+
downloads?: IAgentDownloadStore,
|
|
924
|
+
/**
|
|
925
|
+
* The starter pack the knowledge base was filled from, if any (see
|
|
926
|
+
* `modules/onboarding`): its untouched pages do not end the `firstRun`
|
|
927
|
+
* note, and the note names the pages it suggests. Optional; without it the
|
|
928
|
+
* note reads the knowledge folder alone.
|
|
929
|
+
*/
|
|
930
|
+
starterPacks?: FirstRunStarterSource,
|
|
931
|
+
/**
|
|
932
|
+
* The one tree walk (see `shared/fs.contract.ts`), for the `firstRun`
|
|
933
|
+
* note's look at the knowledge folder. Optional for the harnesses about the
|
|
934
|
+
* file primitives; without it `start_session` answers the id alone.
|
|
935
|
+
*/
|
|
936
|
+
disk?: ITreeWalker,
|
|
937
|
+
): WorkspaceToolsPorts {
|
|
879
938
|
const { kbDirName } = kb;
|
|
880
939
|
/**
|
|
881
940
|
* The one extension→reader registry every read-shaped decision routes
|
|
@@ -1243,6 +1302,24 @@ export function registerWorkspaceTools(
|
|
|
1243
1302
|
return rel !== null && isPlatformFolder(rel, kb.layout) ? platformFolderRefusal(rel) : undefined;
|
|
1244
1303
|
};
|
|
1245
1304
|
|
|
1305
|
+
/**
|
|
1306
|
+
* Why `delete_file` may not delete the FILE at `path` whoever asks, or
|
|
1307
|
+
* undefined when the caller's write access decides. Narrower than
|
|
1308
|
+
* {@link managedReason}: a nested `access.md` never moves, but whoever may
|
|
1309
|
+
* write it may delete it — as in the app — and its folder then follows its
|
|
1310
|
+
* parent's rules. The root's `access.md` and `roles.yaml` are deleted by
|
|
1311
|
+
* nobody. Judged on the on-disk spelling, as `managedReason` is.
|
|
1312
|
+
*/
|
|
1313
|
+
const fileDeleteRefusal = (path: string): string | undefined => {
|
|
1314
|
+
const norm = path.replace(/^\.?\/+/, '').replace(/\/+$/, '');
|
|
1315
|
+
if (isGitMetadata(norm)) return managedReason(norm, 'file');
|
|
1316
|
+
const rel = toKbRelative(norm, kbDirName);
|
|
1317
|
+
if (rel === null || !isPlatformFile(rel, kb.layout)) return undefined;
|
|
1318
|
+
if (isRepositoryOwnFile(rel, kb.layout)) return repositoryOwnFileDeleteRefusal(rel);
|
|
1319
|
+
const name = rel.slice(rel.lastIndexOf('/') + 1);
|
|
1320
|
+
return name === 'access.md' ? undefined : `${name} is a platform file and cannot be deleted through the agent tools.`;
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1246
1323
|
/** The workspace root on disk for `branch`. */
|
|
1247
1324
|
const workspaceRoot = (branch: string, ctx: ToolContext): Promise<string> =>
|
|
1248
1325
|
ctx.workspaceService.getWorkspacePath(workspaceIdForBranch(branch));
|
|
@@ -1480,6 +1557,12 @@ export function registerWorkspaceTools(
|
|
|
1480
1557
|
* flag adds nothing to what the agent reads.
|
|
1481
1558
|
*/
|
|
1482
1559
|
gated?: boolean;
|
|
1560
|
+
/**
|
|
1561
|
+
* Refuse a read-only credential, as a write tool is refused, WITHOUT being
|
|
1562
|
+
* a write: the read-only-deployment gate does not apply. For a read a
|
|
1563
|
+
* read-only key may still not make (`request_file_download`).
|
|
1564
|
+
*/
|
|
1565
|
+
writeScope?: boolean;
|
|
1483
1566
|
/**
|
|
1484
1567
|
* This tool resolves `branch` ITSELF and must not be pre-checked here.
|
|
1485
1568
|
* Only `execute_command` sets it: for an internal session that leaves the
|
|
@@ -1489,6 +1572,8 @@ export function registerWorkspaceTools(
|
|
|
1489
1572
|
* handler. Every other tool takes the check below.
|
|
1490
1573
|
*/
|
|
1491
1574
|
resolvesBranchItself?: boolean;
|
|
1575
|
+
/** Arguments this tool refuses by name itself, with its own wording (see `ToolDefSpec.refusesItself`). */
|
|
1576
|
+
refusesItself?: string[];
|
|
1492
1577
|
handler: ToolHandler;
|
|
1493
1578
|
}): void => {
|
|
1494
1579
|
const path = `/api/agent/tools/${spec.name}`;
|
|
@@ -1500,12 +1585,6 @@ export function registerWorkspaceTools(
|
|
|
1500
1585
|
// description from the END, where what is specific to the tool sits. The
|
|
1501
1586
|
// rules themselves are in the handshake instructions and in the managed
|
|
1502
1587
|
// guide (see `shared-file-rules.ts`), stated once and from one text.
|
|
1503
|
-
// Whether a call to this tool MUST name a branch, read off the tool's own
|
|
1504
|
-
// declaration rather than assumed of the family. Every tool mounted here
|
|
1505
|
-
// requires `branch` today; keying on the schema means a tool that declares
|
|
1506
|
-
// it optional (and resolves absence itself, as `list_tool_setup` does on its
|
|
1507
|
-
// own route) is not handed a refusal it never asked for.
|
|
1508
|
-
const requiresBranch = ((spec.inputs as { required?: string[] }).required ?? []).includes('branch');
|
|
1509
1588
|
const describe = (): string =>
|
|
1510
1589
|
(typeof spec.description === 'function' ? spec.description() : spec.description) +
|
|
1511
1590
|
(spec.gated ? agentAccessGate.notes.gatedToolNote() : '');
|
|
@@ -1515,8 +1594,14 @@ export function registerWorkspaceTools(
|
|
|
1515
1594
|
path,
|
|
1516
1595
|
inputs: spec.inputs,
|
|
1517
1596
|
outputs: spec.outputs,
|
|
1597
|
+
refusesItself: spec.refusesItself,
|
|
1518
1598
|
tags: spec.write ? ['workspace', 'write'] : ['workspace'],
|
|
1519
1599
|
});
|
|
1600
|
+
// Every tool here declares `branch` required in its inputs, which is how
|
|
1601
|
+
// `toolDef` records it: the tool handler refuses a branch-less call before
|
|
1602
|
+
// the path work below and before any handler. Most of these tools would
|
|
1603
|
+
// meet the same refusal one layer down at `getFilesystem`, but not all —
|
|
1604
|
+
// `unzip` hands `branch` straight to the workspace service by id.
|
|
1520
1605
|
registry.registerInternalTool(def);
|
|
1521
1606
|
if (!spec.internalOnly) registry.registerExternalTool(def);
|
|
1522
1607
|
/**
|
|
@@ -1565,16 +1650,6 @@ export function registerWorkspaceTools(
|
|
|
1565
1650
|
// never reached the repository — the whole bug, spelled with a prefix.
|
|
1566
1651
|
toolHandler(
|
|
1567
1652
|
async (args, ctx) => {
|
|
1568
|
-
// FIRST, before the path work and before any handler: every tool
|
|
1569
|
-
// mounted here declares `branch` as a required, non-empty string, and
|
|
1570
|
-
// nothing enforced that, so a call that named none was carried down
|
|
1571
|
-
// until `workspaceIdForBranch` made a workspace directory out of the
|
|
1572
|
-
// missing value. Most of these tools would meet the same refusal one
|
|
1573
|
-
// layer down at `getFilesystem`, but not all of them do — `unzip`
|
|
1574
|
-
// hands `branch` straight to the workspace service by id — so the
|
|
1575
|
-
// check belongs on the mount every one of them shares rather than on
|
|
1576
|
-
// the resolver only some of them reach.
|
|
1577
|
-
if (requiresBranch && !spec.resolvesBranchItself) assertBranchProvided(args.branch);
|
|
1578
1653
|
// BEFORE the normaliser: see `assertToolPathsNotGitInternals`.
|
|
1579
1654
|
if (spec.fileTool !== false) await assertToolPathsNotGitInternals(args, ctx);
|
|
1580
1655
|
const normalized = normalizePathArgs(
|
|
@@ -1608,7 +1683,8 @@ export function registerWorkspaceTools(
|
|
|
1608
1683
|
);
|
|
1609
1684
|
}
|
|
1610
1685
|
},
|
|
1611
|
-
|
|
1686
|
+
// `execute_command` resolves its own branch (see `resolvesBranchItself`).
|
|
1687
|
+
{ write: spec.write, writeScope: spec.writeScope, ...(spec.resolvesBranchItself ? { branch: 'own' as const } : {}) },
|
|
1612
1688
|
),
|
|
1613
1689
|
);
|
|
1614
1690
|
};
|
|
@@ -1640,12 +1716,17 @@ export function registerWorkspaceTools(
|
|
|
1640
1716
|
const startSessionDef = toolDef({
|
|
1641
1717
|
name: 'start_session',
|
|
1642
1718
|
description:
|
|
1643
|
-
'Mint
|
|
1719
|
+
'Mint this conversation\'s id: the `sessionId` KnowledgeBase tools take. Call this ONCE, at the start of your work — minting a new id mid-run starts a second conversation as far as the server is concerned. The id is also a chat session in the app, so you can hand the SAME id to the `ask` tool: your reads and your questions are then one conversation. Pass the returned id as `sessionId` on every later KnowledgeBase tool call, inside `call_tool_chain` too. RETRYING IS SAFE: a call that fails created nothing, so retry it. If a retry lands after a success you hold two independent ids, which is harmless: keep passing the one you already used and ignore the other. Returns `{ sessionId }`, plus `firstRun` while the knowledge base is still empty: a note to act on.',
|
|
1644
1720
|
path: '/api/agent/tools/start_session',
|
|
1645
1721
|
inputs: { type: 'object', properties: {}, additionalProperties: false },
|
|
1646
1722
|
outputs: {
|
|
1647
1723
|
type: 'object',
|
|
1648
|
-
properties: {
|
|
1724
|
+
properties: {
|
|
1725
|
+
sessionId: str('The minted session id — pass it as `sessionId` on subsequent KnowledgeBase tool calls and to `ask`.'),
|
|
1726
|
+
firstRun: str(
|
|
1727
|
+
`Present only while the knowledge base holds nothing but its starter guide (and a starter pack's untouched pages): what to offer the person (the guide's \`${FIRST_RUN_SECTION_ID}\` section says how).`,
|
|
1728
|
+
),
|
|
1729
|
+
},
|
|
1649
1730
|
required: ['sessionId'],
|
|
1650
1731
|
},
|
|
1651
1732
|
tags: ['workspace'],
|
|
@@ -1665,11 +1746,104 @@ export function registerWorkspaceTools(
|
|
|
1665
1746
|
requireExternalSource,
|
|
1666
1747
|
toolHandler(async (_args, ctx) => {
|
|
1667
1748
|
const { sessionId } = await sessionSink.createSession(ctx.user.id, new Date());
|
|
1668
|
-
|
|
1749
|
+
const firstRun = await firstRunFor(ctx);
|
|
1750
|
+
return firstRun ? { sessionId, firstRun } : { sessionId };
|
|
1669
1751
|
}),
|
|
1670
1752
|
);
|
|
1671
1753
|
|
|
1754
|
+
/**
|
|
1755
|
+
* The `firstRun` note (see `first-run.ts`) while the default branch's
|
|
1756
|
+
* knowledge folder holds nothing but the starter guide, else null. Asked of
|
|
1757
|
+
* a clone that is ALREADY there — never one this call would have to make, so
|
|
1758
|
+
* the first call of a conversation does no clone — and never allowed to fail
|
|
1759
|
+
* the call: minting the id is what `start_session` is for, and the note is a
|
|
1760
|
+
* courtesy on top of it.
|
|
1761
|
+
*/
|
|
1762
|
+
const firstRunFor = async (ctx: ToolContext): Promise<string | null> => {
|
|
1763
|
+
try {
|
|
1764
|
+
if (!disk || !kb.isBranchModelConfigured()) return null;
|
|
1765
|
+
const workspaceId = kb.defaultWorkspaceId();
|
|
1766
|
+
if (!(await ctx.workspaceService.hasBootstrappedWorkspace(workspaceId))) return null;
|
|
1767
|
+
const knowledgeDir = kb.layout.knowledgeBaseDir;
|
|
1768
|
+
// GATED LIKE A READ. The note says what the knowledge folder holds —
|
|
1769
|
+
// nothing, or a pack's pages still as the pack wrote them — so it goes
|
|
1770
|
+
// only to a caller who may read that folder, and judges the folder as
|
|
1771
|
+
// THEY may see it: a page they may not read does not make it old for
|
|
1772
|
+
// them (`mayRead` below), since reading the folder is no leave to learn
|
|
1773
|
+
// what restricted pages sit in it.
|
|
1774
|
+
if (!(await accessControl.canRead(workspaceId, ctx.user.email, knowledgeDir))) return null;
|
|
1775
|
+
const root = await ctx.workspaceService.getWorkspacePath(workspaceId);
|
|
1776
|
+
const mayRead = async (rels: string[]) => {
|
|
1777
|
+
const verdicts = await accessControl.canReadBatch(workspaceId, ctx.user.email, rels.map((rel) => `${knowledgeDir}/${rel}`));
|
|
1778
|
+
return new Map(rels.map((rel) => [rel, verdicts.get(`${knowledgeDir}/${rel}`) === true]));
|
|
1779
|
+
};
|
|
1780
|
+
// A starter pack's pages, still as the pack wrote them, are tasks to
|
|
1781
|
+
// fill in rather than pages anyone wrote: they leave the note standing,
|
|
1782
|
+
// and the note names what the pack suggests drafting first. A pack
|
|
1783
|
+
// page the caller may not read keeps the note away altogether: it
|
|
1784
|
+
// names the pack and what it suggests drafting, which is about pages
|
|
1785
|
+
// this caller is not to know of — in the checkout or gone from it.
|
|
1786
|
+
const starter = (await starterPacks?.firstRunStarter()) ?? null;
|
|
1787
|
+
const pages = starter ? await readableStarterPages(ctx, workspaceId, knowledgeDir, starter.pages) : undefined;
|
|
1788
|
+
if (starter && pages && pages.size < starter.pages.size) return null;
|
|
1789
|
+
if (!(await knowledgeFolderIsNew(disk, join(root, kbDirName), knowledgeDir, pages, mayRead))) return null;
|
|
1790
|
+
return firstRunNote(`${kbDirName}/${knowledgeDir}`, starter ?? undefined);
|
|
1791
|
+
} catch (err) {
|
|
1792
|
+
log.debug('start_session: could not tell whether the knowledge base is new', {
|
|
1793
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1794
|
+
});
|
|
1795
|
+
return null;
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
|
|
1799
|
+
/** The starter pages (paths below the knowledge folder → text) the caller may read, and no other. */
|
|
1800
|
+
const readableStarterPages = async (
|
|
1801
|
+
ctx: ToolContext,
|
|
1802
|
+
workspaceId: string,
|
|
1803
|
+
knowledgeDir: string,
|
|
1804
|
+
pages: ReadonlyMap<string, string>,
|
|
1805
|
+
): Promise<ReadonlyMap<string, string>> => {
|
|
1806
|
+
const rels = [...pages.keys()];
|
|
1807
|
+
const verdicts = await accessControl.canReadBatch(workspaceId, ctx.user.email, rels.map((rel) => `${knowledgeDir}/${rel}`));
|
|
1808
|
+
return new Map(rels.filter((rel) => verdicts.get(`${knowledgeDir}/${rel}`)).map((rel) => [rel, pages.get(rel)!]));
|
|
1809
|
+
};
|
|
1810
|
+
|
|
1672
1811
|
// ── reads ──────────────────────────────────────────────────────────────
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* What reading a workspace file ANSWERS, gate and all — `read_file`'s whole
|
|
1815
|
+
* behaviour minus the spill ref and the `offset`/`limit` slice, which are
|
|
1816
|
+
* that tool's own arguments.
|
|
1817
|
+
*
|
|
1818
|
+
* Factored out because a second tool has to answer the same way: `open_page`
|
|
1819
|
+
* (see `modules/embed`) promises the file's text "the way `read_file` does",
|
|
1820
|
+
* and the refusals `read_file` gives for a path the caller may not read or
|
|
1821
|
+
* one that does not exist. Any of that re-derived there would be a second
|
|
1822
|
+
* answer to a question with one correct answer — the access gate, the read
|
|
1823
|
+
* hook, the extraction and the not-found message all have to match, and a
|
|
1824
|
+
* copy drifts on the first change to any of them.
|
|
1825
|
+
*/
|
|
1826
|
+
const readForTool: ReadForTool = async (branch, p, ctx) => {
|
|
1827
|
+
// The guide's name at the repository root answers with the platform's
|
|
1828
|
+
// guide, which is text the code owns and every agent may read: no gate
|
|
1829
|
+
// and no read hook for it. A file the knowledge base keeps under that
|
|
1830
|
+
// name is ITS OWN conventions page and is read as any file is — gated,
|
|
1831
|
+
// noted — and comes first, with the guide after it. A copy of the
|
|
1832
|
+
// guide an earlier release wrote to disk (still on a draft, say) is
|
|
1833
|
+
// recognised by its header and not served a second time.
|
|
1834
|
+
if (agentGuide && isAgentGuidePath(toKbRelative(p, kbDirName) ?? '')) {
|
|
1835
|
+
return { kind: 'text', text: await guideAt(branch, ctx, p) };
|
|
1836
|
+
}
|
|
1837
|
+
await notifyAgentRead(agentAccessGate, ctx, branch, p);
|
|
1838
|
+
await assertCanRead(readGateFor(branch, ctx), p);
|
|
1839
|
+
const fs = await ctx.getFilesystem(branch);
|
|
1840
|
+
// Reading (extraction, image and binary handling included) happens AFTER
|
|
1841
|
+
// the access gate and the read hook above — a document read is still a
|
|
1842
|
+
// KB read. ONE registry dispatch picks the reader by extension.
|
|
1843
|
+
const bytes = await orNotFound(p, async () => asBytes(await fs.readFile(p)));
|
|
1844
|
+
return readers.readerFor(p).read(bytes, p);
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1673
1847
|
mount({
|
|
1674
1848
|
name: 'read_file',
|
|
1675
1849
|
gated: true,
|
|
@@ -1712,26 +1886,7 @@ export function registerWorkspaceTools(
|
|
|
1712
1886
|
? content.slice(start, limit !== undefined ? start + limit : undefined)
|
|
1713
1887
|
: content;
|
|
1714
1888
|
};
|
|
1715
|
-
|
|
1716
|
-
// guide, which is text the code owns and every agent may read: no gate
|
|
1717
|
-
// and no read hook for it. A file the knowledge base keeps under that
|
|
1718
|
-
// name is ITS OWN conventions page and is read as any file is — gated,
|
|
1719
|
-
// noted — and comes first, with the guide after it. A copy of the
|
|
1720
|
-
// guide an earlier release wrote to disk (still on a draft, say) is
|
|
1721
|
-
// recognised by its header and not served a second time.
|
|
1722
|
-
if (agentGuide && isAgentGuidePath(toKbRelative(p, kbDirName) ?? '')) {
|
|
1723
|
-
return { path: p, content: slice(await guideAt(a.branch as string, ctx, p)) };
|
|
1724
|
-
}
|
|
1725
|
-
await notifyAgentRead(agentAccessGate, ctx, a.branch as string, p);
|
|
1726
|
-
await assertCanRead(readGateFor(a.branch as string, ctx), p);
|
|
1727
|
-
const fs = await ctx.getFilesystem(a.branch as string);
|
|
1728
|
-
// Reading (extraction, image and binary handling included) happens AFTER
|
|
1729
|
-
// the access gate and the read hook above — a document read is still a
|
|
1730
|
-
// KB read. ONE registry dispatch picks the reader by
|
|
1731
|
-
// extension; everything below just maps its ReadResult onto the tool's
|
|
1732
|
-
// result shape.
|
|
1733
|
-
const bytes = await orNotFound(p, async () => asBytes(await fs.readFile(p)));
|
|
1734
|
-
const result = await readers.readerFor(p).read(bytes, p);
|
|
1889
|
+
const result = await readForTool(a.branch as string, p, ctx);
|
|
1735
1890
|
// Images return the picture itself as an MCP image content block, so a
|
|
1736
1891
|
// multimodal model SEES it. The handler returns the `McpImageResult`
|
|
1737
1892
|
// sentinel; the MCP result shaping (`toCallToolResult` in
|
|
@@ -1904,8 +2059,8 @@ export function registerWorkspaceTools(
|
|
|
1904
2059
|
'never move or delete, judged like the dry runs (on a draft branch writes are not gated); `movable` judges the SOURCE ' +
|
|
1905
2060
|
'side only, so the destination still wants a `move_file` dry run. ' +
|
|
1906
2061
|
'For a folder, `descendants` counts the files under it at any depth; counting stops at 10000 and ' +
|
|
1907
|
-
'`descendantsTruncated` says so, past which `movable` and `deletable` are false
|
|
1908
|
-
'
|
|
2062
|
+
'`descendantsTruncated` says so, past which `movable` and `deletable` are false (not judged in full): run the ' +
|
|
2063
|
+
'`move_file` or `delete_folder` dry run for the real verdict.',
|
|
1909
2064
|
inputs: {
|
|
1910
2065
|
type: 'object',
|
|
1911
2066
|
properties: {
|
|
@@ -2055,7 +2210,11 @@ export function registerWorkspaceTools(
|
|
|
2055
2210
|
// `mime` below, so it is never passed through.
|
|
2056
2211
|
delete stat.mimeType;
|
|
2057
2212
|
const kind = stat.type === 'directory' ? 'folder' : 'file';
|
|
2058
|
-
const
|
|
2213
|
+
const onDisk = await onDiskSpelling(root, p);
|
|
2214
|
+
const managed = managedReason(onDisk, kind) !== undefined;
|
|
2215
|
+
// A nested `access.md` is managed — it never moves — yet deleted by
|
|
2216
|
+
// whoever may write it, so a FILE's delete is judged on its own rule.
|
|
2217
|
+
const undeletable = kind === 'file' ? fileDeleteRefusal(onDisk) !== undefined : managed;
|
|
2059
2218
|
const verdicts = await accessAt(branch, ctx, p);
|
|
2060
2219
|
const access =
|
|
2061
2220
|
a.explainAccess === true ? { ...verdicts, ...(await explainAccessAt(branch, ctx, p, kind)) } : verdicts;
|
|
@@ -2075,7 +2234,7 @@ export function registerWorkspaceTools(
|
|
|
2075
2234
|
// `movable`/`deletable` false rather than judging part of a folder
|
|
2076
2235
|
// and calling it the whole (`delete_folder`'s dry run, which walks
|
|
2077
2236
|
// uncapped, remains the authority for a folder that large).
|
|
2078
|
-
const decided = managed || link;
|
|
2237
|
+
const decided = (managed && undeletable) || link;
|
|
2079
2238
|
const { files, links, truncated } =
|
|
2080
2239
|
kind === 'folder'
|
|
2081
2240
|
? await filesUnder(fs, p, DESCENDANTS_CAP)
|
|
@@ -2097,9 +2256,9 @@ export function registerWorkspaceTools(
|
|
|
2097
2256
|
const out: Record<string, unknown> = {
|
|
2098
2257
|
...stat,
|
|
2099
2258
|
managed,
|
|
2100
|
-
movable: open,
|
|
2259
|
+
movable: open && !managed,
|
|
2101
2260
|
// delete_folder also refuses a folder holding a link.
|
|
2102
|
-
deletable: open && links.length === 0,
|
|
2261
|
+
deletable: open && !undeletable && links.length === 0,
|
|
2103
2262
|
access,
|
|
2104
2263
|
};
|
|
2105
2264
|
if (kind === 'folder') {
|
|
@@ -2130,7 +2289,7 @@ export function registerWorkspaceTools(
|
|
|
2130
2289
|
name: 'grep',
|
|
2131
2290
|
gated: true,
|
|
2132
2291
|
description:
|
|
2133
|
-
'Regex
|
|
2292
|
+
'Regex search across the workspace. Returns `{ matches: [{ path, line, text }] }` (capped). Use to find where something is defined/referenced. `path` may name a DIRECTORY (searches the subtree) or a single FILE (searches just that file); a path with nothing at it is an error, never an empty result. Searches INSIDE Office and OpenDocument files (.docx/.pptx/.xlsx, .odt/.odp/.ods), PDFs and email files (.eml/.msg) via their extracted text — matches there carry the extraction\'s line numbers, and the `[slide N]`/`[sheet: Name]`/`[page N]`/`[from]`/`[subject]` marker lines locate them; a bounded number of not-yet-extracted documents is extracted per call, and the result notes how many were skipped (re-run to cover them).',
|
|
2134
2293
|
inputs: {
|
|
2135
2294
|
type: 'object',
|
|
2136
2295
|
properties: {
|
|
@@ -2295,6 +2454,8 @@ export function registerWorkspaceTools(
|
|
|
2295
2454
|
// ── writes (through the lock/commit pipeline) ───────────────────────────
|
|
2296
2455
|
mount({
|
|
2297
2456
|
name: 'write_file',
|
|
2457
|
+
// A mode that is not one of the three answers `bad_mode`, which lists them.
|
|
2458
|
+
refusesItself: ['mode'],
|
|
2298
2459
|
gated: true,
|
|
2299
2460
|
description:
|
|
2300
2461
|
'Write a workspace TEXT file. The change is committed + pushed as you. Returns `{ path, bytes, outcome }`, where `outcome` is ' +
|
|
@@ -2377,6 +2538,8 @@ export function registerWorkspaceTools(
|
|
|
2377
2538
|
|
|
2378
2539
|
mount({
|
|
2379
2540
|
name: 'write_files',
|
|
2541
|
+
// A mode that is not one of the three answers `bad_mode`, which lists them.
|
|
2542
|
+
refusesItself: ['mode'],
|
|
2380
2543
|
gated: true,
|
|
2381
2544
|
description:
|
|
2382
2545
|
'Batch-write many files in ONE commit — far faster than calling write_file once per file when ' +
|
|
@@ -2438,8 +2601,10 @@ export function registerWorkspaceTools(
|
|
|
2438
2601
|
proposable: true,
|
|
2439
2602
|
handler: async (a, ctx: ToolContext) => {
|
|
2440
2603
|
const files = (a.files as Array<{ path: string; content: string }>) ?? [];
|
|
2441
|
-
|
|
2604
|
+
// The mode is judged before anything else, so an empty batch with a mode
|
|
2605
|
+
// that is not one answers `bad_mode` like any other call would.
|
|
2442
2606
|
const mode = modeOf(a);
|
|
2607
|
+
if (files.length === 0) return { count: 0, files: [] };
|
|
2443
2608
|
// The POLICY gate still judges the whole batch: a restricted run is a
|
|
2444
2609
|
// call that should not have been made at all, not a per-path outcome.
|
|
2445
2610
|
// The write hook is asked PER PATH, below, so a path it refuses is that
|
|
@@ -2671,10 +2836,8 @@ export function registerWorkspaceTools(
|
|
|
2671
2836
|
throw new ToolError(`"${path}" is a folder, not a file — use delete_folder to delete it and the files under it.`, 400);
|
|
2672
2837
|
}
|
|
2673
2838
|
const onDisk = await onDiskSpelling(root, path);
|
|
2674
|
-
|
|
2675
|
-
if (
|
|
2676
|
-
throw new ToolError(`${onDisk.slice(onDisk.lastIndexOf('/') + 1)} is a platform file and cannot be deleted through the agent tools.`, 400);
|
|
2677
|
-
}
|
|
2839
|
+
const refused = fileDeleteRefusal(onDisk);
|
|
2840
|
+
if (refused !== undefined) throw new ToolError(refused, 400);
|
|
2678
2841
|
await assertNoSymlinkOnPath(root, path, true);
|
|
2679
2842
|
if ((await writeBlocked(branch, ctx, [path])).length > 0) throw await writeRefusal(branch, path);
|
|
2680
2843
|
await orNotFound(path, () => fs.deleteFile(path), 'Nothing to delete');
|
|
@@ -2860,12 +3023,13 @@ export function registerWorkspaceTools(
|
|
|
2860
3023
|
// A plain string again: what refuses a move names the guide, and that is in
|
|
2861
3024
|
// the shared rules now, which are rebuilt from the layout where they live.
|
|
2862
3025
|
description:
|
|
2863
|
-
'Move or rename a workspace FILE or FOLDER; a folder moves recursively, with everything under it. `dest` is the full new path, not the folder to move into.
|
|
3026
|
+
'Move or rename a workspace FILE or FOLDER; a folder moves recursively, with everything under it. `dest` is the full new path, not the folder to move into. Committed + pushed as you. ' +
|
|
2864
3027
|
'The destination must not exist — a move never overwrites a file or merges into a folder. Access follows the ' +
|
|
2865
3028
|
'DESTINATION folder, so a move can change what you (and others) may do with the file: the dry run answers ' +
|
|
2866
3029
|
'`{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }`, where `access` is your ' +
|
|
2867
3030
|
'own `{ read, write, download, owner }` at the source and at the destination AS IT WILL BE once the move has landed, ' +
|
|
2868
|
-
'with every `access.md` inside a moved folder counted at its new place, and a move whose `accessChanges` is true wants `confirm: true`.'
|
|
3031
|
+
'with every `access.md` inside a moved folder counted at its new place, and a move whose `accessChanges` is true wants `confirm: true`. ' +
|
|
3032
|
+
'Links are rewritten by default, in one commit with the move: those in the moved files and those in other markdown files pointing at them (`links` reports them). `rewriteLinks: false` turns that off. Path mentions in plain prose or code are never changed.',
|
|
2869
3033
|
inputs: {
|
|
2870
3034
|
type: 'object',
|
|
2871
3035
|
properties: {
|
|
@@ -2874,6 +3038,7 @@ export function registerWorkspaceTools(
|
|
|
2874
3038
|
dest: wsPath(kbDirName, 'Destination path — the full new path; must not exist yet'),
|
|
2875
3039
|
dryRun: { type: 'boolean', description: 'Answer with the impact and change nothing.' },
|
|
2876
3040
|
confirm: { type: 'boolean', description: 'Required when the move changes your access. Set it only after a dry run.' },
|
|
3041
|
+
rewriteLinks: { type: 'boolean', description: 'Rewrite the links into, out of and between the moved files (default true). `false` moves without touching any link.' },
|
|
2877
3042
|
sessionId: SESSION_ID_INPUT,
|
|
2878
3043
|
},
|
|
2879
3044
|
required: ['branch', 'src', 'dest'],
|
|
@@ -2894,6 +3059,12 @@ export function registerWorkspaceTools(
|
|
|
2894
3059
|
confirmationRequired: { type: 'boolean', description: 'True when the call stopped for want of `confirm: true`.' },
|
|
2895
3060
|
message: str('One sentence on what happened (or did not).'),
|
|
2896
3061
|
moved: { type: 'boolean', description: 'True once the move landed.' },
|
|
3062
|
+
links: {
|
|
3063
|
+
type: 'object',
|
|
3064
|
+
description:
|
|
3065
|
+
'The link rewrite, the same on a dry run and on the move: `{ filesEdited, linksRewritten, edits: [{ path, from, to }] (the first 100), ' +
|
|
3066
|
+
'notRewritten: [{ path, reason, links }], unsearched? }`. Absent with `rewriteLinks: false`.',
|
|
3067
|
+
},
|
|
2897
3068
|
},
|
|
2898
3069
|
required: ['src', 'dest', 'moved'],
|
|
2899
3070
|
},
|
|
@@ -2984,6 +3155,56 @@ export function registerWorkspaceTools(
|
|
|
2984
3155
|
: occupiedBy !== null
|
|
2985
3156
|
? entryExistsMessage(occupiedBy, dest)
|
|
2986
3157
|
: undefined;
|
|
3158
|
+
// The links are planned only for a move that may run: a refused one
|
|
3159
|
+
// reads no page. The plan is the same on a dry run and on the move.
|
|
3160
|
+
const rewriteLinks = a.rewriteLinks !== false;
|
|
3161
|
+
const linkPlan = rewriteLinks && reason === undefined
|
|
3162
|
+
? await planMoveLinks({
|
|
3163
|
+
src,
|
|
3164
|
+
dest,
|
|
3165
|
+
branch,
|
|
3166
|
+
kbDirName,
|
|
3167
|
+
allFiles: await (async () => {
|
|
3168
|
+
const { files, links } = await filesUnder(fs, kbDirName);
|
|
3169
|
+
const symlinks = new Set(links);
|
|
3170
|
+
return files.filter((f) => !symlinks.has(f));
|
|
3171
|
+
})(),
|
|
3172
|
+
canRead: (paths) => resolveReadableMap(
|
|
3173
|
+
(wid, email, rels) => accessControl.canReadBatch(wid, email, rels),
|
|
3174
|
+
workspaceIdForBranch(branch),
|
|
3175
|
+
ctx.user.email,
|
|
3176
|
+
kbDirName,
|
|
3177
|
+
paths,
|
|
3178
|
+
),
|
|
3179
|
+
writeBlocked: (paths) => writeBlocked(branch, ctx, paths),
|
|
3180
|
+
// Through the guarded filesystem the tools read with, never the raw
|
|
3181
|
+
// disk: a page replaced by a link since the listing is refused there
|
|
3182
|
+
// instead of read through to wherever the link points.
|
|
3183
|
+
readText: async (p) => String(await fs.readFile(p, { encoding: 'utf8' })),
|
|
3184
|
+
// The read hook, for every page the answer would NAME — an edited
|
|
3185
|
+
// one, one left with its links listed — and for no page merely
|
|
3186
|
+
// searched: naming is the disclosure, and the hook's refusal makes
|
|
3187
|
+
// the page one the caller cannot read, covered by the one sentence.
|
|
3188
|
+
readRefused: async (path) => {
|
|
3189
|
+
try {
|
|
3190
|
+
await notifyAgentRead(agentAccessGate, ctx, branch, path);
|
|
3191
|
+
return false;
|
|
3192
|
+
} catch {
|
|
3193
|
+
return true;
|
|
3194
|
+
}
|
|
3195
|
+
},
|
|
3196
|
+
// The write hook, for a page the move would edit, at its post-move path.
|
|
3197
|
+
writeRefusal: async (_lockAt, path) => {
|
|
3198
|
+
try {
|
|
3199
|
+
await assertAgentWriteAllowed(agentAccessGate, ctx, branch, path);
|
|
3200
|
+
return null;
|
|
3201
|
+
} catch (err) {
|
|
3202
|
+
return `refused: ${err instanceof Error ? err.message : String(err)}`;
|
|
3203
|
+
}
|
|
3204
|
+
},
|
|
3205
|
+
})
|
|
3206
|
+
: undefined;
|
|
3207
|
+
const moveReason = reason ?? linkPlan?.overCap;
|
|
2987
3208
|
const impact = {
|
|
2988
3209
|
src,
|
|
2989
3210
|
dest,
|
|
@@ -2991,8 +3212,9 @@ export function registerWorkspaceTools(
|
|
|
2991
3212
|
descendants,
|
|
2992
3213
|
access: { before, after },
|
|
2993
3214
|
accessChanges,
|
|
2994
|
-
allowed:
|
|
2995
|
-
...(
|
|
3215
|
+
allowed: moveReason === undefined,
|
|
3216
|
+
...(moveReason !== undefined ? { reason: moveReason } : {}),
|
|
3217
|
+
...(linkPlan ? { links: linkPlan.report } : {}),
|
|
2996
3218
|
};
|
|
2997
3219
|
if (a.dryRun === true) return { ...impact, dryRun: true, moved: false };
|
|
2998
3220
|
if (managed) throw new ToolError(reason!, 400);
|
|
@@ -3004,6 +3226,7 @@ export function registerWorkspaceTools(
|
|
|
3004
3226
|
throw await writeRefusal(branch, blocked[0], folderPath ? 'dir' : 'file');
|
|
3005
3227
|
}
|
|
3006
3228
|
if (collision) throw new ToolError(reason!, 409);
|
|
3229
|
+
if (linkPlan?.overCap !== undefined) throw new ToolError(linkPlan.overCap, 400);
|
|
3007
3230
|
if (accessChanges && a.confirm !== true) {
|
|
3008
3231
|
return {
|
|
3009
3232
|
...impact,
|
|
@@ -3016,7 +3239,37 @@ export function registerWorkspaceTools(
|
|
|
3016
3239
|
// no-replace move is what guarantees it. A destination created between
|
|
3017
3240
|
// the two — by another agent, or by the sidebar, which moves without
|
|
3018
3241
|
// taking this lock — comes back here as a refusal, not an overwrite.
|
|
3019
|
-
|
|
3242
|
+
if (linkPlan === undefined) {
|
|
3243
|
+
await asEntryExists(() => fs.moveFile(src, dest));
|
|
3244
|
+
} else {
|
|
3245
|
+
// The move and every link edit, one commit. Under the locks, each page
|
|
3246
|
+
// is checked to still hold the bytes its edit was computed from.
|
|
3247
|
+
const edits = linkPlan.edits.map((e) => ({ path: e.path, lockAt: e.lockAt, content: e.content }));
|
|
3248
|
+
const check = async () => {
|
|
3249
|
+
for (const e of linkPlan.edits) {
|
|
3250
|
+
const now = await fs.readFile(e.lockAt, { encoding: 'utf8' }).then(String, () => null);
|
|
3251
|
+
if (now !== e.original) {
|
|
3252
|
+
throw new ToolError(`"${e.lockAt}" changed while the move was being planned, so nothing was moved. Run the move again.`, 409);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
};
|
|
3256
|
+
const summary = `Move ${src} to ${dest}`.slice(0, 200);
|
|
3257
|
+
const locking = fs as LocalFilesystem & { moveWithEdits?: LockingFilesystem['moveWithEdits'] };
|
|
3258
|
+
try {
|
|
3259
|
+
if (typeof locking.moveWithEdits === 'function') {
|
|
3260
|
+
await asEntryExists(() => locking.moveWithEdits!(src, dest, edits, summary, check));
|
|
3261
|
+
} else {
|
|
3262
|
+
// A filesystem without the one-commit move (none the agent is
|
|
3263
|
+
// handed for writing): the move, then each edit.
|
|
3264
|
+
await check();
|
|
3265
|
+
await asEntryExists(() => fs.moveFile(src, dest));
|
|
3266
|
+
for (const e of edits) await fs.writeFile(e.path, e.content);
|
|
3267
|
+
}
|
|
3268
|
+
} catch (err) {
|
|
3269
|
+
if (err instanceof MoveLockedError || err instanceof MoveRacedError) throw new ToolError(err.message, 409);
|
|
3270
|
+
throw err;
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3020
3273
|
// Moving the last file — or a whole folder — out leaves the folder it
|
|
3021
3274
|
// came from in place, like a delete.
|
|
3022
3275
|
await keepFolderOf(fs, ctx, branch, src, kbDirName);
|
|
@@ -3474,6 +3727,8 @@ export function registerWorkspaceTools(
|
|
|
3474
3727
|
|
|
3475
3728
|
mount({
|
|
3476
3729
|
name: 'apply_file_upload',
|
|
3730
|
+
// A mode that is not one of the three answers `bad_mode`, which lists them.
|
|
3731
|
+
refusesItself: ['mode'],
|
|
3477
3732
|
gated: true,
|
|
3478
3733
|
description:
|
|
3479
3734
|
'Land a file you have already uploaded (see `request_file_upload`) in a folder on a branch, in ONE commit, as you. ' +
|
|
@@ -3539,6 +3794,177 @@ export function registerWorkspaceTools(
|
|
|
3539
3794
|
});
|
|
3540
3795
|
}
|
|
3541
3796
|
|
|
3797
|
+
// ── downloads (bytes that never pass through the model, the other way) ──
|
|
3798
|
+
// The twin of the upload pair: `read_file` answers content INTO the
|
|
3799
|
+
// conversation, so an agent that needs exact bytes on its own disk had no
|
|
3800
|
+
// way to get them. `request_file_download` judges every file on its own,
|
|
3801
|
+
// captures the ones that pass, and answers a one-time link per file (and a
|
|
3802
|
+
// zip per requested folder) that any HTTP client can fetch.
|
|
3803
|
+
|
|
3804
|
+
/** The type a file's link answers with: the reader's, as `file_stat` reports it — but never active content. */
|
|
3805
|
+
const downloadContentType = (p: string, bytes: Buffer): string => {
|
|
3806
|
+
const reader = readers.readerFor(p);
|
|
3807
|
+
const mime = fileTypeOf(reader, p, needsContent(reader) ? bytes : undefined).mime;
|
|
3808
|
+
// SVG and HTML run scripts wherever they are opened — saved to disk and
|
|
3809
|
+
// re-opened under `file://`, too — so they go out as bytes, as the app's
|
|
3810
|
+
// own Download button sends them.
|
|
3811
|
+
return ACTIVE_CONTENT_TYPES.has(mime) ? 'application/octet-stream' : mime;
|
|
3812
|
+
};
|
|
3813
|
+
|
|
3814
|
+
if (downloads) {
|
|
3815
|
+
mount({
|
|
3816
|
+
name: 'request_file_download',
|
|
3817
|
+
gated: true,
|
|
3818
|
+
description:
|
|
3819
|
+
'Copy knowledge-base files onto your own disk without their content passing through the conversation — the ' +
|
|
3820
|
+
'way out, as `request_file_upload` is the way in. Give `branch` and `paths` (files or folders). Every file, ' +
|
|
3821
|
+
'each one inside a folder too, is included only if you may read AND download that file. Returns ' +
|
|
3822
|
+
'`{ expiresAt, expiresInSeconds, files: [{ path, bytes, sha256, downloadUrl }], folders: [{ path, bytes, ' +
|
|
3823
|
+
'downloadUrl, files }], refused: [{ path, reason }] }`: a link per file, with its own content type, and per ' +
|
|
3824
|
+
`folder a zip at full repository paths (\`apply_file_upload\` it at \`${kbDirName}/\` to put every file back). ` +
|
|
3825
|
+
'Fetch with any HTTP client (`curl -o <name> "<downloadUrl>"`, or the address without its last segment and an ' +
|
|
3826
|
+
'`x-download-token` header). Each link works ONCE, for 15 minutes, and serves the files as they are now. ' +
|
|
3827
|
+
'Refused: `not found` (missing or named-but-unreadable; folders omit unreadable files), ' +
|
|
3828
|
+
'`download permission required`; no link when nothing is included. At most 500 MB per request.',
|
|
3829
|
+
inputs: {
|
|
3830
|
+
type: 'object',
|
|
3831
|
+
properties: {
|
|
3832
|
+
branch: BRANCH_INPUT,
|
|
3833
|
+
paths: {
|
|
3834
|
+
type: 'array',
|
|
3835
|
+
minItems: 1,
|
|
3836
|
+
items: { type: 'string' },
|
|
3837
|
+
description: `Files and folders to download, under \`${kbDirName}/\` (e.g. \`${kbDirName}/KnowledgeBase/Foo.md\`), with or without a leading slash — a path without that prefix is placed under \`${kbDirName}/\`.`,
|
|
3838
|
+
},
|
|
3839
|
+
sessionId: SESSION_ID_INPUT,
|
|
3840
|
+
},
|
|
3841
|
+
required: ['branch', 'paths'],
|
|
3842
|
+
additionalProperties: false,
|
|
3843
|
+
},
|
|
3844
|
+
outputs: {
|
|
3845
|
+
type: 'object',
|
|
3846
|
+
properties: {
|
|
3847
|
+
expiresAt: {
|
|
3848
|
+
type: ['string', 'null'],
|
|
3849
|
+
description: 'ISO-8601 instant after which every link of this answer is gone; null when no link was issued.',
|
|
3850
|
+
},
|
|
3851
|
+
expiresInSeconds: int('Seconds until `expiresAt`; 0 when no link was issued.'),
|
|
3852
|
+
files: {
|
|
3853
|
+
type: 'array',
|
|
3854
|
+
description: 'One entry per included file, each named once however many times it was asked for.',
|
|
3855
|
+
items: {
|
|
3856
|
+
type: 'object',
|
|
3857
|
+
properties: {
|
|
3858
|
+
path: str('The workspace path.'),
|
|
3859
|
+
bytes: int('Size in bytes.'),
|
|
3860
|
+
sha256: str('SHA-256 of the bytes the link serves, hex.'),
|
|
3861
|
+
downloadUrl: str('One-time link to the file itself.'),
|
|
3862
|
+
},
|
|
3863
|
+
required: ['path', 'bytes', 'sha256', 'downloadUrl'],
|
|
3864
|
+
},
|
|
3865
|
+
},
|
|
3866
|
+
folders: {
|
|
3867
|
+
type: 'array',
|
|
3868
|
+
description: 'One entry per requested folder that kept at least one file.',
|
|
3869
|
+
items: {
|
|
3870
|
+
type: 'object',
|
|
3871
|
+
properties: {
|
|
3872
|
+
path: str('The folder, as requested.'),
|
|
3873
|
+
bytes: int('Uncompressed total of the files in its zip.'),
|
|
3874
|
+
downloadUrl: str('One-time link to the zip.'),
|
|
3875
|
+
files: { type: 'array', items: { type: 'string' }, description: 'The workspace paths the zip holds.' },
|
|
3876
|
+
},
|
|
3877
|
+
required: ['path', 'bytes', 'downloadUrl', 'files'],
|
|
3878
|
+
},
|
|
3879
|
+
},
|
|
3880
|
+
refused: {
|
|
3881
|
+
type: 'array',
|
|
3882
|
+
description: 'Every path left out, with why.',
|
|
3883
|
+
items: {
|
|
3884
|
+
type: 'object',
|
|
3885
|
+
properties: { path: str('The path left out.'), reason: str('Why: `not found`, `download permission required`, or the deployment\'s own words.') },
|
|
3886
|
+
required: ['path', 'reason'],
|
|
3887
|
+
},
|
|
3888
|
+
},
|
|
3889
|
+
},
|
|
3890
|
+
required: ['expiresAt', 'expiresInSeconds', 'files', 'folders', 'refused'],
|
|
3891
|
+
},
|
|
3892
|
+
// A download is a READ: a read-only deployment still serves it. A
|
|
3893
|
+
// read-only CREDENTIAL may not take bytes out, though.
|
|
3894
|
+
write: false,
|
|
3895
|
+
writeScope: true,
|
|
3896
|
+
handler: async (a, ctx: ToolContext) => requestFileDownload(a, ctx, downloads),
|
|
3897
|
+
});
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
/** `request_file_download`'s handler: judge, capture, and issue the links. */
|
|
3901
|
+
async function requestFileDownload(
|
|
3902
|
+
a: Record<string, unknown>,
|
|
3903
|
+
ctx: ToolContext,
|
|
3904
|
+
store: IAgentDownloadStore,
|
|
3905
|
+
): Promise<unknown> {
|
|
3906
|
+
const branch = a.branch as string;
|
|
3907
|
+
const raw = a.paths;
|
|
3908
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.some((p) => typeof p !== 'string' || p.trim() === '')) {
|
|
3909
|
+
throw new ToolError('Name at least one path to download in `paths`: an array of file and folder paths.', 400);
|
|
3910
|
+
}
|
|
3911
|
+
// At the cap, say so before a branch is resolved; the slot below counts again.
|
|
3912
|
+
store.assertCanIssue(ctx.user);
|
|
3913
|
+
// Resolves (clones, if need be) the branch's workspace, as a read does —
|
|
3914
|
+
// so an unknown branch is the call's refusal, not every path's.
|
|
3915
|
+
await ctx.getFilesystem(branch);
|
|
3916
|
+
const workspaceId = workspaceIdForBranch(branch);
|
|
3917
|
+
const refusedSpelling: { path: string; reason: string }[] = [];
|
|
3918
|
+
const requested: string[] = [];
|
|
3919
|
+
for (const p of raw as string[]) {
|
|
3920
|
+
try {
|
|
3921
|
+
// As written: a name may begin or end with a space, and trimming it
|
|
3922
|
+
// would ask for another file. A trailing slash is the one spelling
|
|
3923
|
+
// folded, so `Shared/` and `Shared` are one request, not two zips.
|
|
3924
|
+
requested.push(normalizeWorkspacePath(p, kbDirName).replace(/\/+$/, ''));
|
|
3925
|
+
} catch (err) {
|
|
3926
|
+
if (!hasHttpStatus(err)) throw err;
|
|
3927
|
+
refusedSpelling.push({ path: p, reason: err.message });
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
// A slot is taken before anything is built (a caller at the cap is told
|
|
3931
|
+
// so at once) and given back when nothing is issued.
|
|
3932
|
+
return store.withRequestSlot(ctx.user, async (issue) => {
|
|
3933
|
+
const built = await buildDownload(requested, {
|
|
3934
|
+
kbDirName,
|
|
3935
|
+
maxBytes: ZIP_DOWNLOAD_MAX_BYTES,
|
|
3936
|
+
maxFiles: DOWNLOAD_MAX_FILES,
|
|
3937
|
+
candidatesAt: (p) => ctx.workspaceService.downloadCandidatesAt(workspaceId, p),
|
|
3938
|
+
canReadBatch: (paths) => accessControl.canReadBatch(workspaceId, ctx.user.email, paths),
|
|
3939
|
+
canDownloadBatch: (paths) => accessControl.canDownloadBatch(workspaceId, ctx.user.email, paths),
|
|
3940
|
+
// The folder-level gate the app's zip route applies, before the files.
|
|
3941
|
+
canDownloadFolder: (p) =>
|
|
3942
|
+
accessControl.canDownload(workspaceId, ctx.user.email, toKbRelative(p, kbDirName) ?? p),
|
|
3943
|
+
notifyRead: (p) => notifyAgentRead(agentAccessGate, ctx, branch, p),
|
|
3944
|
+
readFile: (p) => ctx.workspaceService.readFileBinary(workspaceId, p),
|
|
3945
|
+
contentTypeOf: downloadContentType,
|
|
3946
|
+
});
|
|
3947
|
+
const refused = [...refusedSpelling, ...built.refused];
|
|
3948
|
+
if (built.artifacts.length === 0) {
|
|
3949
|
+
return { expiresAt: null, expiresInSeconds: 0, files: [], folders: [], refused };
|
|
3950
|
+
}
|
|
3951
|
+
const issued = await issue(built.artifacts);
|
|
3952
|
+
const urls = issued.downloadUrls;
|
|
3953
|
+
return {
|
|
3954
|
+
expiresAt: issued.expiresAt,
|
|
3955
|
+
expiresInSeconds: issued.expiresInSeconds,
|
|
3956
|
+
files: built.files.map((f, i) => ({ ...f, downloadUrl: urls[i]! })),
|
|
3957
|
+
folders: built.folders.map((f, i) => ({
|
|
3958
|
+
path: f.path,
|
|
3959
|
+
bytes: f.bytes,
|
|
3960
|
+
downloadUrl: urls[built.files.length + i]!,
|
|
3961
|
+
files: f.files,
|
|
3962
|
+
})),
|
|
3963
|
+
refused,
|
|
3964
|
+
};
|
|
3965
|
+
});
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3542
3968
|
// ── shell (internal-only) ───────────────────────────────────────────────
|
|
3543
3969
|
mount({
|
|
3544
3970
|
name: 'execute_command',
|
|
@@ -3726,4 +4152,7 @@ export function registerWorkspaceTools(
|
|
|
3726
4152
|
});
|
|
3727
4153
|
},
|
|
3728
4154
|
});
|
|
4155
|
+
|
|
4156
|
+
// The one read `open_page` borrows — see `readForTool` above.
|
|
4157
|
+
return { readForTool };
|
|
3729
4158
|
}
|