@bevel-software/platform-core-backend 0.15.1 → 0.19.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/THIRD-PARTY-NOTICES.md +4 -4
- package/dist/core/catalog-cache-invalidation.d.ts +16 -0
- package/dist/core/catalog-cache-invalidation.d.ts.map +1 -1
- package/dist/core/catalog-cache-invalidation.js +6 -1
- package/dist/core/catalog-cache-invalidation.js.map +1 -1
- package/dist/core/catalog-revision.d.ts +39 -0
- package/dist/core/catalog-revision.d.ts.map +1 -0
- package/dist/core/catalog-revision.js +94 -0
- package/dist/core/catalog-revision.js.map +1 -0
- package/dist/core/create-core-server.d.ts +7 -0
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +199 -75
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +49 -2
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +212 -86
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +138 -0
- package/dist/core/lifecycle.d.ts.map +1 -0
- package/dist/core/lifecycle.js +218 -0
- package/dist/core/lifecycle.js.map +1 -0
- package/dist/core/public-config.d.ts +82 -0
- package/dist/core/public-config.d.ts.map +1 -0
- package/dist/core/public-config.js +78 -0
- package/dist/core/public-config.js.map +1 -0
- package/dist/core/readiness.d.ts +86 -0
- package/dist/core/readiness.d.ts.map +1 -0
- package/dist/core/readiness.js +56 -0
- package/dist/core/readiness.js.map +1 -0
- package/dist/core-config.d.ts +30 -0
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +64 -2
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +215 -11
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +57 -11
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +616 -155
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-mutation.service.d.ts +71 -5
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +161 -13
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-view.d.ts +147 -0
- package/dist/modules/access/access-view.d.ts.map +1 -0
- package/dist/modules/access/access-view.js +211 -0
- package/dist/modules/access/access-view.js.map +1 -0
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +255 -79
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/admin-locked-commit.d.ts +2 -3
- package/dist/modules/access/admin-locked-commit.d.ts.map +1 -1
- package/dist/modules/access/admin-locked-commit.js +9 -8
- package/dist/modules/access/admin-locked-commit.js.map +1 -1
- package/dist/modules/access/admin-route-helpers.d.ts.map +1 -1
- package/dist/modules/access/admin-route-helpers.js +4 -4
- package/dist/modules/access/admin-route-helpers.js.map +1 -1
- package/dist/modules/access/change-read-gate.d.ts +53 -0
- package/dist/modules/access/change-read-gate.d.ts.map +1 -0
- package/dist/modules/access/change-read-gate.js +134 -0
- package/dist/modules/access/change-read-gate.js.map +1 -0
- package/dist/modules/access/creator-access.d.ts +33 -39
- package/dist/modules/access/creator-access.d.ts.map +1 -1
- package/dist/modules/access/creator-access.js +92 -119
- package/dist/modules/access/creator-access.js.map +1 -1
- package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
- package/dist/modules/access/directory-sync-bot.js +3 -1
- package/dist/modules/access/directory-sync-bot.js.map +1 -1
- package/dist/modules/access/groups-admin.service.d.ts +3 -3
- package/dist/modules/access/groups-admin.service.js +2 -2
- package/dist/modules/access/reference-scan.d.ts +1 -1
- package/dist/modules/access/reference-scan.d.ts.map +1 -1
- package/dist/modules/access/reference-scan.js +5 -4
- package/dist/modules/access/reference-scan.js.map +1 -1
- package/dist/modules/access/roles-admin.service.d.ts +72 -13
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +71 -13
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/access/synced-groups-committer.d.ts.map +1 -1
- package/dist/modules/access/synced-groups-committer.js +8 -6
- package/dist/modules/access/synced-groups-committer.js.map +1 -1
- package/dist/modules/access/synced-groups-writer.d.ts.map +1 -1
- package/dist/modules/access/synced-groups-writer.js +3 -8
- package/dist/modules/access/synced-groups-writer.js.map +1 -1
- package/dist/modules/access/user-access-removal.service.d.ts +209 -0
- package/dist/modules/access/user-access-removal.service.d.ts.map +1 -0
- package/dist/modules/access/user-access-removal.service.js +662 -0
- package/dist/modules/access/user-access-removal.service.js.map +1 -0
- package/dist/modules/access-model/access-errors.d.ts +32 -1
- package/dist/modules/access-model/access-errors.d.ts.map +1 -1
- package/dist/modules/access-model/access-errors.js +38 -11
- package/dist/modules/access-model/access-errors.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +49 -24
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +68 -48
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/access-model/access-splice.d.ts.map +1 -1
- package/dist/modules/access-model/access-splice.js +18 -18
- package/dist/modules/access-model/access-splice.js.map +1 -1
- package/dist/modules/access-model/access-template.d.ts +22 -0
- package/dist/modules/access-model/access-template.d.ts.map +1 -0
- package/dist/modules/access-model/access-template.js +42 -0
- package/dist/modules/access-model/access-template.js.map +1 -0
- package/dist/modules/access-model/change-gate.d.ts +65 -0
- package/dist/modules/access-model/change-gate.d.ts.map +1 -0
- package/dist/modules/access-model/change-gate.js +25 -0
- package/dist/modules/access-model/change-gate.js.map +1 -0
- package/dist/modules/access-model/creator.d.ts +13 -16
- package/dist/modules/access-model/creator.d.ts.map +1 -1
- package/dist/modules/access-model/creator.js.map +1 -1
- package/dist/modules/access-model/frontmatter-lines.d.ts +27 -0
- package/dist/modules/access-model/frontmatter-lines.d.ts.map +1 -0
- package/dist/modules/access-model/frontmatter-lines.js +74 -0
- package/dist/modules/access-model/frontmatter-lines.js.map +1 -0
- package/dist/modules/access-model/plugin-principals.d.ts +5 -3
- package/dist/modules/access-model/plugin-principals.d.ts.map +1 -1
- package/dist/modules/access-model/plugin-principals.js +11 -17
- package/dist/modules/access-model/plugin-principals.js.map +1 -1
- package/dist/modules/access-model/roles-yaml-guard.d.ts +73 -2
- package/dist/modules/access-model/roles-yaml-guard.d.ts.map +1 -1
- package/dist/modules/access-model/roles-yaml-guard.js +153 -3
- package/dist/modules/access-model/roles-yaml-guard.js.map +1 -1
- package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
- package/dist/modules/admin/admin-access.service.js +5 -2
- package/dist/modules/admin/admin-access.service.js.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +21 -0
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -0
- package/dist/modules/agent-instructions/agent-instructions.routes.js +38 -0
- package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -0
- package/dist/modules/agent-instructions/compose.d.ts +71 -0
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -0
- package/dist/modules/agent-instructions/compose.js +234 -0
- package/dist/modules/agent-instructions/compose.js.map +1 -0
- package/dist/modules/agent-instructions/index.d.ts +4 -0
- package/dist/modules/agent-instructions/index.d.ts.map +1 -0
- package/dist/modules/agent-instructions/index.js +4 -0
- package/dist/modules/agent-instructions/index.js.map +1 -0
- package/dist/modules/agent-instructions/read-preamble.d.ts +37 -0
- package/dist/modules/agent-instructions/read-preamble.d.ts.map +1 -0
- package/dist/modules/agent-instructions/read-preamble.js +81 -0
- package/dist/modules/agent-instructions/read-preamble.js.map +1 -0
- package/dist/modules/auth/account-erasure.service.d.ts +30 -4
- package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
- package/dist/modules/auth/account-erasure.service.js +96 -10
- package/dist/modules/auth/account-erasure.service.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts +6 -3
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +109 -10
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts +10 -3
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +9 -4
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +89 -5
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +160 -25
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.d.ts +46 -3
- package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.js +99 -21
- package/dist/modules/auth/oidc-auth-provider.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 +14 -2
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
- package/dist/modules/connection-probe/connection-probe.service.js +56 -7
- package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
- package/dist/modules/database/advisory-lock.d.ts +103 -0
- package/dist/modules/database/advisory-lock.d.ts.map +1 -0
- package/dist/modules/database/advisory-lock.js +320 -0
- package/dist/modules/database/advisory-lock.js.map +1 -0
- package/dist/modules/database/connection.d.ts.map +1 -1
- package/dist/modules/database/connection.js +9 -1
- package/dist/modules/database/connection.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +346 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +119 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/database/migrate.d.ts.map +1 -1
- package/dist/modules/database/migrate.js +26 -6
- package/dist/modules/database/migrate.js.map +1 -1
- package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
- package/dist/modules/declared-variables/declared-variables.routes.js +4 -2
- package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
- package/dist/modules/diff/diff.routes.d.ts.map +1 -1
- package/dist/modules/diff/diff.routes.js +2 -4
- package/dist/modules/diff/diff.routes.js.map +1 -1
- package/dist/modules/diff/diff.service.d.ts +4 -1
- package/dist/modules/diff/diff.service.d.ts.map +1 -1
- package/dist/modules/diff/diff.service.js +79 -81
- package/dist/modules/diff/diff.service.js.map +1 -1
- package/dist/modules/kb-fs/bevel-ignore.d.ts +35 -0
- package/dist/modules/kb-fs/bevel-ignore.d.ts.map +1 -0
- package/dist/modules/kb-fs/bevel-ignore.js +71 -0
- package/dist/modules/kb-fs/bevel-ignore.js.map +1 -0
- package/dist/modules/kb-fs/file-change-notifier.d.ts.map +1 -1
- package/dist/modules/kb-fs/file-change-notifier.js +3 -1
- package/dist/modules/kb-fs/file-change-notifier.js.map +1 -1
- package/dist/modules/kb-fs/git-guarded-filesystem.d.ts +37 -0
- package/dist/modules/kb-fs/git-guarded-filesystem.d.ts.map +1 -0
- package/dist/modules/kb-fs/git-guarded-filesystem.js +104 -0
- package/dist/modules/kb-fs/git-guarded-filesystem.js.map +1 -0
- package/dist/modules/kb-fs/locking-filesystem.d.ts +113 -18
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +353 -76
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/kb-fs/node-fs.d.ts +23 -0
- package/dist/modules/kb-fs/node-fs.d.ts.map +1 -0
- package/dist/modules/kb-fs/node-fs.js +158 -0
- package/dist/modules/kb-fs/node-fs.js.map +1 -0
- package/dist/modules/kb-fs/read-only-filesystem.d.ts +10 -9
- package/dist/modules/kb-fs/read-only-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/read-only-filesystem.js +18 -9
- package/dist/modules/kb-fs/read-only-filesystem.js.map +1 -1
- package/dist/modules/kb-fs/repo-path.d.ts +100 -3
- package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
- package/dist/modules/kb-fs/repo-path.js +195 -3
- package/dist/modules/kb-fs/repo-path.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.js +4 -2
- package/dist/modules/kb-sync/kb-sync.routes.js.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 +7 -5
- package/dist/modules/kb-sync/kb-sync.service.js.map +1 -1
- package/dist/modules/marketplace/git-http.routes.d.ts.map +1 -1
- package/dist/modules/marketplace/git-http.routes.js +6 -4
- package/dist/modules/marketplace/git-http.routes.js.map +1 -1
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts +12 -2
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.d.ts.map +1 -1
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js +32 -3
- package/dist/modules/marketplace/github-facade/github-facade-admin.routes.js.map +1 -1
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts +27 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.d.ts.map +1 -1
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js +45 -0
- package/dist/modules/marketplace/github-facade/github-facade-credentials.service.js.map +1 -1
- package/dist/modules/marketplace/github-facade/github-facade.routes.d.ts.map +1 -1
- package/dist/modules/marketplace/github-facade/github-facade.routes.js +30 -6
- package/dist/modules/marketplace/github-facade/github-facade.routes.js.map +1 -1
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts +21 -9
- package/dist/modules/marketplace/github-facade/github-facade.service.d.ts.map +1 -1
- package/dist/modules/marketplace/github-facade/github-facade.service.js +60 -19
- package/dist/modules/marketplace/github-facade/github-facade.service.js.map +1 -1
- package/dist/modules/marketplace/github-facade/index.d.ts +1 -1
- package/dist/modules/marketplace/github-facade/index.d.ts.map +1 -1
- package/dist/modules/marketplace/github-facade/index.js +1 -1
- package/dist/modules/marketplace/github-facade/index.js.map +1 -1
- package/dist/modules/marketplace/marketplace-repo.service.d.ts +14 -1
- package/dist/modules/marketplace/marketplace-repo.service.d.ts.map +1 -1
- package/dist/modules/marketplace/marketplace-repo.service.js +26 -8
- package/dist/modules/marketplace/marketplace-repo.service.js.map +1 -1
- package/dist/modules/mcp/downstream-pool.d.ts +107 -0
- package/dist/modules/mcp/downstream-pool.d.ts.map +1 -0
- package/dist/modules/mcp/downstream-pool.js +221 -0
- package/dist/modules/mcp/downstream-pool.js.map +1 -0
- package/dist/modules/mcp/downstream-token-refresh.d.ts +77 -0
- package/dist/modules/mcp/downstream-token-refresh.d.ts.map +1 -0
- package/dist/modules/mcp/downstream-token-refresh.js +166 -0
- package/dist/modules/mcp/downstream-token-refresh.js.map +1 -0
- package/dist/modules/mcp/first-call-probe.cli-args.d.ts +78 -0
- package/dist/modules/mcp/first-call-probe.cli-args.d.ts.map +1 -0
- package/dist/modules/mcp/first-call-probe.cli-args.js +151 -0
- package/dist/modules/mcp/first-call-probe.cli-args.js.map +1 -0
- package/dist/modules/mcp/first-call-probe.cli.d.ts +2 -0
- package/dist/modules/mcp/first-call-probe.cli.d.ts.map +1 -0
- package/dist/modules/mcp/first-call-probe.cli.js +62 -0
- package/dist/modules/mcp/first-call-probe.cli.js.map +1 -0
- package/dist/modules/mcp/first-call-probe.d.ts +165 -0
- package/dist/modules/mcp/first-call-probe.d.ts.map +1 -0
- package/dist/modules/mcp/first-call-probe.js +298 -0
- package/dist/modules/mcp/first-call-probe.js.map +1 -0
- package/dist/modules/mcp/manual-failure-memo.d.ts +42 -17
- package/dist/modules/mcp/manual-failure-memo.d.ts.map +1 -1
- package/dist/modules/mcp/manual-failure-memo.js +79 -23
- package/dist/modules/mcp/manual-failure-memo.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts +9 -5
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +24 -16
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.routes.d.ts +4 -4
- package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.routes.js +83 -179
- package/dist/modules/mcp/mcp.routes.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +218 -61
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +713 -159
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +4 -2
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js +3 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
- package/dist/modules/mcp/surface-log-throttle.d.ts +59 -0
- package/dist/modules/mcp/surface-log-throttle.d.ts.map +1 -0
- package/dist/modules/mcp/surface-log-throttle.js +94 -0
- package/dist/modules/mcp/surface-log-throttle.js.map +1 -0
- package/dist/modules/plugins/compile/compile-marketplace.d.ts +13 -6
- package/dist/modules/plugins/compile/compile-marketplace.d.ts.map +1 -1
- package/dist/modules/plugins/compile/compile-marketplace.js +119 -36
- package/dist/modules/plugins/compile/compile-marketplace.js.map +1 -1
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts +8 -1
- package/dist/modules/plugins/compile/marketplace-compiler.service.d.ts.map +1 -1
- package/dist/modules/plugins/compile/marketplace-compiler.service.js +9 -6
- package/dist/modules/plugins/compile/marketplace-compiler.service.js.map +1 -1
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.d.ts.map +1 -1
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js +32 -9
- package/dist/modules/plugins/discovery/bundle-dialect/bundle.source.js.map +1 -1
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts +26 -6
- package/dist/modules/plugins/discovery/bundle-dialect/registry.d.ts.map +1 -1
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js +71 -14
- package/dist/modules/plugins/discovery/bundle-dialect/registry.js.map +1 -1
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts +4 -2
- package/dist/modules/plugins/discovery/kb-plugin-source.d.ts.map +1 -1
- package/dist/modules/plugins/discovery/kb-plugin-source.js +9 -6
- package/dist/modules/plugins/discovery/kb-plugin-source.js.map +1 -1
- package/dist/modules/plugins/discovery/native.source.d.ts +2 -1
- package/dist/modules/plugins/discovery/native.source.d.ts.map +1 -1
- package/dist/modules/plugins/discovery/native.source.js +22 -4
- package/dist/modules/plugins/discovery/native.source.js.map +1 -1
- package/dist/modules/plugins/discovery/plugin-source.d.ts +17 -3
- package/dist/modules/plugins/discovery/plugin-source.d.ts.map +1 -1
- package/dist/modules/plugins/index.d.ts +6 -3
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js +6 -3
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/join-request-jobs.service.d.ts +250 -0
- package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -0
- package/dist/modules/plugins/join-request-jobs.service.js +602 -0
- package/dist/modules/plugins/join-request-jobs.service.js.map +1 -0
- package/dist/modules/plugins/join-request-records.store.d.ts +161 -0
- package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -0
- package/dist/modules/plugins/join-request-records.store.js +179 -0
- package/dist/modules/plugins/join-request-records.store.js.map +1 -0
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
- package/dist/modules/plugins/join-requests.service.js +4 -2
- package/dist/modules/plugins/join-requests.service.js.map +1 -1
- package/dist/modules/plugins/plugin-links.d.ts +4 -1
- package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-links.js +6 -3
- package/dist/modules/plugins/plugin-links.js.map +1 -1
- package/dist/modules/plugins/plugin-links.service.d.ts +2 -5
- package/dist/modules/plugins/plugin-links.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-links.service.js +4 -13
- package/dist/modules/plugins/plugin-links.service.js.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.d.ts +91 -19
- package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-provision.service.js +183 -38
- package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
- package/dist/modules/plugins/plugin-rename.service.d.ts +3 -1
- package/dist/modules/plugins/plugin-rename.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-rename.service.js +20 -11
- package/dist/modules/plugins/plugin-rename.service.js.map +1 -1
- package/dist/modules/plugins/plugins.contract.d.ts +52 -4
- package/dist/modules/plugins/plugins.contract.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +38 -5
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +190 -116
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/plugins/plugins.service.d.ts +38 -6
- package/dist/modules/plugins/plugins.service.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.service.js +86 -17
- package/dist/modules/plugins/plugins.service.js.map +1 -1
- package/dist/modules/plugins/plugins.tools.d.ts +25 -0
- package/dist/modules/plugins/plugins.tools.d.ts.map +1 -0
- package/dist/modules/plugins/plugins.tools.js +82 -0
- package/dist/modules/plugins/plugins.tools.js.map +1 -0
- package/dist/modules/plugins/teams.routes.d.ts +42 -0
- package/dist/modules/plugins/teams.routes.d.ts.map +1 -0
- package/dist/modules/plugins/teams.routes.js +79 -0
- package/dist/modules/plugins/teams.routes.js.map +1 -0
- package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +40 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.js +202 -37
- package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
- package/dist/modules/secrets-vault/index.d.ts +1 -1
- package/dist/modules/secrets-vault/index.d.ts.map +1 -1
- package/dist/modules/secrets-vault/index.js.map +1 -1
- package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts.map +1 -1
- package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js +5 -3
- package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-variable-loader.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-variable-loader.js +3 -1
- package/dist/modules/secrets-vault/secrets-variable-loader.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +78 -0
- package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.contract.js +40 -0
- package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js +106 -52
- package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
- package/dist/modules/settings/connection-check.d.ts +118 -0
- package/dist/modules/settings/connection-check.d.ts.map +1 -0
- package/dist/modules/settings/connection-check.js +287 -0
- package/dist/modules/settings/connection-check.js.map +1 -0
- package/dist/modules/settings/deployment-settings.service.d.ts +153 -7
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +348 -51
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/git-root-folders.d.ts +54 -0
- package/dist/modules/settings/git-root-folders.d.ts.map +1 -0
- package/dist/modules/settings/git-root-folders.js +130 -0
- package/dist/modules/settings/git-root-folders.js.map +1 -0
- package/dist/modules/settings/oidc-check.d.ts +85 -0
- package/dist/modules/settings/oidc-check.d.ts.map +1 -0
- package/dist/modules/settings/oidc-check.js +172 -0
- package/dist/modules/settings/oidc-check.js.map +1 -0
- package/dist/modules/settings/setup.routes.d.ts +31 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +400 -108
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/allowed-tools-check.d.ts +132 -0
- package/dist/modules/skills/allowed-tools-check.d.ts.map +1 -0
- package/dist/modules/skills/allowed-tools-check.js +260 -0
- package/dist/modules/skills/allowed-tools-check.js.map +1 -0
- package/dist/modules/skills/index.d.ts +3 -1
- package/dist/modules/skills/index.d.ts.map +1 -1
- package/dist/modules/skills/index.js +1 -0
- package/dist/modules/skills/index.js.map +1 -1
- package/dist/modules/skills/pending-skills.service.d.ts +6 -11
- package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
- package/dist/modules/skills/pending-skills.service.js +18 -70
- package/dist/modules/skills/pending-skills.service.js.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.js +9 -6
- package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +44 -10
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.d.ts +8 -1
- package/dist/modules/skills/skills.routes.d.ts.map +1 -1
- package/dist/modules/skills/skills.routes.js +16 -2
- package/dist/modules/skills/skills.routes.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +31 -7
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +191 -81
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts +4 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +41 -6
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/connection-key-rejection.d.ts +18 -0
- package/dist/modules/tool-auth/connection-key-rejection.d.ts.map +1 -0
- package/dist/modules/tool-auth/connection-key-rejection.js +20 -0
- package/dist/modules/tool-auth/connection-key-rejection.js.map +1 -0
- package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts +14 -0
- package/dist/modules/tool-auth/connection-keys-admin.routes.d.ts.map +1 -0
- package/dist/modules/tool-auth/connection-keys-admin.routes.js +67 -0
- package/dist/modules/tool-auth/connection-keys-admin.routes.js.map +1 -0
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +51 -3
- package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts +16 -6
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +61 -18
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +5 -2
- package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.js +5 -2
- package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
- package/dist/modules/tool-auth/key-or-session.middleware.d.ts +23 -0
- package/dist/modules/tool-auth/key-or-session.middleware.d.ts.map +1 -0
- package/dist/modules/tool-auth/key-or-session.middleware.js +39 -0
- package/dist/modules/tool-auth/key-or-session.middleware.js.map +1 -0
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +7 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +8 -5
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/tool-context.d.ts +14 -0
- package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-context.js +28 -6
- package/dist/modules/tool-helpers/tool-context.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +22 -5
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +6 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.js +7 -1
- package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
- package/dist/modules/tool-manuals/index.d.ts +2 -1
- package/dist/modules/tool-manuals/index.d.ts.map +1 -1
- package/dist/modules/tool-manuals/index.js +1 -0
- package/dist/modules/tool-manuals/index.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.js +8 -6
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +10 -2
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +12 -15
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
- package/dist/modules/tool-manuals/pending-tools.service.d.ts +50 -0
- package/dist/modules/tool-manuals/pending-tools.service.d.ts.map +1 -0
- package/dist/modules/tool-manuals/pending-tools.service.js +149 -0
- package/dist/modules/tool-manuals/pending-tools.service.js.map +1 -0
- package/dist/modules/tool-manuals/tool-delete.service.d.ts +135 -0
- package/dist/modules/tool-manuals/tool-delete.service.d.ts.map +1 -0
- package/dist/modules/tool-manuals/tool-delete.service.js +303 -0
- package/dist/modules/tool-manuals/tool-delete.service.js.map +1 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +161 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js +9 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +19 -2
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +127 -57
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +119 -5
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +404 -74
- package/dist/modules/tool-manuals/tool-manuals.service.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 +78 -6
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +88 -54
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/event-bus.d.ts +10 -4
- package/dist/modules/workflow/event-bus.d.ts.map +1 -1
- package/dist/modules/workflow/event-bus.js +14 -11
- package/dist/modules/workflow/event-bus.js.map +1 -1
- package/dist/modules/workflow/events.routes.d.ts +17 -2
- package/dist/modules/workflow/events.routes.d.ts.map +1 -1
- package/dist/modules/workflow/events.routes.js +31 -12
- package/dist/modules/workflow/events.routes.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +22 -4
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +27 -4
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/change-request-link.d.ts +20 -0
- package/dist/modules/workflow/git/change-request-link.d.ts.map +1 -0
- package/dist/modules/workflow/git/change-request-link.js +34 -0
- package/dist/modules/workflow/git/change-request-link.js.map +1 -0
- package/dist/modules/workflow/git/git-version.d.ts.map +1 -1
- package/dist/modules/workflow/git/git-version.js +3 -1
- package/dist/modules/workflow/git/git-version.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +263 -4
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +528 -118
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.d.ts +36 -0
- package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -0
- package/dist/modules/workflow/git/node-git-runner.js +273 -0
- package/dist/modules/workflow/git/node-git-runner.js.map +1 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts +51 -2
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +157 -25
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +11 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +24 -4
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts +15 -8
- package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.js +107 -58
- package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
- package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
- package/dist/modules/workflow/recovery-bot.js +3 -1
- package/dist/modules/workflow/recovery-bot.js.map +1 -1
- package/dist/modules/workflow/review-workflow/approval-lock.d.ts +60 -0
- package/dist/modules/workflow/review-workflow/approval-lock.d.ts.map +1 -0
- package/dist/modules/workflow/review-workflow/approval-lock.js +116 -0
- package/dist/modules/workflow/review-workflow/approval-lock.js.map +1 -0
- package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts +54 -7
- package/dist/modules/workflow/review-workflow/review-workflow.interface.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +64 -0
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +269 -90
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/sanitize-error.d.ts +5 -2
- package/dist/modules/workflow/sanitize-error.d.ts.map +1 -1
- package/dist/modules/workflow/sanitize-error.js +8 -4
- package/dist/modules/workflow/sanitize-error.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts +2 -0
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +259 -29
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +166 -14
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +906 -106
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/empty-dirs.d.ts +15 -0
- package/dist/modules/workspace/empty-dirs.d.ts.map +1 -0
- package/dist/modules/workspace/empty-dirs.js +90 -0
- package/dist/modules/workspace/empty-dirs.js.map +1 -0
- package/dist/modules/workspace/file-readers/content-mode.d.ts +50 -0
- package/dist/modules/workspace/file-readers/content-mode.d.ts.map +1 -0
- package/dist/modules/workspace/file-readers/content-mode.js +55 -0
- package/dist/modules/workspace/file-readers/content-mode.js.map +1 -0
- package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +4 -1
- package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/doc-extract.service.js +4 -1
- package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
- package/dist/modules/workspace/file-readers/doc-extract.types.d.ts +5 -2
- package/dist/modules/workspace/file-readers/doc-extract.types.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/doc-extract.types.js +7 -4
- package/dist/modules/workspace/file-readers/doc-extract.types.js.map +1 -1
- package/dist/modules/workspace/file-readers/document-reader.d.ts +2 -0
- package/dist/modules/workspace/file-readers/document-reader.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/document-reader.js +5 -0
- package/dist/modules/workspace/file-readers/document-reader.js.map +1 -1
- package/dist/modules/workspace/file-readers/extract-docx.d.ts +6 -0
- package/dist/modules/workspace/file-readers/extract-docx.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/extract-docx.js +6 -0
- package/dist/modules/workspace/file-readers/extract-docx.js.map +1 -1
- package/dist/modules/workspace/file-readers/file-reader.d.ts +24 -5
- package/dist/modules/workspace/file-readers/file-reader.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/file-reader.js +11 -6
- package/dist/modules/workspace/file-readers/file-reader.js.map +1 -1
- package/dist/modules/workspace/file-readers/file-reader.registry.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/file-reader.registry.js +6 -1
- package/dist/modules/workspace/file-readers/file-reader.registry.js.map +1 -1
- package/dist/modules/workspace/file-readers/image-reader.d.ts +7 -5
- package/dist/modules/workspace/file-readers/image-reader.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/image-reader.js +9 -5
- package/dist/modules/workspace/file-readers/image-reader.js.map +1 -1
- package/dist/modules/workspace/file-readers/ooxml-text.d.ts +3 -0
- package/dist/modules/workspace/file-readers/ooxml-text.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/ooxml-text.js +39 -2
- package/dist/modules/workspace/file-readers/ooxml-text.js.map +1 -1
- package/dist/modules/workspace/file-readers/text-reader.d.ts +29 -1
- package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/text-reader.js +57 -3
- package/dist/modules/workspace/file-readers/text-reader.js.map +1 -1
- package/dist/modules/workspace/git-internals.middleware.d.ts +28 -0
- package/dist/modules/workspace/git-internals.middleware.d.ts.map +1 -0
- package/dist/modules/workspace/git-internals.middleware.js +93 -0
- package/dist/modules/workspace/git-internals.middleware.js.map +1 -0
- package/dist/modules/workspace/not-found.d.ts +80 -0
- package/dist/modules/workspace/not-found.d.ts.map +1 -0
- package/dist/modules/workspace/not-found.js +112 -0
- package/dist/modules/workspace/not-found.js.map +1 -0
- package/dist/modules/workspace/session-ontology.gate.d.ts +1 -1
- package/dist/modules/workspace/session-ontology.gate.d.ts.map +1 -1
- package/dist/modules/workspace/session-ontology.gate.js +1 -1
- package/dist/modules/workspace/session-ontology.gate.js.map +1 -1
- package/dist/modules/workspace/startup/beside-checkout.d.ts +42 -0
- package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -0
- package/dist/modules/workspace/startup/beside-checkout.js +110 -0
- package/dist/modules/workspace/startup/beside-checkout.js.map +1 -0
- package/dist/modules/workspace/startup/kb-git.d.ts +7 -12
- package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-git.js +34 -35
- package/dist/modules/workspace/startup/kb-git.js.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +95 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +257 -55
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +45 -0
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +417 -385
- package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -1
- package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts +48 -0
- package/dist/modules/workspace/startup/steps/personal-spaces.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/personal-spaces.step.js +183 -0
- package/dist/modules/workspace/startup/steps/personal-spaces.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts +51 -0
- package/dist/modules/workspace/startup/steps/plugin-display-names.step.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-display-names.step.js +158 -0
- package/dist/modules/workspace/startup/steps/plugin-display-names.step.js.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-layout.d.ts +64 -0
- package/dist/modules/workspace/startup/steps/plugin-layout.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-layout.js +97 -0
- package/dist/modules/workspace/startup/steps/plugin-layout.js.map +1 -0
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts +7 -1
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js +45 -81
- package/dist/modules/workspace/startup/steps/plugin-manifests.step.js.map +1 -1
- package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +3 -1
- package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/roles-yaml.step.js +4 -12
- package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts +6 -1
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +154 -78
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts +96 -24
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +446 -138
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.d.ts +94 -0
- package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -0
- package/dist/modules/workspace/startup/steps/template-source.js +160 -0
- package/dist/modules/workspace/startup/steps/template-source.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.d.ts +14 -2
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +649 -238
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +290 -12
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +924 -269
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +42 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +1590 -146
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/workspace/write-denial.d.ts +76 -0
- package/dist/modules/workspace/write-denial.d.ts.map +1 -0
- package/dist/modules/workspace/write-denial.js +138 -0
- package/dist/modules/workspace/write-denial.js.map +1 -0
- package/dist/shared/canonical-file-identity.d.ts +64 -0
- package/dist/shared/canonical-file-identity.d.ts.map +1 -0
- package/dist/shared/canonical-file-identity.js +92 -0
- package/dist/shared/canonical-file-identity.js.map +1 -0
- package/dist/shared/domain-errors.d.ts +108 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +137 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/email-identity.d.ts +32 -0
- package/dist/shared/email-identity.d.ts.map +1 -0
- package/dist/shared/email-identity.js +37 -0
- package/dist/shared/email-identity.js.map +1 -0
- package/dist/shared/fs.contract.d.ts +206 -0
- package/dist/shared/fs.contract.d.ts.map +1 -0
- package/dist/shared/fs.contract.js +79 -0
- package/dist/shared/fs.contract.js.map +1 -0
- package/dist/shared/git-failure.d.ts +80 -0
- package/dist/shared/git-failure.d.ts.map +1 -0
- package/dist/shared/git-failure.js +139 -0
- package/dist/shared/git-failure.js.map +1 -0
- package/dist/shared/git-internals.d.ts +17 -0
- package/dist/shared/git-internals.d.ts.map +1 -0
- package/dist/shared/git-internals.js +123 -0
- package/dist/shared/git-internals.js.map +1 -0
- package/dist/shared/git.contract.d.ts +157 -0
- package/dist/shared/git.contract.d.ts.map +1 -0
- package/dist/shared/git.contract.js +103 -0
- package/dist/shared/git.contract.js.map +1 -0
- package/dist/shared/http-errors.d.ts +33 -0
- package/dist/shared/http-errors.d.ts.map +1 -0
- package/dist/shared/http-errors.js +35 -0
- package/dist/shared/http-errors.js.map +1 -0
- package/dist/shared/logger.contract.d.ts +63 -0
- package/dist/shared/logger.contract.d.ts.map +1 -0
- package/dist/shared/logger.contract.js +126 -0
- package/dist/shared/logger.contract.js.map +1 -0
- package/dist/shared/logging.d.ts +31 -0
- package/dist/shared/logging.d.ts.map +1 -0
- package/dist/shared/logging.js +92 -0
- package/dist/shared/logging.js.map +1 -0
- package/dist/shared/path-containment.d.ts +21 -0
- package/dist/shared/path-containment.d.ts.map +1 -0
- package/dist/shared/path-containment.js +34 -0
- package/dist/shared/path-containment.js.map +1 -0
- package/dist/shared/pending-proposals.d.ts +71 -0
- package/dist/shared/pending-proposals.d.ts.map +1 -0
- package/dist/shared/pending-proposals.js +98 -0
- package/dist/shared/pending-proposals.js.map +1 -0
- package/dist/shared/printable.d.ts +22 -0
- package/dist/shared/printable.d.ts.map +1 -0
- package/dist/shared/printable.js +65 -0
- package/dist/shared/printable.js.map +1 -0
- package/dist/shared/redact-secret.d.ts +30 -0
- package/dist/shared/redact-secret.d.ts.map +1 -0
- package/dist/shared/redact-secret.js +114 -0
- package/dist/shared/redact-secret.js.map +1 -0
- package/dist/shared/rename-no-replace.d.ts +119 -0
- package/dist/shared/rename-no-replace.d.ts.map +1 -0
- package/dist/shared/rename-no-replace.js +323 -0
- package/dist/shared/rename-no-replace.js.map +1 -0
- package/dist/shared/ssrf.d.ts.map +1 -1
- package/dist/shared/ssrf.js +49 -18
- package/dist/shared/ssrf.js.map +1 -1
- package/dist/version.d.ts +10 -6
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +10 -6
- package/dist/version.js.map +1 -1
- package/kb-template/.bevelignore +4 -3
- package/kb-template/AGENTS.md +281 -36
- package/kb-template/KnowledgeBase/How to get started.md +1 -1
- package/kb-template/access.md +5 -3
- package/kb-template/mcp-description.md +27 -0
- package/migrations/0006_api_tokens_revoked_by.sql +1 -0
- package/migrations/0007_github_facade_registered.sql +1 -0
- package/migrations/0008_change_request_apply_failure.sql +4 -0
- package/migrations/0009_change_request_apply_failure_kind.sql +13 -0
- package/migrations/0010_plugin_join_requests.sql +15 -0
- package/migrations/0011_plugin_join_request_claim.sql +11 -0
- package/migrations/meta/0006_snapshot.json +1723 -0
- package/migrations/meta/0007_snapshot.json +1729 -0
- package/migrations/meta/0008_snapshot.json +1753 -0
- package/migrations/meta/0009_snapshot.json +1759 -0
- package/migrations/meta/0010_snapshot.json +1872 -0
- package/migrations/meta/0011_snapshot.json +1884 -0
- package/migrations/meta/_journal.json +42 -0
- package/package.json +6 -5
- package/src/__tests__/core-config.domain.test.ts +67 -2
- package/src/__tests__/kb-layout-config.test.ts +186 -1
- package/src/__tests__/open-change-gate.ts +15 -0
- package/src/core/__tests__/catalog-cache-invalidation.test.ts +83 -3
- package/src/core/__tests__/catalog-revision.test.ts +190 -0
- package/src/core/__tests__/lifecycle.test.ts +369 -0
- package/src/core/__tests__/public-config.test.ts +30 -0
- package/src/core/__tests__/readiness.test.ts +109 -0
- package/src/core/catalog-cache-invalidation.ts +20 -1
- package/src/core/catalog-revision.ts +110 -0
- package/src/core/create-core-server.ts +258 -79
- package/src/core/create-core-services.ts +302 -81
- package/src/core/lifecycle.ts +312 -0
- package/src/core/public-config.ts +86 -0
- package/src/core/readiness.ts +123 -0
- package/src/core-config.ts +70 -4
- package/src/index.ts +15 -0
- package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +362 -0
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +2 -1
- package/src/modules/access/__tests__/access-control.atref-cache.test.ts +46 -22
- package/src/modules/access/__tests__/access-control.deny-dependents.test.ts +215 -0
- package/src/modules/access/__tests__/access-control.download-read.test.ts +268 -0
- package/src/modules/access/__tests__/access-control.everyone-read.test.ts +145 -0
- package/src/modules/access/__tests__/access-control.group-read.test.ts +163 -0
- package/src/modules/access/__tests__/access-control.invalidate-race.test.ts +2 -0
- package/src/modules/access/__tests__/access-control.platform-restore.test.ts +152 -0
- package/src/modules/access/__tests__/access-control.prospective.test.ts +189 -0
- package/src/modules/access/__tests__/access-control.read-batch.test.ts +2 -1
- package/src/modules/access/__tests__/access-control.service.test.ts +123 -78
- package/src/modules/access/__tests__/access-groups.test.ts +47 -14
- package/src/modules/access/__tests__/access-md-format.test.ts +2 -1
- package/src/modules/access/__tests__/access-mutation.service.test.ts +52 -4
- package/src/modules/access/__tests__/access-own-read-grant.test.ts +2 -0
- package/src/modules/access/__tests__/access-personal-plugin.test.ts +153 -0
- package/src/modules/access/__tests__/access-plugin-principals.test.ts +18 -1
- package/src/modules/access/__tests__/access-restrict-here.test.ts +450 -0
- package/src/modules/access/__tests__/access.routes.batch.test.ts +139 -0
- package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +394 -0
- package/src/modules/access/__tests__/access.routes.group-grant.test.ts +6 -2
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +3 -2
- package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +234 -0
- package/src/modules/access/__tests__/access.routes.prospective.test.ts +216 -0
- package/src/modules/access/__tests__/access.routes.revoke.test.ts +7 -2
- package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +360 -0
- package/src/modules/access/__tests__/change-read-gate.test.ts +272 -0
- package/src/modules/access/__tests__/creator-access.test.ts +81 -83
- package/src/modules/access/__tests__/grant-sources.test.ts +3 -2
- package/src/modules/access/__tests__/groups-admin.service.test.ts +6 -5
- package/src/modules/access/__tests__/roles-admin.security.test.ts +196 -0
- package/src/modules/access/__tests__/roles-admin.service.test.ts +127 -2
- package/src/modules/access/__tests__/roles-capabilities.test.ts +5 -4
- package/src/modules/access/__tests__/roles.routes.test.ts +5 -2
- package/src/modules/access/__tests__/user-access-removal.service.test.ts +402 -0
- package/src/modules/access/__tests__/users-db-double.ts +63 -0
- package/src/modules/access/access-control.interface.ts +251 -11
- package/src/modules/access/access-control.service.ts +719 -164
- package/src/modules/access/access-mutation.service.ts +182 -13
- package/src/modules/access/access-view.ts +316 -0
- package/src/modules/access/access.routes.ts +293 -95
- package/src/modules/access/admin-locked-commit.ts +12 -14
- package/src/modules/access/admin-route-helpers.ts +4 -4
- package/src/modules/access/change-read-gate.ts +148 -0
- package/src/modules/access/creator-access.ts +89 -124
- package/src/modules/access/directory-sync-bot.ts +4 -1
- package/src/modules/access/groups-admin.service.ts +3 -3
- package/src/modules/access/reference-scan.ts +5 -5
- package/src/modules/access/roles-admin.service.ts +94 -15
- package/src/modules/access/synced-groups-committer.ts +10 -15
- package/src/modules/access/synced-groups-writer.ts +3 -12
- package/src/modules/access/user-access-removal.service.ts +711 -0
- package/src/modules/access-model/__tests__/access-splice.test.ts +63 -1
- package/src/modules/access-model/__tests__/frontmatter-fences.test.ts +71 -0
- package/src/modules/access-model/__tests__/roles-yaml-guard.test.ts +131 -0
- package/src/modules/access-model/access-errors.ts +56 -15
- package/src/modules/access-model/access-grammar.ts +85 -48
- package/src/modules/access-model/access-splice.ts +18 -18
- package/src/modules/access-model/access-template.ts +44 -0
- package/src/modules/access-model/change-gate.ts +80 -0
- package/src/modules/access-model/creator.ts +18 -21
- package/src/modules/access-model/frontmatter-lines.ts +98 -0
- package/src/modules/access-model/plugin-principals.ts +12 -19
- package/src/modules/access-model/roles-yaml-guard.ts +182 -4
- package/src/modules/admin/admin-access.service.ts +6 -2
- package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +100 -0
- package/src/modules/agent-instructions/__tests__/compose.test.ts +256 -0
- package/src/modules/agent-instructions/__tests__/read-preamble.test.ts +135 -0
- package/src/modules/agent-instructions/agent-instructions.routes.ts +41 -0
- package/src/modules/agent-instructions/compose.ts +271 -0
- package/src/modules/agent-instructions/index.ts +14 -0
- package/src/modules/agent-instructions/read-preamble.ts +88 -0
- package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +209 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +178 -2
- package/src/modules/auth/__tests__/auth.middleware.test.ts +78 -0
- package/src/modules/auth/__tests__/auth.service.test.ts +338 -5
- package/src/modules/auth/__tests__/oidc-auth-provider.test.ts +352 -12
- package/src/modules/auth/__tests__/onboarding-done.route.test.ts +3 -1
- package/src/modules/auth/account-erasure.service.ts +117 -10
- package/src/modules/auth/account.routes.ts +115 -14
- package/src/modules/auth/auth.routes.ts +20 -7
- package/src/modules/auth/auth.service.ts +187 -28
- package/src/modules/auth/oidc-auth-provider.ts +120 -22
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +109 -1
- package/src/modules/code-mode/code-mode.tool.ts +13 -2
- package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +78 -0
- package/src/modules/connection-probe/connection-probe.service.ts +59 -7
- package/src/modules/database/__tests__/advisory-lease.test.ts +152 -0
- package/src/modules/database/__tests__/advisory-lock.test.ts +147 -0
- package/src/modules/database/advisory-lock.ts +346 -0
- package/src/modules/database/connection.ts +9 -1
- package/src/modules/database/core-schema.ts +123 -0
- package/src/modules/database/migrate.ts +27 -6
- package/src/modules/declared-variables/declared-variables.routes.ts +6 -3
- package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +3 -0
- package/src/modules/diff/__tests__/diff.service.test.ts +3 -0
- package/src/modules/diff/diff.routes.ts +2 -4
- package/src/modules/diff/diff.service.ts +75 -79
- package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +472 -9
- package/src/modules/kb-fs/__tests__/node-fs.test.ts +409 -0
- package/src/modules/kb-fs/__tests__/repo-path.test.ts +210 -1
- package/src/modules/{workspace → kb-fs}/bevel-ignore.ts +22 -15
- package/src/modules/kb-fs/file-change-notifier.ts +4 -1
- package/src/modules/kb-fs/git-guarded-filesystem.ts +123 -0
- package/src/modules/kb-fs/locking-filesystem.ts +399 -109
- package/src/modules/kb-fs/node-fs.ts +165 -0
- package/src/modules/kb-fs/read-only-filesystem.ts +18 -9
- package/src/modules/kb-fs/repo-path.ts +213 -3
- package/src/modules/kb-sync/kb-sync.routes.ts +5 -2
- package/src/modules/kb-sync/kb-sync.service.ts +8 -5
- package/src/modules/marketplace/__tests__/github-facade.test.ts +134 -2
- package/src/modules/marketplace/__tests__/marketplace-git.test.ts +2 -1
- package/src/modules/marketplace/__tests__/marketplace-repo.head.test.ts +47 -0
- package/src/modules/marketplace/git-http.routes.ts +7 -4
- package/src/modules/marketplace/github-facade/github-facade-admin.routes.ts +35 -3
- package/src/modules/marketplace/github-facade/github-facade-credentials.service.ts +55 -0
- package/src/modules/marketplace/github-facade/github-facade.routes.ts +32 -6
- package/src/modules/marketplace/github-facade/github-facade.service.ts +80 -19
- package/src/modules/marketplace/github-facade/index.ts +1 -0
- package/src/modules/marketplace/marketplace-repo.service.ts +23 -8
- package/src/modules/mcp/__tests__/downstream-pool.test.ts +278 -0
- package/src/modules/mcp/__tests__/downstream-token-refresh.test.ts +178 -0
- package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +156 -0
- package/src/modules/mcp/__tests__/first-call-probe.cli-args.test.ts +178 -0
- package/src/modules/mcp/__tests__/first-call-probe.test.ts +414 -0
- package/src/modules/mcp/__tests__/manual-failure-memo.test.ts +98 -6
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +8 -5
- package/src/modules/mcp/__tests__/mcp-routes-harness.ts +1 -3
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +1055 -59
- package/src/modules/mcp/__tests__/mcp.routes.stateless.test.ts +182 -0
- package/src/modules/mcp/__tests__/mcp.service.test.ts +274 -6
- package/src/modules/mcp/__tests__/surface-log-throttle.test.ts +169 -0
- package/src/modules/mcp/downstream-pool.ts +256 -0
- package/src/modules/mcp/downstream-token-refresh.ts +170 -0
- package/src/modules/mcp/first-call-probe.cli-args.ts +159 -0
- package/src/modules/mcp/first-call-probe.cli.ts +60 -0
- package/src/modules/mcp/first-call-probe.ts +418 -0
- package/src/modules/mcp/manual-failure-memo.ts +81 -23
- package/src/modules/mcp/mcp-auth.middleware.ts +25 -16
- package/src/modules/mcp/mcp.routes.ts +376 -482
- package/src/modules/mcp/mcp.service.ts +846 -176
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +5 -2
- package/src/modules/mcp/oauth/oauth-consent.routes.ts +4 -4
- package/src/modules/mcp/surface-log-throttle.ts +120 -0
- package/src/modules/plugins/__tests__/bundle-dialect.e2e.test.ts +10 -7
- package/src/modules/plugins/__tests__/bundle-dialect.test.ts +165 -0
- package/src/modules/plugins/__tests__/compile-marketplace.test.ts +139 -10
- package/src/modules/plugins/__tests__/fake-join-request-store.ts +189 -0
- package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +608 -0
- package/src/modules/plugins/__tests__/plugin-creation-display-name.test.ts +241 -0
- package/src/modules/plugins/__tests__/plugin-index.service.test.ts +148 -7
- package/src/modules/plugins/__tests__/plugin-links.service.test.ts +10 -13
- package/src/modules/plugins/__tests__/plugin-names.test.ts +77 -0
- package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +129 -19
- package/src/modules/plugins/__tests__/plugin-rename.service.test.ts +91 -29
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +370 -19
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +131 -0
- package/src/modules/plugins/__tests__/teams.routes.test.ts +248 -0
- package/src/modules/plugins/compile/compile-marketplace.ts +143 -46
- package/src/modules/plugins/compile/marketplace-compiler.service.ts +8 -7
- package/src/modules/plugins/discovery/__tests__/kb-plugin-source.test.ts +56 -20
- package/src/modules/plugins/discovery/bundle-dialect/bundle.source.ts +30 -9
- package/src/modules/plugins/discovery/bundle-dialect/registry.ts +93 -18
- package/src/modules/plugins/discovery/kb-plugin-source.ts +10 -7
- package/src/modules/plugins/discovery/native.source.ts +21 -3
- package/src/modules/plugins/discovery/plugin-source.ts +17 -3
- package/src/modules/plugins/index.ts +16 -3
- package/src/modules/plugins/join-request-jobs.service.ts +675 -0
- package/src/modules/plugins/join-request-records.store.ts +353 -0
- package/src/modules/plugins/join-requests.service.ts +7 -4
- package/src/modules/plugins/plugin-links.service.ts +4 -17
- package/src/modules/plugins/plugin-links.ts +6 -3
- package/src/modules/plugins/plugin-provision.service.ts +209 -37
- package/src/modules/plugins/plugin-rename.service.ts +21 -9
- package/src/modules/plugins/plugins.contract.ts +52 -4
- package/src/modules/plugins/plugins.routes.ts +204 -124
- package/src/modules/plugins/plugins.service.ts +92 -18
- package/src/modules/plugins/plugins.tools.ts +88 -0
- package/src/modules/plugins/teams.routes.ts +129 -0
- package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +359 -15
- package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +248 -0
- package/src/modules/secrets-vault/__tests__/namespace-secrets.test.ts +202 -0
- package/src/modules/secrets-vault/db-secrets-vault.service.ts +217 -35
- package/src/modules/secrets-vault/index.ts +1 -0
- package/src/modules/secrets-vault/mcp-oauth-discovery.service.ts +6 -3
- package/src/modules/secrets-vault/secrets-variable-loader.ts +4 -4
- package/src/modules/secrets-vault/secrets-vault.contract.ts +93 -0
- package/src/modules/secrets-vault/secrets-vault.routes.ts +99 -38
- package/src/modules/settings/__tests__/connection-check.test.ts +233 -0
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +188 -6
- package/src/modules/settings/__tests__/git-root-folders.test.ts +247 -0
- package/src/modules/settings/__tests__/oidc-check.test.ts +185 -0
- package/src/modules/settings/__tests__/setup.routes.boot-failure.test.ts +119 -0
- package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +170 -0
- package/src/modules/settings/__tests__/setup.routes.oidc.test.ts +555 -0
- package/src/modules/settings/__tests__/setup.routes.test.ts +667 -16
- package/src/modules/settings/connection-check.ts +351 -0
- package/src/modules/settings/deployment-settings.service.ts +403 -57
- package/src/modules/settings/git-root-folders.ts +171 -0
- package/src/modules/settings/oidc-check.ts +229 -0
- package/src/modules/settings/setup.routes.ts +456 -110
- package/src/modules/skills/__tests__/allowed-tools-check.test.ts +224 -0
- package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +210 -0
- package/src/modules/skills/__tests__/pending-skills.service.test.ts +3 -3
- package/src/modules/skills/__tests__/skills.routes.warnings.test.ts +91 -0
- package/src/modules/skills/__tests__/skills.service.test.ts +223 -2
- package/src/modules/skills/allowed-tools-check.ts +352 -0
- package/src/modules/skills/index.ts +4 -0
- package/src/modules/skills/pending-skills.service.ts +21 -82
- package/src/modules/skills/skill-access-requests.routes.ts +10 -6
- package/src/modules/skills/skills.contract.ts +43 -11
- package/src/modules/skills/skills.routes.ts +16 -1
- package/src/modules/skills/skills.service.ts +214 -84
- package/src/modules/skills/skills.tools.ts +43 -5
- package/src/modules/tool-auth/__tests__/connection-key-rejection.test.ts +192 -0
- package/src/modules/tool-auth/__tests__/connection-keys-admin.routes.test.ts +116 -0
- package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +124 -0
- package/src/modules/tool-auth/connection-key-rejection.ts +25 -0
- package/src/modules/tool-auth/connection-keys-admin.routes.ts +76 -0
- package/src/modules/tool-auth/external-api-key.interface.ts +56 -3
- package/src/modules/tool-auth/external-api-key.service.ts +73 -25
- package/src/modules/tool-auth/internal-token.service.ts +5 -2
- package/src/modules/tool-auth/key-or-session.middleware.ts +45 -0
- package/src/modules/tool-auth/tool-auth.middleware.ts +16 -7
- package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +228 -0
- package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +33 -4
- package/src/modules/tool-helpers/tool-context.ts +46 -7
- package/src/modules/tool-helpers/tool-handler.ts +23 -5
- package/src/modules/tool-helpers/tool.contract.ts +5 -0
- package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +2 -1
- package/src/modules/tool-manuals/__tests__/pending-tools.service.test.ts +411 -0
- package/src/modules/tool-manuals/__tests__/tool-delete.route.test.ts +133 -0
- package/src/modules/tool-manuals/__tests__/tool-delete.service.test.ts +469 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +21 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +9 -4
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +6 -1
- package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.routes.test.ts +254 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.invalid.test.ts +577 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +6 -1
- package/src/modules/tool-manuals/__tests__/tool-manuals.pending.route.test.ts +102 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.restart.test.ts +285 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +178 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +114 -2
- package/src/modules/tool-manuals/index.ts +3 -0
- package/src/modules/tool-manuals/mcp-json-discovery.ts +13 -10
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +11 -14
- package/src/modules/tool-manuals/pending-tools.service.ts +179 -0
- package/src/modules/tool-manuals/tool-delete.service.ts +401 -0
- package/src/modules/tool-manuals/tool-manuals.contract.ts +165 -0
- package/src/modules/tool-manuals/tool-manuals.routes.ts +138 -54
- package/src/modules/tool-manuals/tool-manuals.service.ts +432 -87
- package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -6
- package/src/modules/workflow/__tests__/apply-failure.test.ts +1038 -0
- package/src/modules/workflow/__tests__/event-bus.test.ts +61 -9
- package/src/modules/workflow/__tests__/fake-file-lock-db.ts +199 -0
- package/src/modules/workflow/__tests__/file-lock.service.path-identity.test.ts +237 -0
- package/src/modules/workflow/__tests__/format-affected-owners.test.ts +18 -0
- package/src/modules/workflow/__tests__/merge-announces-tree.test.ts +220 -0
- package/src/modules/workflow/__tests__/pending-commits.worker.concurrency.test.ts +187 -0
- package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +2 -0
- package/src/modules/workflow/__tests__/sanitize-error.test.ts +10 -0
- package/src/modules/workflow/__tests__/workflow.routes.folder-proposals.test.ts +128 -0
- package/src/modules/workflow/__tests__/workflow.routes.fork-point-file.test.ts +149 -0
- package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +305 -0
- package/src/modules/workflow/__tests__/workflow.service.branch-in-use.test.ts +2 -0
- package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +2 -0
- package/src/modules/workflow/__tests__/workflow.service.deleted-branch-sweep.test.ts +2 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +294 -25
- package/src/modules/workflow/__tests__/workflow.service.folder-proposals.test.ts +564 -0
- package/src/modules/workflow/__tests__/workflow.service.platform-restore.test.ts +135 -0
- package/src/modules/workflow/__tests__/workflow.service.read-gate.test.ts +172 -0
- package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +14 -0
- package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +5 -0
- package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +281 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +158 -7
- package/src/modules/workflow/agent-tools/workflow.tools.ts +93 -64
- package/src/modules/workflow/event-bus.ts +26 -28
- package/src/modules/workflow/events.routes.ts +56 -21
- package/src/modules/workflow/file-lock.service.ts +27 -4
- package/src/modules/workflow/git/__tests__/change-request-link.test.ts +43 -0
- package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +1 -0
- package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +238 -1
- package/src/modules/workflow/git/__tests__/git.service.forkPoint.test.ts +245 -0
- package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +35 -0
- package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +149 -0
- package/src/modules/workflow/git/__tests__/git.service.revertPathsAndPush.test.ts +177 -0
- package/src/modules/workflow/git/__tests__/node-git-runner.test.ts +114 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +69 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +121 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +79 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +1 -1
- package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-update.test.ts +111 -0
- package/src/modules/workflow/git/change-request-link.ts +35 -0
- package/src/modules/workflow/git/git-version.ts +5 -2
- package/src/modules/workflow/git/git.service.ts +684 -144
- package/src/modules/workflow/git/node-git-runner.ts +315 -0
- package/src/modules/workflow/git/pull-request.service.ts +179 -27
- package/src/modules/workflow/pending-commits.service.ts +26 -7
- package/src/modules/workflow/pending-commits.worker.ts +114 -69
- package/src/modules/workflow/recovery-bot.ts +4 -1
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +180 -1
- package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
- package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +520 -0
- package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +84 -0
- package/src/modules/workflow/review-workflow/__tests__/merge-gate.test.ts +89 -77
- package/src/modules/workflow/review-workflow/approval-lock.ts +122 -0
- package/src/modules/workflow/review-workflow/review-workflow.interface.ts +59 -7
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +309 -105
- package/src/modules/workflow/sanitize-error.ts +9 -4
- package/src/modules/workflow/workflow.routes.ts +273 -29
- package/src/modules/workflow/workflow.service.ts +1087 -171
- package/src/modules/workspace/__tests__/empty-dirs.test.ts +44 -0
- package/src/modules/workspace/__tests__/file-stat-access.test.ts +301 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +792 -0
- package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +464 -0
- package/src/modules/workspace/__tests__/workspace.routes.branch-errors.test.ts +139 -0
- package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +189 -37
- package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +51 -11
- package/src/modules/workspace/__tests__/workspace.routes.download-implies-read.test.ts +161 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +109 -10
- package/src/modules/workspace/__tests__/workspace.routes.file-writes.test.ts +624 -0
- package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +409 -0
- package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +327 -0
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +25 -6
- package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +109 -0
- package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -5
- package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +218 -0
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +56 -1
- package/src/modules/workspace/__tests__/workspace.service.test.ts +811 -42
- package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +197 -0
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +128 -0
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +212 -0
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +2632 -110
- package/src/modules/workspace/__tests__/write-denial.test.ts +75 -0
- package/src/modules/workspace/empty-dirs.ts +79 -0
- package/src/modules/workspace/file-readers/README.md +32 -0
- package/src/modules/workspace/file-readers/__tests__/doc-extract.test.ts +59 -2
- package/src/modules/workspace/file-readers/__tests__/file-reader.registry.test.ts +133 -4
- package/src/modules/workspace/file-readers/__tests__/fixtures/soft-break.docx +0 -0
- package/src/modules/workspace/file-readers/__tests__/fixtures/tab.docx +0 -0
- package/src/modules/workspace/file-readers/content-mode.ts +87 -0
- package/src/modules/workspace/file-readers/doc-extract.service.ts +4 -1
- package/src/modules/workspace/file-readers/doc-extract.types.ts +8 -4
- package/src/modules/workspace/file-readers/document-reader.ts +6 -0
- package/src/modules/workspace/file-readers/extract-docx.ts +6 -0
- package/src/modules/workspace/file-readers/file-reader.registry.ts +9 -1
- package/src/modules/workspace/file-readers/file-reader.ts +27 -6
- package/src/modules/workspace/file-readers/image-reader.ts +10 -5
- package/src/modules/workspace/file-readers/ooxml-text.ts +38 -2
- package/src/modules/workspace/file-readers/text-reader.ts +61 -4
- package/src/modules/workspace/git-internals.middleware.ts +86 -0
- package/src/modules/workspace/not-found.ts +119 -0
- package/src/modules/workspace/session-ontology.gate.ts +1 -1
- package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +181 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +361 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +53 -16
- package/src/modules/workspace/startup/beside-checkout.ts +112 -0
- package/src/modules/workspace/startup/kb-git.ts +43 -36
- package/src/modules/workspace/startup/kb-startup-runner.ts +303 -63
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +1215 -73
- package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +460 -417
- package/src/modules/workspace/startup/steps/personal-spaces.step.ts +191 -0
- package/src/modules/workspace/startup/steps/plugin-display-names.step.ts +169 -0
- package/src/modules/workspace/startup/steps/plugin-layout.ts +110 -0
- package/src/modules/workspace/startup/steps/plugin-manifests.step.ts +46 -85
- package/src/modules/workspace/startup/steps/roles-yaml.step.ts +6 -12
- package/src/modules/workspace/startup/steps/seed-tree.ts +159 -78
- package/src/modules/workspace/startup/steps/template-files.step.ts +492 -141
- package/src/modules/workspace/startup/steps/template-source.ts +168 -0
- package/src/modules/workspace/workspace.routes.ts +687 -257
- package/src/modules/workspace/workspace.service.ts +991 -303
- package/src/modules/workspace/workspace.tools.ts +1787 -180
- package/src/modules/workspace/write-denial.ts +189 -0
- package/src/shared/__tests__/canonical-file-identity.test.ts +154 -0
- package/src/shared/__tests__/email-identity.test.ts +35 -0
- package/src/shared/__tests__/filename.test.ts +0 -0
- package/src/shared/__tests__/frontmatter-id.test.ts +5 -0
- package/src/shared/__tests__/git-failure.test.ts +198 -0
- package/src/shared/__tests__/git-internals.test.ts +115 -0
- package/src/shared/__tests__/http-errors.test.ts +42 -0
- package/src/shared/__tests__/logging.test.ts +185 -0
- package/src/shared/__tests__/path-containment.test.ts +60 -0
- package/src/shared/__tests__/pending-proposals.readAt.test.ts +102 -0
- package/src/shared/__tests__/printable.test.ts +64 -0
- package/src/shared/__tests__/redact-secret.test.ts +122 -0
- package/src/shared/__tests__/rename-no-replace.test.ts +466 -0
- package/src/shared/__tests__/ssrf.test.ts +20 -0
- package/src/shared/canonical-file-identity.ts +92 -0
- package/src/shared/domain-errors.ts +149 -0
- package/src/shared/email-identity.ts +38 -0
- package/src/shared/fs.contract.ts +225 -0
- package/src/shared/git-failure.ts +196 -0
- package/src/shared/git-internals.ts +124 -0
- package/src/shared/git.contract.ts +170 -0
- package/src/shared/http-errors.ts +36 -0
- package/src/shared/logger.contract.ts +141 -0
- package/src/shared/logging.ts +96 -0
- package/src/shared/path-containment.ts +34 -0
- package/src/shared/pending-proposals.ts +161 -0
- package/src/shared/printable.ts +68 -0
- package/src/shared/redact-secret.ts +118 -0
- package/src/shared/rename-no-replace.ts +386 -0
- package/src/shared/ssrf.ts +44 -16
- package/src/version.ts +10 -6
- package/src/modules/diff/diff-paths.ts +0 -9
- package/src/modules/mcp/__tests__/mcp-session-store.test.ts +0 -151
- package/src/modules/mcp/__tests__/mcp.routes.session.test.ts +0 -226
- package/src/modules/mcp/mcp-session-store.ts +0 -162
- package/src/shared/__tests__/fs-walk.test.ts +0 -63
- package/src/shared/__tests__/kb-walk.test.ts +0 -107
- package/src/shared/fs-errors.ts +0 -10
- package/src/shared/fs-walk.ts +0 -45
- package/src/shared/hash-email.ts +0 -14
- package/src/shared/kb-walk.ts +0 -95
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import nodeFs from 'node:fs/promises';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import type { Router, RequestHandler } from 'express';
|
|
4
5
|
import type { LocalFilesystem } from '@mastra/core/workspace';
|
|
@@ -20,22 +21,70 @@ import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
|
20
21
|
// Leaf-level shared primitive (same exception `workspace.service.ts` already
|
|
21
22
|
// relies on) — not a workflow service, so this stays inside the module boundary.
|
|
22
23
|
import { assertValidBranchName } from '../kb-fs/branch-name.js';
|
|
23
|
-
import { assertInsideRepo } from '../kb-fs/repo-path.js';
|
|
24
|
+
import { assertInsideRepo, assertRepoRootNameFreeArgs, normalizePathArgs } from '../kb-fs/repo-path.js';
|
|
25
|
+
import { GitGuardedFilesystem } from '../kb-fs/git-guarded-filesystem.js';
|
|
26
|
+
import { assertNoGitInternalsSegment, hasGitInternalsSegment } from '../../shared/git-internals.js';
|
|
27
|
+
import { isRolesYamlPath } from '../access-model/roles-yaml-guard.js';
|
|
24
28
|
import type { ISessionSink } from './session-sink.js';
|
|
25
|
-
import
|
|
29
|
+
import { isAbsence } from '../../shared/fs.contract.js';
|
|
30
|
+
import type { AccessDecisionSource, AccessTargetKind, IAccessControl } from '../access/access-control.interface.js';
|
|
31
|
+
import { accessRoster, resolveAccessView } from '../access/access-view.js';
|
|
32
|
+
import { accessMdPathForFolder, fileCarriesAccessRules, governingFolderOf } from '../access/access-mutation.service.js';
|
|
26
33
|
import { toKbRelative, resolveReadableMap } from '../access-model/kb-read-filter.js';
|
|
27
34
|
import type { SpillStore } from './spill-store.js';
|
|
28
35
|
import type { DocExtractService } from './file-readers/doc-extract.service.js';
|
|
29
|
-
import type
|
|
36
|
+
import { displayPath, type FileKind, type FileReaderRegistry } from './file-readers/file-reader.js';
|
|
37
|
+
import { fileTypeOf, needsContent } from './file-readers/content-mode.js';
|
|
30
38
|
import { createFileReaderRegistry } from './file-readers/file-reader.registry.js';
|
|
31
39
|
import { DocumentReader } from './file-readers/document-reader.js';
|
|
32
40
|
import { mcpImageResult } from '@bevel-software/platform-mcp-core';
|
|
41
|
+
import {
|
|
42
|
+
AGENTS_FILE,
|
|
43
|
+
LEGACY_AGENTS_FILE,
|
|
44
|
+
folderPlaceholderPath,
|
|
45
|
+
isFolderPlaceholder,
|
|
46
|
+
isPlatformFile,
|
|
47
|
+
isPlatformFolder,
|
|
48
|
+
isProtectedBranch,
|
|
49
|
+
onKbLayoutApplied,
|
|
50
|
+
platformFileCreationRefusal,
|
|
51
|
+
platformFileNames,
|
|
52
|
+
platformFileRefusal,
|
|
53
|
+
platformFolderRefusal,
|
|
54
|
+
entryExistsMessage,
|
|
55
|
+
type ExistingEntryKind,
|
|
56
|
+
} from '@bevel-software/platform-shared';
|
|
57
|
+
import { AccessDeniedError } from '../access-model/access-errors.js';
|
|
58
|
+
import { removeEmptyDirs } from './empty-dirs.js';
|
|
59
|
+
import { PROPOSAL_ROUTE_NOTE, rethrowAsWriteDenial } from './write-denial.js';
|
|
60
|
+
import type { IChangeReadGate } from '../access-model/change-gate.js';
|
|
61
|
+
import { notFound, orDeclaredNotFound, orNotFound } from './not-found.js';
|
|
62
|
+
import { logger } from '../../shared/logging.js';
|
|
63
|
+
import { printable } from '../../shared/printable.js';
|
|
64
|
+
import { DestinationTakenError, inspectDestination } from '../../shared/rename-no-replace.js';
|
|
65
|
+
|
|
66
|
+
const log = logger('workspace-tools');
|
|
67
|
+
|
|
68
|
+
/** The caller's verdict per access verb on one path. */
|
|
69
|
+
interface AccessVerbs {
|
|
70
|
+
read: boolean;
|
|
71
|
+
write: boolean;
|
|
72
|
+
download: boolean;
|
|
73
|
+
owner: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** How many files `file_stat` counts under a folder before it stops and says so. */
|
|
77
|
+
const DESCENDANTS_CAP = 10_000;
|
|
78
|
+
|
|
79
|
+
/** How many of a folder's files a `delete_folder` answer names. */
|
|
80
|
+
const LISTED_FILES_CAP = 100;
|
|
33
81
|
|
|
34
82
|
/** A directory entry as returned by `LocalFilesystem.readdir`. */
|
|
35
83
|
interface DirEntry {
|
|
36
84
|
name: string;
|
|
37
85
|
type: 'file' | 'directory';
|
|
38
86
|
size?: number;
|
|
87
|
+
isSymlink?: boolean;
|
|
39
88
|
}
|
|
40
89
|
|
|
41
90
|
/**
|
|
@@ -94,40 +143,151 @@ async function filterReadableEntries(
|
|
|
94
143
|
}
|
|
95
144
|
|
|
96
145
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
146
|
+
* Drop the empty-folder placeholder from a directory listing: it keeps a
|
|
147
|
+
* folder alive in git and is never content (see `placeholder.ts`).
|
|
148
|
+
*/
|
|
149
|
+
function withoutPlaceholder(entries: DirEntry[]): DirEntry[] {
|
|
150
|
+
return entries.filter((e) => e.type === 'directory' || !isFolderPlaceholder(e.name));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Keep the folder a removal just emptied. A folder exists until it is deleted
|
|
155
|
+
* explicitly, so when deleting or moving out its last entry leaves it empty it
|
|
156
|
+
* gets the placeholder, written through the same filesystem (the agent's
|
|
157
|
+
* lock-aware one commits it) within the same tool call. Only folders inside
|
|
158
|
+
* the repository qualify, never the clone folder itself.
|
|
159
|
+
*
|
|
160
|
+
* It runs in the folder's turn, which the explicit folder delete also takes,
|
|
161
|
+
* and looks inside it: a folder that is gone by then was deleted explicitly
|
|
162
|
+
* and stays gone. A failure fails the call — the removal landed, but the
|
|
163
|
+
* folder would vanish on the next clone, and the agent must hear that.
|
|
164
|
+
*/
|
|
165
|
+
async function keepFolderOf(
|
|
166
|
+
fs: LocalFilesystem,
|
|
167
|
+
ctx: ToolContext,
|
|
168
|
+
branch: string,
|
|
169
|
+
removedPath: string,
|
|
170
|
+
kbDirName: string,
|
|
171
|
+
): Promise<void> {
|
|
172
|
+
const trimmed = removedPath.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
173
|
+
const dir = trimmed.includes('/') ? trimmed.slice(0, trimmed.lastIndexOf('/')) : '';
|
|
174
|
+
if (!dir.startsWith(`${kbDirName}/`)) return;
|
|
175
|
+
try {
|
|
176
|
+
await ctx.workspaceService.withFolderTurn(workspaceIdForBranch(branch), dir, async () => {
|
|
177
|
+
let entries: unknown[];
|
|
178
|
+
try {
|
|
179
|
+
entries = await fs.readdir(dir);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (isAbsence(err)) return;
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
if (entries.length > 0) return;
|
|
185
|
+
await fs.writeFile(folderPlaceholderPath(dir), '');
|
|
186
|
+
});
|
|
187
|
+
} catch (err) {
|
|
188
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
189
|
+
log.error(`could not keep the folder ${printable(dir)} after removing ${printable(removedPath)}: ${printable(reason)}`);
|
|
190
|
+
const message = `"${removedPath}" was removed, but its folder "${dir}" could not be kept: ${reason}`;
|
|
191
|
+
// The original error keeps its status (a lock held elsewhere stays a 409).
|
|
192
|
+
if (!(err instanceof Error)) throw new ToolError(message, 500);
|
|
193
|
+
err.message = message;
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Appended (centrally, in `mount`) to EVERY workspace tool description. The
|
|
200
|
+
* platform's managed agent guide sits at the workspace root and documents the
|
|
201
|
+
* conventions of that knowledge base; agents (ours and external) should consult
|
|
202
|
+
* it before touching files. It rides on every entrypoint — reads (grep/
|
|
203
|
+
* list_files/file_stat) included — because any of them can be a session's first
|
|
204
|
+
* touch.
|
|
102
205
|
*
|
|
103
206
|
* `CLAUDE.md` is named as a fallback because knowledge bases seeded before the
|
|
104
207
|
* rename still carry one, and the seeder never deletes a file it did not
|
|
105
208
|
* expect. Naming both means an agent finds the conventions either way, instead
|
|
106
209
|
* of reading none because it looked for the newer name and stopped.
|
|
210
|
+
*
|
|
211
|
+
* WHEN THE GUIDE HAS BEEN RENAMED the sentence names two files, ours first. The
|
|
212
|
+
* second is the organisation's OWN `AGENTS.md`, which on such a deployment is
|
|
213
|
+
* ordinary content the platform never touches — and which no harness reads for a
|
|
214
|
+
* remote agent, because a remote agent has no checkout. Telling it to read both
|
|
215
|
+
* is the only way the conventions the customer actually wrote reach the agent
|
|
216
|
+
* working in their knowledge base. Under the default name the wording collapses
|
|
217
|
+
* to the one file it has always named.
|
|
218
|
+
*
|
|
219
|
+
* A FUNCTION, called at mount time: the name is a deployment setting applied at
|
|
220
|
+
* boot, and a module-scope string would snapshot the default.
|
|
107
221
|
*/
|
|
108
|
-
|
|
109
|
-
|
|
222
|
+
function kbConventionsNote(): string {
|
|
223
|
+
if (AGENTS_FILE === LEGACY_AGENTS_FILE) {
|
|
224
|
+
return ' Before your first read or change in a workspace, read `AGENTS.md` at the KB root — or `CLAUDE.md` on a knowledge base seeded before it was renamed — if either exists: it holds the author\'s conventions for this knowledge base, and you should follow them.';
|
|
225
|
+
}
|
|
226
|
+
return (
|
|
227
|
+
` Before your first read or change in a workspace, read \`${AGENTS_FILE}\` at the KB root, then ` +
|
|
228
|
+
'`AGENTS.md` if it also exists (the organisation\'s own conventions) — or `CLAUDE.md` on a knowledge base seeded before it was renamed: together they hold the conventions for this knowledge base, and you should follow them.'
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The platform files as a tool description lists them — the guide under its own name. */
|
|
233
|
+
function platformFileList(): string {
|
|
234
|
+
return platformFileNames()
|
|
235
|
+
.map((name) => `\`${name}\``)
|
|
236
|
+
.join(', ');
|
|
237
|
+
}
|
|
110
238
|
|
|
111
239
|
const int = (description: string): JsonSchema => ({ type: 'integer', description });
|
|
112
240
|
|
|
113
241
|
const str = (description: string): JsonSchema => ({ type: 'string', description });
|
|
114
242
|
|
|
115
243
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* repository. Saying so in the input itself, not only in prose, is what the
|
|
121
|
-
* agent actually sees when it fills the argument. `refused` is false for the
|
|
122
|
-
* inputs that may legitimately name a stray (a source to rescue, a file to
|
|
123
|
-
* remove).
|
|
244
|
+
* Where pictures go, on the two tools that write pages. An agent in core cannot
|
|
245
|
+
* upload bytes yet (TODOS.md), but it can write the page with the link a person
|
|
246
|
+
* will satisfy, and this sentence is what keeps every page it writes on the
|
|
247
|
+
* README's convention: images beside the page, linked relatively.
|
|
124
248
|
*/
|
|
125
|
-
const
|
|
249
|
+
const IMAGE_CONVENTION_NOTE =
|
|
250
|
+
' Images: keep them in an `assets/` folder next to the page that uses them and link them with a relative path, e.g. ``; the page renders them inline.';
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* A path input that names the clone folder, and says what happens when it does
|
|
254
|
+
* not. The tools are rooted at the WORKSPACE dir, one level above the git clone,
|
|
255
|
+
* so a path reaches git only when it starts with that folder — and a path that
|
|
256
|
+
* does not name it is PLACED under it now rather than refused, by the one
|
|
257
|
+
* normaliser every route and tool goes through. An agent that reads
|
|
258
|
+
* `KnowledgeBase/Foo.md` in a URL or a doc and passes it verbatim gets the page
|
|
259
|
+
* of that name in the repository, which is what it meant; it no longer gets a
|
|
260
|
+
* refusal, and it never again gets a file written beside the repository where
|
|
261
|
+
* nothing commits it. Saying so in the input itself, not only in prose, is what
|
|
262
|
+
* the agent actually sees when it fills the argument.
|
|
263
|
+
*
|
|
264
|
+
* ONE sentence for every input, where there used to be a shorter second form for
|
|
265
|
+
* the ones that could legitimately name a stray (a source to rescue, a file to
|
|
266
|
+
* remove). Nothing can name a stray any more. Traversal (`.`/`..`), backslashes
|
|
267
|
+
* and absolute paths are still refused, everywhere.
|
|
268
|
+
*/
|
|
269
|
+
const wsPath = (kbDirName: string, what: string): JsonSchema =>
|
|
126
270
|
str(
|
|
127
|
-
`${what}:
|
|
128
|
-
|
|
271
|
+
`${what}: under \`${kbDirName}/\` (e.g. \`${kbDirName}/KnowledgeBase/Foo.md\`), with or without a leading slash (\`/${kbDirName}/…\` is the same path). ` +
|
|
272
|
+
`A path without that prefix is placed under \`${kbDirName}/\`, so \`KnowledgeBase/Foo.md\` means \`${kbDirName}/KnowledgeBase/Foo.md\`; \`.\` or \`..\` segments, backslashes and absolute paths are refused.`,
|
|
129
273
|
);
|
|
130
274
|
|
|
275
|
+
/**
|
|
276
|
+
* The inputs each tool CREATES at, for the reserved-root-name rule (see
|
|
277
|
+
* `assertRepoRootNameFreeArgs`). Destinations only: `src` of a copy or a move,
|
|
278
|
+
* and the path of a delete, are left out on purpose, so an existing reserved
|
|
279
|
+
* folder can be moved out of or removed. A tool absent here creates nothing.
|
|
280
|
+
*/
|
|
281
|
+
const RESERVED_ROOT_NAME_TARGETS: Readonly<Record<string, readonly string[]>> = {
|
|
282
|
+
write_file: ['path'],
|
|
283
|
+
write_files: ['files'],
|
|
284
|
+
edit_file: ['path'],
|
|
285
|
+
mkdir: ['path'],
|
|
286
|
+
copy_file: ['dest'],
|
|
287
|
+
move_file: ['dest'],
|
|
288
|
+
unzip: ['destination'],
|
|
289
|
+
};
|
|
290
|
+
|
|
131
291
|
function asText(content: string | Buffer): string {
|
|
132
292
|
return typeof content === 'string' ? content : content.toString('utf8');
|
|
133
293
|
}
|
|
@@ -155,24 +315,60 @@ interface DocGrepState {
|
|
|
155
315
|
}
|
|
156
316
|
|
|
157
317
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
|
|
164
|
-
|
|
318
|
+
* THE binary capability contract, stated once and appended (in `mount`) to
|
|
319
|
+
* every file tool's description — which is also what `tools_info` returns.
|
|
320
|
+
* The split it states is enforced by the reader registry: the text tools
|
|
321
|
+
* refuse what their reader marks not `textEditable` (and binary content under
|
|
322
|
+
* any name) with a `binary_not_writable` refusal; the byte tools never look.
|
|
323
|
+
*/
|
|
324
|
+
export const CONTENT_RULE =
|
|
325
|
+
' Content rule (the same on every file tool): read_file returns text for text files and extracted text for documents (.docx/.pptx/.xlsx/.odt/.odp/.ods/.pdf, .eml/.msg); write_file, write_files and edit_file accept TEXT only — they refuse documents, images, archives and other binary files (legacy .doc/.ppt/.xls included) with kind `binary_not_writable`, naming the file\'s kind and the tool to use instead; copy_file, move_file, delete_file and unzip act on bytes of any kind; new binary content arrives through upload (`request_upload_token` + `apply_upload` where offered, otherwise Upload in the app). file_stat reports `contentMode` (`text` | `document` | `binary`) so you can decide before acting.';
|
|
326
|
+
|
|
327
|
+
/** What a `binary_not_writable` refusal points to, in the order to try them. */
|
|
328
|
+
const BINARY_USE_INSTEAD = ['upload', 'copy_file', 'move_file'] as const;
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The ONE refusal the text tools give for content they must not write: status
|
|
332
|
+
* 415, kind `binary_not_writable`, the file's kind, and the tools to use
|
|
333
|
+
* instead — upload for new bytes, copy_file/move_file for bytes already in
|
|
334
|
+
* the workspace. `explanation` is the format-specific why.
|
|
335
|
+
*/
|
|
336
|
+
function binaryNotWritable(fileKind: FileKind, explanation: string): ToolError {
|
|
337
|
+
return new ToolError(
|
|
338
|
+
`${explanation} [binary_not_writable: this file's kind is ${fileKind}; write_file, write_files and edit_file accept text only. ` +
|
|
339
|
+
'Use upload for new bytes (`request_upload_token` + `apply_upload` where offered, otherwise Upload in the app), ' +
|
|
340
|
+
'or copy_file / move_file to place bytes that are already in the workspace.]',
|
|
341
|
+
415,
|
|
342
|
+
{ kind: 'binary_not_writable', fileKind, useInstead: [...BINARY_USE_INSTEAD] },
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** The generic why, for a reader without format-specific refusal copy. */
|
|
347
|
+
const KIND_EXPLANATION: Record<FileKind, (path: string) => string> = {
|
|
348
|
+
text: (p) => `"${p}" cannot be written as text.`,
|
|
349
|
+
document: (p) =>
|
|
350
|
+
`"${p}" is an office document/PDF. read_file returns EXTRACTED text for it — not the file's real ` +
|
|
351
|
+
'content — so text written back cannot round-trip and would corrupt the document. To change it, ' +
|
|
352
|
+
'replace the document by uploading a new version.',
|
|
353
|
+
image: (p) => `"${p}" is an image. Its content is bytes, and text written to it could only produce a broken picture.`,
|
|
354
|
+
archive: (p) => `"${p}" is an archive. Its content is bytes, and text written to it could only produce a broken archive.`,
|
|
355
|
+
binary: (p) => `"${p}" is a binary file. Its content is bytes, and text written to it could only produce a broken file.`,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* The write-refusal by FORMAT on the agent TEXT-editing tools (write_file /
|
|
360
|
+
* write_files / edit_file): documents (read_file returns an EXTRACTION, so
|
|
361
|
+
* text written back would silently destroy the real document), images,
|
|
362
|
+
* archives and other binary formats — every reader that declares
|
|
363
|
+
* `textEditable: false`. Uploads and the plain HTTP write routes are
|
|
364
|
+
* untouched — humans replacing a file is exactly the right move — and the
|
|
365
|
+
* byte tools (copy/move/delete/unzip) never ask.
|
|
165
366
|
*/
|
|
166
367
|
function assertNotDocumentEdit(readers: FileReaderRegistry, path: string): void {
|
|
167
368
|
const reader = readers.readerFor(path);
|
|
168
369
|
if (reader.textEditable) return;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
`"${path}" is an office document/PDF. read_file returns EXTRACTED text for it — not the file's real ` +
|
|
172
|
-
'content — so text written back cannot round-trip and would corrupt the document. To change it, ' +
|
|
173
|
-
'replace the document by uploading a new version.',
|
|
174
|
-
400,
|
|
175
|
-
);
|
|
370
|
+
const shown = displayPath(path);
|
|
371
|
+
throw binaryNotWritable(reader.fileKind, reader.editRefusal?.(shown) ?? KIND_EXPLANATION[reader.fileKind](shown));
|
|
176
372
|
}
|
|
177
373
|
|
|
178
374
|
/**
|
|
@@ -200,17 +396,191 @@ async function assertNotBinaryOverwrite(
|
|
|
200
396
|
existing = asBytes(await fs.readFile(path));
|
|
201
397
|
} catch (err) {
|
|
202
398
|
// Only a MISSING file is a create (both raw Node errors and Mastra's
|
|
203
|
-
// FileNotFoundError carry
|
|
204
|
-
// I/O — means the existing content could not be inspected:
|
|
205
|
-
// rather than let the write destroy bytes the gate never saw.
|
|
206
|
-
if ((err
|
|
399
|
+
// FileNotFoundError carry the disk's absence codes). Any other failure —
|
|
400
|
+
// permissions, I/O — means the existing content could not be inspected:
|
|
401
|
+
// propagate it rather than let the write destroy bytes the gate never saw.
|
|
402
|
+
if (isAbsence(err)) return undefined; // nothing there yet
|
|
207
403
|
throw err;
|
|
208
404
|
}
|
|
209
405
|
const refusal = reader.editRefusalForExisting(existing, path);
|
|
210
|
-
if (refusal !== null) throw
|
|
406
|
+
if (refusal !== null) throw binaryNotWritable('binary', refusal);
|
|
211
407
|
return existing;
|
|
212
408
|
}
|
|
213
409
|
|
|
410
|
+
/** What a write is ALLOWED to do at a path. `create` is the default everywhere. */
|
|
411
|
+
export type WriteMode = 'create' | 'overwrite' | 'update';
|
|
412
|
+
|
|
413
|
+
/** What a write actually DID at a path, in the answer the caller reads. */
|
|
414
|
+
export type WriteOutcome = 'created' | 'replaced' | 'updated';
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The `mode` input, on both write tools. Stated on the input itself and not
|
|
418
|
+
* only in the description, because the argument is where an agent decides:
|
|
419
|
+
* the default refuses to replace anything, so "write this here" can no longer
|
|
420
|
+
* destroy a page the agent never read.
|
|
421
|
+
*/
|
|
422
|
+
const WRITE_MODE_INPUT: JsonSchema = {
|
|
423
|
+
type: 'string',
|
|
424
|
+
enum: ['create', 'overwrite', 'update'],
|
|
425
|
+
description:
|
|
426
|
+
'What the write may do at the path, default `create`: `create` writes a NEW file and refuses (`exists`) a path that already ' +
|
|
427
|
+
'holds something; `overwrite` replaces what is there, and creates the file when there is nothing; `update` replaces an ' +
|
|
428
|
+
'EXISTING file and refuses (`missing`) a path that holds nothing.',
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
/** The same three modes, said once, for both tool descriptions. */
|
|
432
|
+
const WRITE_MODE_NOTE =
|
|
433
|
+
' `mode` decides what may happen at a path and DEFAULTS TO `create`: `create` writes a new file and refuses a path that ' +
|
|
434
|
+
'already exists (`exists`, with the path — pass `mode: overwrite` to replace it), `overwrite` replaces what is there ' +
|
|
435
|
+
'(creating it if there is nothing), `update` replaces an existing file and refuses a path that does not exist (`missing`). ' +
|
|
436
|
+
'A refused path is left exactly as it was.';
|
|
437
|
+
|
|
438
|
+
/** The refusal `create` gives on a path that already holds something. */
|
|
439
|
+
function pathExists(path: string): ToolError {
|
|
440
|
+
return new ToolError(
|
|
441
|
+
`"${displayPath(path)}" already exists — pass mode: overwrite to replace it, or write to a different path.`,
|
|
442
|
+
409,
|
|
443
|
+
{ code: 'exists', path },
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** The refusal `update` gives on a path that holds nothing. */
|
|
448
|
+
function pathMissing(path: string): ToolError {
|
|
449
|
+
return new ToolError(
|
|
450
|
+
`"${displayPath(path)}" does not exist — pass mode: create to create it.`,
|
|
451
|
+
404,
|
|
452
|
+
{ code: 'missing', path },
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* The mode gate for ONE path: refuses the write the mode does not allow, or
|
|
458
|
+
* names what it is about to do. `create` on something that exists and `update`
|
|
459
|
+
* on something that does not are the two refusals; everything else writes, and
|
|
460
|
+
* the outcome distinguishes a file that was there from one that was not.
|
|
461
|
+
*/
|
|
462
|
+
function decideWrite(mode: WriteMode, path: string, exists: boolean): WriteOutcome {
|
|
463
|
+
if (mode === 'create' && exists) throw pathExists(path);
|
|
464
|
+
if (mode === 'update' && !exists) throw pathMissing(path);
|
|
465
|
+
if (mode === 'update') return 'updated';
|
|
466
|
+
return exists ? 'replaced' : 'created';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** The three modes, as a set the handler can check a raw argument against. */
|
|
470
|
+
const WRITE_MODES: readonly WriteMode[] = ['create', 'overwrite', 'update'];
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* The call's mode — `create` when it says nothing, which is the whole point of
|
|
474
|
+
* the default. A mode that is not one of the three is REFUSED rather than
|
|
475
|
+
* treated as the nearest thing: a tool that quietly read `replace` as
|
|
476
|
+
* "overwrite" would put the silent overwrite back, by a different door.
|
|
477
|
+
*/
|
|
478
|
+
function modeOf(a: Record<string, unknown>): WriteMode {
|
|
479
|
+
if (a.mode === undefined || a.mode === null) return 'create';
|
|
480
|
+
if (typeof a.mode === 'string' && (WRITE_MODES as readonly string[]).includes(a.mode)) return a.mode as WriteMode;
|
|
481
|
+
throw new ToolError(
|
|
482
|
+
`"${String(a.mode)}" is not a write mode: use ${WRITE_MODES.map((m) => `\`${m}\``).join(', ')} (default \`create\`).`,
|
|
483
|
+
400,
|
|
484
|
+
{ code: 'bad_mode' },
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* What searching ONE file amounted to. The walk ignores this (a file with
|
|
490
|
+
* nothing searchable is just a file with no matches), but a grep whose path
|
|
491
|
+
* NAMES that one file has nothing else to report: without this it could only
|
|
492
|
+
* answer an empty match list, which reads as "your pattern is not in there"
|
|
493
|
+
* when the truth is "there was never any text to look at".
|
|
494
|
+
*/
|
|
495
|
+
type FileGrepOutcome =
|
|
496
|
+
/** Its text was searched; any matches are in `out`. */
|
|
497
|
+
| 'searched'
|
|
498
|
+
/** No searchable text at all: an image, binary content, a corrupt document. */
|
|
499
|
+
| 'no-text'
|
|
500
|
+
/** A cold document the per-call extraction budget could not afford (counted in `docs`). */
|
|
501
|
+
| 'budget-skipped';
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Search ONE file and append its matches to `out`. Shared by the directory
|
|
505
|
+
* walk and by a grep whose `path` names a file, so both produce the same match
|
|
506
|
+
* shape (that path, 1-based line numbers, 300-char text) under the same cap.
|
|
507
|
+
*
|
|
508
|
+
* What is searched is the file's reader's business: text content for the text
|
|
509
|
+
* reader (null on NUL bytes / invalid UTF-8 — binary is not searchable), the
|
|
510
|
+
* EXTRACTION for a document reader (marker line included, so the
|
|
511
|
+
* `[slide N]`/`[sheet: …]`/`[page N]` lines are themselves searchable and line
|
|
512
|
+
* numbers match what read_file returns), nothing for images.
|
|
513
|
+
*
|
|
514
|
+
* Throws whatever reading the file throws — the caller decides whether that is
|
|
515
|
+
* a file to skip (the walk) or an error to surface (a single-file grep).
|
|
516
|
+
*/
|
|
517
|
+
async function grepOneFile(
|
|
518
|
+
fs: LocalFilesystem,
|
|
519
|
+
path: string,
|
|
520
|
+
re: RegExp,
|
|
521
|
+
out: { path: string; line: number; text: string }[],
|
|
522
|
+
max: number,
|
|
523
|
+
docs: DocGrepState,
|
|
524
|
+
): Promise<FileGrepOutcome> {
|
|
525
|
+
const reader = docs.readers.readerFor(path);
|
|
526
|
+
const bytes = asBytes(await fs.readFile(path));
|
|
527
|
+
let content = reader.greppableText ? await reader.greppableText(bytes, path) : null;
|
|
528
|
+
if (content === null && reader instanceof DocumentReader) {
|
|
529
|
+
// Cold document (extraction not yet cached): cached ones above are free,
|
|
530
|
+
// extracting draws on the per-walk budget (see UNCACHED_DOCS_PER_GREP) —
|
|
531
|
+
// beyond it the search skips the document and counts it.
|
|
532
|
+
if (docs.uncachedBudget <= 0) {
|
|
533
|
+
docs.skippedUncached++;
|
|
534
|
+
return 'budget-skipped';
|
|
535
|
+
}
|
|
536
|
+
docs.uncachedBudget--;
|
|
537
|
+
const res = await reader.read(bytes, path);
|
|
538
|
+
// A non-text outcome is a corrupt document — nothing searchable.
|
|
539
|
+
content = res.kind === 'text' ? res.text : null;
|
|
540
|
+
}
|
|
541
|
+
if (content === null) return 'no-text';
|
|
542
|
+
const lines = content.split('\n');
|
|
543
|
+
for (let i = 0; i < lines.length && out.length < max; i++) {
|
|
544
|
+
re.lastIndex = 0;
|
|
545
|
+
if (re.test(lines[i])) out.push({ path, line: i + 1, text: lines[i].slice(0, 300) });
|
|
546
|
+
}
|
|
547
|
+
return 'searched';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* What a grep's `path` actually names. The walk starts with `readdir`, which
|
|
552
|
+
* fails on a file and on a path with nothing at it alike — both would end as a
|
|
553
|
+
* silent empty result — so the search root is resolved FIRST and each case
|
|
554
|
+
* gets its own honest answer.
|
|
555
|
+
*
|
|
556
|
+
* This helper never raises an error of its own. Only a DIRECTORY answer earns
|
|
557
|
+
* the walk; everything else takes the single-file route, where the read gate
|
|
558
|
+
* speaks first and the answer is then produced by the very `fs.readFile` that
|
|
559
|
+
* `read_file` calls. That is what makes grep tell read_file's story for an odd
|
|
560
|
+
* path by CONSTRUCTION rather than by coincidence.
|
|
561
|
+
*/
|
|
562
|
+
async function searchRootKind(
|
|
563
|
+
fs: LocalFilesystem,
|
|
564
|
+
path: string,
|
|
565
|
+
): Promise<'directory' | 'file' | 'missing'> {
|
|
566
|
+
try {
|
|
567
|
+
return (await fs.stat(path)).type === 'directory' ? 'directory' : 'file';
|
|
568
|
+
} catch (err) {
|
|
569
|
+
// "Nothing there" is the disk's own definition of absence: plain ENOENT,
|
|
570
|
+
// and ENOTDIR for a path whose parent is an existing FILE
|
|
571
|
+
// (`notes.md/deeper`) — nothing can live there either, so it earns the
|
|
572
|
+
// same honest 404 rather than a raw failure. (Mastra's
|
|
573
|
+
// FileNotFoundError carries these codes, as do raw Node errors.)
|
|
574
|
+
if (isAbsence(err)) return 'missing';
|
|
575
|
+
// Any OTHER stat failure — permissions, I/O, a symlink loop — is not
|
|
576
|
+
// absence and is not this helper's to report. Calling it a file sends the
|
|
577
|
+
// path down the ordinary single-file route: the gate answers 403 if the
|
|
578
|
+
// caller may not read it, and otherwise the read itself fails exactly as
|
|
579
|
+
// `read_file`'s does. Nothing is invented, and nothing extra is disclosed.
|
|
580
|
+
return 'file';
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
214
584
|
/** JS grep over the workspace tree (read methods only) — bounded by match + depth caps. */
|
|
215
585
|
async function grepWalk(
|
|
216
586
|
fs: LocalFilesystem,
|
|
@@ -235,7 +605,8 @@ async function grepWalk(
|
|
|
235
605
|
entries = await filterReadableEntries(gate, dir, entries);
|
|
236
606
|
for (const e of entries) {
|
|
237
607
|
if (out.length >= max) return;
|
|
238
|
-
if (e.name
|
|
608
|
+
if (hasGitInternalsSegment(e.name) || e.name === 'node_modules') continue;
|
|
609
|
+
if (e.type !== 'directory' && isFolderPlaceholder(e.name)) continue;
|
|
239
610
|
const p = dir ? `${dir}/${e.name}` : e.name;
|
|
240
611
|
if (e.type === 'directory') {
|
|
241
612
|
await grepWalk(fs, p, re, out, max, depth + 1, gate, recordOntologyRead, docs);
|
|
@@ -244,42 +615,53 @@ async function grepWalk(
|
|
|
244
615
|
// for a root-level grep that resolves to a neutral root. Record it so a
|
|
245
616
|
// cross-ontology grep poisons later writes (closes the read-leak).
|
|
246
617
|
await recordOntologyRead(p);
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// EXTRACTION for a document reader (marker line included, so the
|
|
250
|
-
// [slide N]/[sheet: …]/[page N] lines are themselves searchable and
|
|
251
|
-
// line numbers match what read_file returns), nothing for images.
|
|
252
|
-
const reader = docs.readers.readerFor(p);
|
|
253
|
-
let content: string | null;
|
|
618
|
+
// A file the walk cannot read is silently skipped: one unreadable entry
|
|
619
|
+
// must not fail a search over the whole tree.
|
|
254
620
|
try {
|
|
255
|
-
|
|
256
|
-
content = reader.greppableText ? await reader.greppableText(bytes, p) : null;
|
|
257
|
-
if (content === null && reader instanceof DocumentReader) {
|
|
258
|
-
// Cold document (extraction not yet cached): cached ones above are
|
|
259
|
-
// free, extracting draws on the per-walk budget (see
|
|
260
|
-
// UNCACHED_DOCS_PER_GREP) — beyond it the walk skips and counts.
|
|
261
|
-
if (docs.uncachedBudget <= 0) {
|
|
262
|
-
docs.skippedUncached++;
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
docs.uncachedBudget--;
|
|
266
|
-
const res = await reader.read(bytes, p);
|
|
267
|
-
// A non-text outcome is a corrupt document — nothing searchable.
|
|
268
|
-
content = res.kind === 'text' ? res.text : null;
|
|
269
|
-
}
|
|
621
|
+
await grepOneFile(fs, p, re, out, max, docs);
|
|
270
622
|
} catch {
|
|
271
623
|
continue;
|
|
272
624
|
}
|
|
273
|
-
if (content === null) continue;
|
|
274
|
-
const lines = content.split('\n');
|
|
275
|
-
for (let i = 0; i < lines.length && out.length < max; i++) {
|
|
276
|
-
re.lastIndex = 0;
|
|
277
|
-
if (re.test(lines[i])) out.push({ path: p, line: i + 1, text: lines[i].slice(0, 300) });
|
|
278
|
-
}
|
|
279
625
|
}
|
|
280
626
|
}
|
|
281
627
|
}
|
|
282
628
|
|
|
629
|
+
/** One `allowed-tools` entry a saved SKILL.md names that no visible tool matches. */
|
|
630
|
+
export interface SkillSaveWarning {
|
|
631
|
+
entry: string;
|
|
632
|
+
message: string;
|
|
633
|
+
suggestion?: string;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** The save-time skill check the write tools consult — satisfied by the skills module's `AllowedToolsChecker`. */
|
|
637
|
+
export interface SkillSaveCheck {
|
|
638
|
+
checkSave(userEmail: string, path: string, content: string): Promise<SkillSaveWarning[]>;
|
|
639
|
+
/** The batch form: `result[i]` is for `files[i]`, and the catalog is read once. */
|
|
640
|
+
checkSaves(userEmail: string, files: readonly { path: string; content: string }[]): Promise<SkillSaveWarning[][]>;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const SAVE_WARNINGS_OUTPUT: JsonSchema = {
|
|
644
|
+
type: 'array',
|
|
645
|
+
description:
|
|
646
|
+
'Present only when the file is a SKILL.md whose `allowed-tools` names platform tools you cannot use: ' +
|
|
647
|
+
'each `{ entry, message, suggestion? }`. The write still happened.',
|
|
648
|
+
items: { type: 'object' },
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* The batch form of {@link SAVE_WARNINGS_OUTPUT}. A batch may save several
|
|
653
|
+
* skills at once, so each warning also carries the `path` it is about —
|
|
654
|
+
* without it the caller cannot tell which SKILL.md a warning names.
|
|
655
|
+
*/
|
|
656
|
+
const BATCH_SAVE_WARNINGS_OUTPUT: JsonSchema = {
|
|
657
|
+
type: 'array',
|
|
658
|
+
description:
|
|
659
|
+
'Present only when the batch wrote a SKILL.md whose `allowed-tools` names platform tools you cannot use: ' +
|
|
660
|
+
'each `{ path, entry, message, suggestion? }`, where `path` is the written file the warning is about. ' +
|
|
661
|
+
'The writes still happened.',
|
|
662
|
+
items: { type: 'object' },
|
|
663
|
+
};
|
|
664
|
+
|
|
283
665
|
/**
|
|
284
666
|
* Workspace domain tools: the file primitives (replacing Mastra's auto-injected
|
|
285
667
|
* Workspace tools) + unzip. Most just re-expose the SAME `LocalFilesystem`
|
|
@@ -301,6 +683,19 @@ export function registerWorkspaceTools(
|
|
|
301
683
|
sessionOntologyGate: SessionOntologyGate,
|
|
302
684
|
writePolicy: IRoutineWritePolicy,
|
|
303
685
|
sessionSink: ISessionSink,
|
|
686
|
+
/**
|
|
687
|
+
* Save-time skill check (see `AllowedToolsChecker`): a write to a SKILL.md
|
|
688
|
+
* returns `warnings` for `allowed-tools` entries naming no visible tool.
|
|
689
|
+
* Advisory only — it never refuses the write.
|
|
690
|
+
*/
|
|
691
|
+
skillSaveCheck?: SkillSaveCheck,
|
|
692
|
+
/**
|
|
693
|
+
* Read-before-write, for the `write-denied` answer's "may you propose this
|
|
694
|
+
* instead?" — the same verdict the lock applies on the draft the proposal
|
|
695
|
+
* would be made on. Optional so tool harnesses need not wire it; the read
|
|
696
|
+
* verdict alone then decides, which differs only at a root.
|
|
697
|
+
*/
|
|
698
|
+
changeGate?: IChangeReadGate,
|
|
304
699
|
): void {
|
|
305
700
|
/**
|
|
306
701
|
* The one extension→reader registry every read-shaped decision routes
|
|
@@ -310,6 +705,17 @@ export function registerWorkspaceTools(
|
|
|
310
705
|
*/
|
|
311
706
|
const readers = createFileReaderRegistry(docExtract);
|
|
312
707
|
|
|
708
|
+
/** `{ warnings }` when a saved skill names tools nobody can resolve, else `{}` — spread into a write's result. */
|
|
709
|
+
const saveWarnings = async (
|
|
710
|
+
ctx: ToolContext,
|
|
711
|
+
path: string,
|
|
712
|
+
content: string,
|
|
713
|
+
): Promise<{ warnings?: SkillSaveWarning[] }> => {
|
|
714
|
+
if (!skillSaveCheck) return {};
|
|
715
|
+
const warnings = await skillSaveCheck.checkSave(ctx.user.email, path, content);
|
|
716
|
+
return warnings.length > 0 ? { warnings } : {};
|
|
717
|
+
};
|
|
718
|
+
|
|
313
719
|
/** Build the per-call read gate from the tool's branch input + caller identity. */
|
|
314
720
|
const readGateFor = (branch: string, ctx: ToolContext): ReadGate => ({
|
|
315
721
|
accessControl,
|
|
@@ -318,22 +724,433 @@ export function registerWorkspaceTools(
|
|
|
318
724
|
userEmail: ctx.user.email,
|
|
319
725
|
});
|
|
320
726
|
|
|
727
|
+
/** A repo-relative path in the workspace-relative form every tool speaks. */
|
|
728
|
+
const toWs = (rel: string): string => (rel ? `${kbDirName}/${rel}` : kbDirName);
|
|
729
|
+
const sourceToWs = (s: AccessDecisionSource | null) => (s ? { ...s, path: toWs(s.path) } : null);
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* What `file_stat` adds to its `access` verdicts when asked to explain them:
|
|
733
|
+
* `why` each verdict holds, from the resolver the gates use, and — only for
|
|
734
|
+
* someone who may write the path's access rules, the same gate the Manage
|
|
735
|
+
* access dialog's grant route applies — the roster that dialog lists. A file
|
|
736
|
+
* that cannot carry rules of its own is governed by its folder's
|
|
737
|
+
* `access.md`, so that is the file the gate asks about.
|
|
738
|
+
*/
|
|
739
|
+
const explainAccessAt = async (branch: string, ctx: ToolContext, p: string, kind: AccessTargetKind) => {
|
|
740
|
+
const norm = p.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
741
|
+
const rel = norm === kbDirName ? '' : toKbRelative(norm, kbDirName);
|
|
742
|
+
if (rel === null) {
|
|
743
|
+
return { why: null, roster: null, rosterReason: `"${p}" is outside the \`${kbDirName}/\` repository, so no access rules apply to it.` };
|
|
744
|
+
}
|
|
745
|
+
if (!accessControl.explainAccess) throw new ToolError('Access explanation is not available on this server.', 501);
|
|
746
|
+
const workspaceId = workspaceIdForBranch(branch);
|
|
747
|
+
const email = ctx.user.email;
|
|
748
|
+
const explained = await accessControl.explainAccess(workspaceId, email, kind, rel);
|
|
749
|
+
const why = Object.fromEntries(
|
|
750
|
+
Object.entries(explained).map(([verb, { source, via, principal }]) => [verb, { source: sourceToWs(source), via, principal }]),
|
|
751
|
+
);
|
|
752
|
+
const rulesPath =
|
|
753
|
+
kind === 'folder'
|
|
754
|
+
? accessMdPathForFolder(rel)
|
|
755
|
+
: fileCarriesAccessRules(rel)
|
|
756
|
+
? rel
|
|
757
|
+
: accessMdPathForFolder(governingFolderOf(rel));
|
|
758
|
+
if (!(await accessControl.canWrite(workspaceId, email, rulesPath))) {
|
|
759
|
+
return {
|
|
760
|
+
why,
|
|
761
|
+
roster: null,
|
|
762
|
+
rosterReason: `You cannot change who has access here (no write on ${toWs(rulesPath)}), so only your own access is shown.`,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const roster = accessRoster(await resolveAccessView(accessControl, workspaceId, rel, email, kind), kind, rel);
|
|
766
|
+
const rosterWs = Object.fromEntries(
|
|
767
|
+
Object.entries(roster).map(([verb, entries]) => [
|
|
768
|
+
verb,
|
|
769
|
+
entries.map((e) => ({ ...e, sources: e.sources.map((s) => ({ ...s, path: toWs(s.path) })) })),
|
|
770
|
+
]),
|
|
771
|
+
);
|
|
772
|
+
return { why, roster: rosterWs };
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Refuse a file tool call that names the repository's git folder, in any
|
|
777
|
+
* input, before any gate, lock or read runs (see `shared/git-internals.ts`).
|
|
778
|
+
* Every spelling first, then the resolved form against the branch's
|
|
779
|
+
* workspace, so a link into the folder is refused the same way. The resolved
|
|
780
|
+
* check only runs on a branch that is already cloned: bootstrapping a clone
|
|
781
|
+
* here would happen before the handler's access and ontology gates. A branch
|
|
782
|
+
* not cloned yet (or that does not resolve) is left to the handler; the
|
|
783
|
+
* filesystem refuses again underneath regardless.
|
|
784
|
+
*/
|
|
785
|
+
const assertToolPathsNotGitInternals = async (args: Record<string, unknown>, ctx: ToolContext): Promise<void> => {
|
|
786
|
+
const paths: string[] = [];
|
|
787
|
+
for (const key of ['path', 'src', 'dest', 'destination'] as const) {
|
|
788
|
+
if (typeof args[key] === 'string') paths.push(args[key]);
|
|
789
|
+
}
|
|
790
|
+
if (Array.isArray(args.files)) {
|
|
791
|
+
for (const f of args.files as unknown[]) {
|
|
792
|
+
const fp = f && typeof f === 'object' ? (f as Record<string, unknown>).path : undefined;
|
|
793
|
+
if (typeof fp === 'string') paths.push(fp);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
for (const p of paths) assertNoGitInternalsSegment(p);
|
|
797
|
+
const onDisk = paths.filter((p) => !spillStore.isSpillRef(p));
|
|
798
|
+
if (onDisk.length === 0 || typeof args.branch !== 'string' || args.branch === '') return;
|
|
799
|
+
let fs: LocalFilesystem;
|
|
800
|
+
try {
|
|
801
|
+
if (!(await ctx.workspaceService.hasBootstrappedWorkspace(workspaceIdForBranch(args.branch)))) return;
|
|
802
|
+
fs = await ctx.getFilesystem(args.branch);
|
|
803
|
+
} catch {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (!(fs instanceof GitGuardedFilesystem)) return;
|
|
807
|
+
for (const p of onDisk) await fs.assertNotGitInternals(p);
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// ── preflight for moves and deletes ─────────────────────────────────────
|
|
811
|
+
// What an agent is told before (and instead of) a destructive operation:
|
|
812
|
+
// whether the item is the platform's own, what the caller may do with it,
|
|
813
|
+
// how much a folder takes with it, and — for a move — whether the caller's
|
|
814
|
+
// access changes on the way. The same verdicts gate the execution, so a
|
|
815
|
+
// dry run and the real call never disagree.
|
|
816
|
+
|
|
817
|
+
/** The caller's verdicts on a KB path. A path outside the repository carries no rules. */
|
|
818
|
+
const accessAt = async (branch: string, ctx: ToolContext, path: string): Promise<AccessVerbs> => {
|
|
819
|
+
const rel = toKbRelative(path, kbDirName);
|
|
820
|
+
if (rel === null) return { read: true, write: true, download: true, owner: false };
|
|
821
|
+
const wid = workspaceIdForBranch(branch);
|
|
822
|
+
const email = ctx.user.email;
|
|
823
|
+
const [read, write, download, owner] = await Promise.all([
|
|
824
|
+
accessControl.canRead(wid, email, rel),
|
|
825
|
+
accessControl.canWrite(wid, email, rel),
|
|
826
|
+
accessControl.canDownload(wid, email, rel),
|
|
827
|
+
accessControl.canOwner(wid, email, rel),
|
|
828
|
+
]);
|
|
829
|
+
return { read, write, download, owner };
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* The paths among `paths` the caller may NOT write, judged exactly as the
|
|
834
|
+
* lock gate judges them (`WorkflowService.acquireLock`): on a protected
|
|
835
|
+
* branch only, against the access tree at HEAD, with no rules at HEAD
|
|
836
|
+
* meaning allow. Empty on a draft branch — changes there reach a protected
|
|
837
|
+
* branch only through a change request.
|
|
838
|
+
*/
|
|
839
|
+
const writeBlocked = async (branch: string, ctx: ToolContext, paths: string[]): Promise<string[]> => {
|
|
840
|
+
if (!isProtectedBranch(branch)) return [];
|
|
841
|
+
const byRel = new Map<string, string>();
|
|
842
|
+
for (const p of paths) {
|
|
843
|
+
const rel = toKbRelative(p, kbDirName);
|
|
844
|
+
if (rel !== null) byRel.set(rel, p);
|
|
845
|
+
}
|
|
846
|
+
if (byRel.size === 0) return [];
|
|
847
|
+
const verdicts = await accessControl.canWriteBatchAtRef(
|
|
848
|
+
workspaceIdForBranch(branch),
|
|
849
|
+
'HEAD',
|
|
850
|
+
ctx.user.email,
|
|
851
|
+
[...byRel.keys()],
|
|
852
|
+
);
|
|
853
|
+
if (!verdicts) return [];
|
|
854
|
+
return [...byRel].filter(([rel]) => verdicts.get(rel) !== true).map(([, p]) => p);
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* The refusal the lock gate itself would raise for a path this tool's own
|
|
859
|
+
* preflight already found unwritable — same `AccessDeniedError`, same
|
|
860
|
+
* "Eligible: …" reading, from the same `eligibleWritersAtRef`.
|
|
861
|
+
*
|
|
862
|
+
* Raised as that error rather than as a finished body on purpose: every
|
|
863
|
+
* proposable tool's handler is wrapped in ONE mapping
|
|
864
|
+
* (`rethrowAsWriteDenial`), which turns an access refusal into the
|
|
865
|
+
* `write-denied` answer with whether and how to propose instead. Going
|
|
866
|
+
* through it means a refusal the preflight found and a refusal the gate
|
|
867
|
+
* found are the same answer in the same shape, and there is one place that
|
|
868
|
+
* decides what that shape is.
|
|
869
|
+
*/
|
|
870
|
+
const writeRefusal = async (
|
|
871
|
+
branch: string,
|
|
872
|
+
path: string,
|
|
873
|
+
/** What `path` is — a folder the move or delete was judged on, or a file. See `AccessDeniedDetails.targetKind`. */
|
|
874
|
+
targetKind: 'file' | 'dir' = 'file',
|
|
875
|
+
): Promise<AccessDeniedError> => {
|
|
876
|
+
const rel = toKbRelative(path, kbDirName);
|
|
877
|
+
const eligible = rel === null
|
|
878
|
+
? null
|
|
879
|
+
: await accessControl.eligibleWritersAtRef(workspaceIdForBranch(branch), 'HEAD', rel);
|
|
880
|
+
return new AccessDeniedError({
|
|
881
|
+
path,
|
|
882
|
+
eligibleRoles: eligible?.roles ?? [],
|
|
883
|
+
eligibleUsers: eligible?.users ?? [],
|
|
884
|
+
targetKind,
|
|
885
|
+
});
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
/** Whether a workspace-relative path is, or lies inside, a repository's `.git` metadata. */
|
|
889
|
+
const isGitMetadata = (path: string): boolean => path.split('/').includes('.git');
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Why the item at `path` is the platform's own and may not be moved or
|
|
893
|
+
* deleted — a platform file, the root or a reserved root folder, or git
|
|
894
|
+
* metadata — or undefined when it is content. Judged on `path`'s on-disk
|
|
895
|
+
* spelling (see `onDiskSpelling`), so an alternate casing on a
|
|
896
|
+
* case-insensitive disk is judged as the item it opens.
|
|
897
|
+
*/
|
|
898
|
+
const managedReason = (path: string, kind: 'file' | 'folder'): string | undefined => {
|
|
899
|
+
const norm = path.replace(/^\.?\/+/, '').replace(/\/+$/, '');
|
|
900
|
+
if (isGitMetadata(norm)) return `"${norm}" is git metadata and cannot be moved or deleted.`;
|
|
901
|
+
if (kind === 'file') {
|
|
902
|
+
const rel = toKbRelative(norm, kbDirName);
|
|
903
|
+
return rel !== null && isPlatformFile(rel) ? platformFileRefusal(rel) : undefined;
|
|
904
|
+
}
|
|
905
|
+
if (norm === '' || norm === kbDirName) return platformFolderRefusal('');
|
|
906
|
+
const rel = toKbRelative(norm, kbDirName);
|
|
907
|
+
return rel !== null && isPlatformFolder(rel) ? platformFolderRefusal(rel) : undefined;
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
/** The workspace root on disk for `branch`. */
|
|
911
|
+
const workspaceRoot = (branch: string, ctx: ToolContext): Promise<string> =>
|
|
912
|
+
ctx.workspaceService.getWorkspacePath(workspaceIdForBranch(branch));
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* `path` spelled as it is on disk: each segment that is not there verbatim
|
|
916
|
+
* but matches exactly one entry case-insensitively takes that entry's name.
|
|
917
|
+
* On a case-sensitive disk an existing path comes back unchanged; on a
|
|
918
|
+
* case-insensitive one `knowledge-base/skills` comes back as the
|
|
919
|
+
* `knowledge-base/Skills` it opens, so the platform checks see the real item.
|
|
920
|
+
*/
|
|
921
|
+
const onDiskSpelling = async (root: string, path: string): Promise<string> => {
|
|
922
|
+
const segments = path.replace(/^\.?\/+/, '').replace(/\/+$/, '').split('/').filter(Boolean);
|
|
923
|
+
const out: string[] = [];
|
|
924
|
+
for (const segment of segments) {
|
|
925
|
+
let names: string[];
|
|
926
|
+
try {
|
|
927
|
+
names = await nodeFs.readdir(join(root, ...out));
|
|
928
|
+
} catch {
|
|
929
|
+
return [...out, ...segments.slice(out.length)].join('/');
|
|
930
|
+
}
|
|
931
|
+
if (!names.includes(segment)) {
|
|
932
|
+
const matches = names.filter((n) => n.toLowerCase() === segment.toLowerCase());
|
|
933
|
+
out.push(matches.length === 1 ? matches[0] : segment);
|
|
934
|
+
} else {
|
|
935
|
+
out.push(segment);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return out.join('/');
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Refuse a path with a `.` or `..` segment or a backslash. Moves and deletes
|
|
943
|
+
* judge the path as written — its access rules, its platform status, its
|
|
944
|
+
* links — so it must be the path of the item that is changed:
|
|
945
|
+
* `knowledge-base/Public/../Locked/x.md` would be judged under `Public/`
|
|
946
|
+
* while removing `Locked/x.md`, or leave the clone altogether.
|
|
947
|
+
*/
|
|
948
|
+
const assertPlainPath = (path: string): void => {
|
|
949
|
+
const segments = path.replace(/^\.?\/+/, '').replace(/\/+$/, '').split('/');
|
|
950
|
+
if (path.includes('\\') || segments.some((seg) => seg === '.' || seg === '..')) {
|
|
951
|
+
throw new ToolError(`"${path}" may not contain "." or ".." segments or backslashes; name the item by its own path.`, 400);
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* The first part of `path` inside the repository that is a symbolic link —
|
|
957
|
+
* the last part too, unless `allowLast` — or undefined. Never follows one.
|
|
958
|
+
* The workspace root and the clone folder itself are the operator's and are
|
|
959
|
+
* not judged.
|
|
960
|
+
*/
|
|
961
|
+
const symlinkOnPath = async (root: string, path: string, allowLast = false): Promise<string | undefined> => {
|
|
962
|
+
const segments = path.replace(/^\.?\/+/, '').replace(/\/+$/, '').split('/').filter(Boolean);
|
|
963
|
+
if (segments[0] !== kbDirName) return undefined;
|
|
964
|
+
const last = allowLast ? segments.length - 1 : segments.length;
|
|
965
|
+
for (let i = 2; i <= last; i++) {
|
|
966
|
+
const prefix = segments.slice(0, i).join('/');
|
|
967
|
+
try {
|
|
968
|
+
if ((await nodeFs.lstat(join(root, prefix))).isSymbolicLink()) return prefix;
|
|
969
|
+
} catch (err) {
|
|
970
|
+
if (isAbsence(err)) return undefined;
|
|
971
|
+
throw err;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return undefined;
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Refuse `path` when it is not plain (see `assertPlainPath`) or any part of
|
|
979
|
+
* it inside the repository is a symbolic link — the last part too, unless
|
|
980
|
+
* `allowLast` (a link removed on its own is just the link). A link on the way
|
|
981
|
+
* would carry the write somewhere else — beside the clone, where git never
|
|
982
|
+
* sees it, or into another folder whose rules were never consulted — so none
|
|
983
|
+
* is followed.
|
|
984
|
+
*/
|
|
985
|
+
const assertNoSymlinkOnPath = async (root: string, path: string, allowLast = false): Promise<void> => {
|
|
986
|
+
assertPlainPath(path);
|
|
987
|
+
const link = await symlinkOnPath(root, path, allowLast);
|
|
988
|
+
if (link !== undefined) {
|
|
989
|
+
throw new ToolError(`"${path}" goes through the symbolic link "${link}"; moves and deletes never follow links.`, 400);
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* What is already at `dest` — `file` or `folder` — or null when the name is
|
|
995
|
+
* free. The kind is what the refusal names, so the sentence says "A folder
|
|
996
|
+
* named …" of a folder.
|
|
997
|
+
*
|
|
998
|
+
* With `src`, the one case that is not a clash is the destination BEING the
|
|
999
|
+
* source — `deal.md` → `Deal.md` on a case-insensitive disk, where the two
|
|
1000
|
+
* spellings are one entry. `inspectDestination` decides that, by the same
|
|
1001
|
+
* reading the move itself uses, so the preflight and the move cannot answer
|
|
1002
|
+
* differently: one inode is not enough (two hard links are one inode under
|
|
1003
|
+
* two names a user sees separately), the parent has to list one entry for
|
|
1004
|
+
* the two spellings. Without `src` — a copy, which creates a second entry
|
|
1005
|
+
* rather than moving the first — anything at `dest` is a clash, the source's
|
|
1006
|
+
* own alternate spelling included.
|
|
1007
|
+
*/
|
|
1008
|
+
const existingAt = async (
|
|
1009
|
+
root: string,
|
|
1010
|
+
dest: string,
|
|
1011
|
+
src?: string,
|
|
1012
|
+
): Promise<ExistingEntryKind | null> => {
|
|
1013
|
+
if (src !== undefined) {
|
|
1014
|
+
const verdict = await inspectDestination(join(root, src), join(root, dest));
|
|
1015
|
+
return verdict.state === 'taken' ? verdict.kind : null;
|
|
1016
|
+
}
|
|
1017
|
+
let destStat: import('node:fs').Stats;
|
|
1018
|
+
try {
|
|
1019
|
+
destStat = await nodeFs.lstat(join(root, dest));
|
|
1020
|
+
} catch (err) {
|
|
1021
|
+
if (isAbsence(err)) return null;
|
|
1022
|
+
throw err;
|
|
1023
|
+
}
|
|
1024
|
+
return destStat.isDirectory() ? 'folder' : 'file';
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Run a move or a copy and answer a lost race the way the look before it
|
|
1029
|
+
* would have: 409 with the one sentence. The filesystem refuses a taken
|
|
1030
|
+
* destination atomically (see `shared/rename-no-replace.ts`), which is what
|
|
1031
|
+
* makes the refusal true even when the name is claimed after the look; this
|
|
1032
|
+
* only carries that refusal out as a tool error rather than a 500.
|
|
1033
|
+
*/
|
|
1034
|
+
const asEntryExists = async <T>(run: () => Promise<T>): Promise<T> => {
|
|
1035
|
+
try {
|
|
1036
|
+
return await run();
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
if (err instanceof DestinationTakenError) throw new ToolError(err.message, 409);
|
|
1039
|
+
throw err;
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
const kindOf = async (fs: LocalFilesystem, path: string): Promise<'file' | 'folder' | null> => {
|
|
1044
|
+
try {
|
|
1045
|
+
return (await fs.stat(path)).type === 'directory' ? 'folder' : 'file';
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
if (isAbsence(err) || (err as { name?: string }).name === 'FileNotFoundError') return null;
|
|
1048
|
+
throw err;
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Every file under `dir`, at any depth, as workspace-relative paths; counts to
|
|
1054
|
+
* `cap` (the walk itself goes on, for `links`). A symbolic link is listed as the entry it is, never followed, so the
|
|
1055
|
+
* walk cannot wander into a folder elsewhere, and is ALSO named in `links`:
|
|
1056
|
+
* the per-file delete cannot remove a link (it stats through it, so a dangling
|
|
1057
|
+
* link or a link to a folder fails), so a folder holding one is refused before
|
|
1058
|
+
* anything is deleted. The git folder is skipped, in any spelling of its name.
|
|
1059
|
+
*/
|
|
1060
|
+
const filesUnder = async (
|
|
1061
|
+
fs: LocalFilesystem,
|
|
1062
|
+
dir: string,
|
|
1063
|
+
cap = Infinity,
|
|
1064
|
+
): Promise<{ files: string[]; links: string[]; truncated: boolean }> => {
|
|
1065
|
+
const files: string[] = [];
|
|
1066
|
+
const links: string[] = [];
|
|
1067
|
+
let truncated = false;
|
|
1068
|
+
// The cap STOPS the walk, so a caller that passes one does bounded work on
|
|
1069
|
+
// a folder of any size. It is the deciding caller's job to make a
|
|
1070
|
+
// truncated answer a refusal rather than a guess: `file_stat`, the only
|
|
1071
|
+
// capped caller, answers `movable`/`deletable` false once `truncated` is
|
|
1072
|
+
// set, which covers the link it may not have reached. The tools that act —
|
|
1073
|
+
// `delete_folder`, `move_file` — pass no cap and see every file and every
|
|
1074
|
+
// link, because they must judge all of them.
|
|
1075
|
+
const walk = async (d: string): Promise<void> => {
|
|
1076
|
+
for (const e of (await fs.readdir(d)) as DirEntry[]) {
|
|
1077
|
+
if (truncated) return;
|
|
1078
|
+
const child = `${d.replace(/\/+$/, '')}/${e.name}`;
|
|
1079
|
+
if (hasGitInternalsSegment(e.name)) continue;
|
|
1080
|
+
if (e.type === 'directory' && !e.isSymlink) {
|
|
1081
|
+
await walk(child);
|
|
1082
|
+
} else {
|
|
1083
|
+
if (e.isSymlink) links.push(child);
|
|
1084
|
+
if (files.length < cap) files.push(child);
|
|
1085
|
+
else {
|
|
1086
|
+
truncated = true;
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
await walk(dir);
|
|
1093
|
+
return { files, links, truncated };
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
/** Whether `path` itself is a symbolic link (never followed). */
|
|
1097
|
+
const isSymlinkAt = async (root: string, path: string): Promise<boolean> => {
|
|
1098
|
+
try {
|
|
1099
|
+
return (await nodeFs.lstat(join(root, path))).isSymbolicLink();
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
if (isAbsence(err)) return false;
|
|
1102
|
+
throw err;
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
/** The refusal for a folder that holds symbolic links: named, with the way out. */
|
|
1107
|
+
const linksRefusal = (path: string, links: string[]): string =>
|
|
1108
|
+
`"${path}" holds ${links.length === 1 ? 'the symbolic link' : `${links.length} symbolic links, e.g.`} "${links[0]}"; ` +
|
|
1109
|
+
'the agent tools never follow or remove links, so the folder cannot be deleted through them. Remove the link outside the agent tools first.';
|
|
1110
|
+
|
|
1111
|
+
/** The refusal for a path that is itself a symbolic link. */
|
|
1112
|
+
const linkRefusal = (path: string): string =>
|
|
1113
|
+
`"${path}" is a symbolic link; the agent tools never follow or remove links.`;
|
|
1114
|
+
|
|
321
1115
|
const mount = (spec: {
|
|
322
1116
|
name: string;
|
|
323
|
-
|
|
1117
|
+
/**
|
|
1118
|
+
* A FUNCTION for a description that names something the layout decides —
|
|
1119
|
+
* the guide's file name, the platform files it belongs to. Those are
|
|
1120
|
+
* applied at boot, but also by the save that completes first-run setup,
|
|
1121
|
+
* which happens AFTER these tools are mounted; a plain string would
|
|
1122
|
+
* snapshot whatever was in effect at mount time and go on telling agents
|
|
1123
|
+
* to read `AGENTS.md` on a deployment that just named its guide something
|
|
1124
|
+
* else. Rebuilt from the function whenever the layout is applied (below).
|
|
1125
|
+
*/
|
|
1126
|
+
description: string | (() => string);
|
|
324
1127
|
inputs: JsonSchema;
|
|
325
1128
|
outputs?: JsonSchema;
|
|
326
1129
|
write: boolean;
|
|
327
1130
|
internalOnly?: boolean;
|
|
1131
|
+
/**
|
|
1132
|
+
* A permission refusal from this tool is answered as `write-denied`
|
|
1133
|
+
* (with whether and how to propose the change instead), and the
|
|
1134
|
+
* description says so.
|
|
1135
|
+
*/
|
|
1136
|
+
proposable?: boolean;
|
|
1137
|
+
/** False for a tool that is not a file tool (the shell), which the content rule does not describe. */
|
|
1138
|
+
fileTool?: boolean;
|
|
328
1139
|
handler: ToolHandler;
|
|
329
1140
|
}): void => {
|
|
330
1141
|
const path = `/api/agent/tools/${spec.name}`;
|
|
1142
|
+
// Every workspace entrypoint carries the agent-guide reminder, every file
|
|
1143
|
+
// tool the one content rule, and every tool a permission can refuse the
|
|
1144
|
+
// proposal route — appended once here so no tool (especially the
|
|
1145
|
+
// read-only ones a session hits first) can miss them.
|
|
1146
|
+
const describe = (): string =>
|
|
1147
|
+
(typeof spec.description === 'function' ? spec.description() : spec.description) +
|
|
1148
|
+
(spec.proposable ? PROPOSAL_ROUTE_NOTE : '') +
|
|
1149
|
+
(spec.fileTool === false ? '' : CONTENT_RULE) +
|
|
1150
|
+
kbConventionsNote();
|
|
331
1151
|
const def = toolDef({
|
|
332
1152
|
name: spec.name,
|
|
333
|
-
|
|
334
|
-
// here so no tool (especially the read-only ones a session hits first) can
|
|
335
|
-
// miss it.
|
|
336
|
-
description: spec.description + KB_CONVENTIONS_NOTE,
|
|
1153
|
+
description: describe(),
|
|
337
1154
|
path,
|
|
338
1155
|
inputs: spec.inputs,
|
|
339
1156
|
outputs: spec.outputs,
|
|
@@ -341,6 +1158,16 @@ export function registerWorkspaceTools(
|
|
|
341
1158
|
});
|
|
342
1159
|
registry.registerInternalTool(def);
|
|
343
1160
|
if (!spec.internalOnly) registry.registerExternalTool(def);
|
|
1161
|
+
// The catalog FOLLOWS the layout. The conventions reminder above names the
|
|
1162
|
+
// guide, and several descriptions name it again as a platform file, so the
|
|
1163
|
+
// save that completes first-run setup — which applies the names the admin
|
|
1164
|
+
// just chose, in that same request, without a restart — must be able to
|
|
1165
|
+
// move the text with them. Rewritten in place: the registry holds this
|
|
1166
|
+
// object, both surfaces hold the same one, and re-registering would be a
|
|
1167
|
+
// duplicate name.
|
|
1168
|
+
onKbLayoutApplied(() => {
|
|
1169
|
+
def.description = describe();
|
|
1170
|
+
});
|
|
344
1171
|
// Internal-only tools (e.g. `execute_command`) keep their route mounted —
|
|
345
1172
|
// our agent calls it over the same loopback — but gate it to internal-source
|
|
346
1173
|
// callers so an external connection key can't invoke it by name.
|
|
@@ -348,7 +1175,52 @@ export function registerWorkspaceTools(
|
|
|
348
1175
|
path.slice('/api'.length),
|
|
349
1176
|
toolAuth,
|
|
350
1177
|
...(spec.internalOnly ? [requireInternalSource] : []),
|
|
351
|
-
|
|
1178
|
+
// EVERY path input becomes a repository path here, once, before any
|
|
1179
|
+
// handler runs: the root-anchored `/<kbDirName>/…` form Copy path gives
|
|
1180
|
+
// names the same workspace path, and a path with no prefix at all is
|
|
1181
|
+
// placed under `<kbDirName>/` instead of being refused.
|
|
1182
|
+
//
|
|
1183
|
+
// The one exception is `read_file`'s `__tool_chain_spill__/…` ref, which
|
|
1184
|
+
// belongs to no workspace and is left exactly as it came — and it is
|
|
1185
|
+
// `read_file`'s ALONE. `read_file` is the only tool that consumes a
|
|
1186
|
+
// spill ref; for any other, `__tool_chain_spill__/x` is an ordinary
|
|
1187
|
+
// path, and exempting it there would be a workspace-relative path that
|
|
1188
|
+
// never reached the repository — the whole bug, spelled with a prefix.
|
|
1189
|
+
toolHandler(
|
|
1190
|
+
async (args, ctx) => {
|
|
1191
|
+
const normalized = normalizePathArgs(
|
|
1192
|
+
args,
|
|
1193
|
+
kbDirName,
|
|
1194
|
+
spec.name === 'read_file' ? (v) => spillStore.isSpillRef(v) : undefined,
|
|
1195
|
+
);
|
|
1196
|
+
// The checkout's own name is reserved at the repository root on this
|
|
1197
|
+
// surface too. The routes meet that rule inside `WorkspaceService`;
|
|
1198
|
+
// these tools write through the locking filesystem, which never
|
|
1199
|
+
// enters it, so the rule is applied here — on the inputs a tool
|
|
1200
|
+
// CREATES at, never on a source, so an existing reserved folder can
|
|
1201
|
+
// still be moved out of or deleted.
|
|
1202
|
+
const creates = RESERVED_ROOT_NAME_TARGETS[spec.name];
|
|
1203
|
+
if (creates) assertRepoRootNameFreeArgs(normalized, kbDirName, creates);
|
|
1204
|
+
// The git folder is refused before the handler — and so before the
|
|
1205
|
+
// write-denial wrapper below, which would otherwise offer to propose
|
|
1206
|
+
// a change to it.
|
|
1207
|
+
if (spec.fileTool !== false) await assertToolPathsNotGitInternals(normalized, ctx);
|
|
1208
|
+
if (!spec.proposable) return spec.handler(normalized, ctx);
|
|
1209
|
+
try {
|
|
1210
|
+
// Awaited here so a refusal is caught; proposable tools never stream.
|
|
1211
|
+
return await spec.handler(normalized, ctx);
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
return rethrowAsWriteDenial(
|
|
1214
|
+
err,
|
|
1215
|
+
{ tool: spec.name, branch: args.branch, userEmail: ctx.user.email, userId: ctx.user.id },
|
|
1216
|
+
accessControl,
|
|
1217
|
+
kbDirName,
|
|
1218
|
+
changeGate,
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
},
|
|
1222
|
+
{ write: spec.write },
|
|
1223
|
+
),
|
|
352
1224
|
);
|
|
353
1225
|
};
|
|
354
1226
|
|
|
@@ -369,10 +1241,19 @@ export function registerWorkspaceTools(
|
|
|
369
1241
|
// ontology and then having `ask` write into another. In a core-only
|
|
370
1242
|
// deployment (no chat/ask) the default sink mints a bare id, which is all
|
|
371
1243
|
// the ontology gate needs.
|
|
1244
|
+
//
|
|
1245
|
+
// The description tells the caller that retrying is safe, and that is a
|
|
1246
|
+
// property of the sink rather than a promise this route makes on its own:
|
|
1247
|
+
// minting leaves nothing half-made. A call that failed created no session
|
|
1248
|
+
// (the core sink is a random id and does no I/O at all; a chat thread the
|
|
1249
|
+
// enterprise sink failed to create does not exist), and two calls that both
|
|
1250
|
+
// succeed leave two unrelated ids, neither of which invalidates the other.
|
|
1251
|
+
// Saying so matters because the alternative is a caller that reads a
|
|
1252
|
+
// transport hiccup on its first call as an unrecoverable start.
|
|
372
1253
|
const startSessionDef = toolDef({
|
|
373
1254
|
name: 'start_session',
|
|
374
1255
|
description:
|
|
375
|
-
'Mint the KnowledgeBase session id this run needs to read or write the knowledge ontologies. Call this ONCE, before any other KnowledgeBase tool, and only once per run — every gated tool needs the `sessionId` it returns to enforce the one-ontology-per-conversation boundary, and minting a new id mid-run resets that boundary. The id is also a chat session in the app, so you can hand the SAME id to the `ask` tool: reads and ask then share one ontology boundary. Pass the returned id explicitly as `sessionId` on every subsequent KnowledgeBase tool call (direct MCP calls and inside `call_tool_chain` alike). Returns `{ sessionId }`.',
|
|
1256
|
+
'Mint the KnowledgeBase session id this run needs to read or write the knowledge ontologies. Call this ONCE, before any other KnowledgeBase tool, and only once per run — every gated tool needs the `sessionId` it returns to enforce the one-ontology-per-conversation boundary, and minting a new id mid-run resets that boundary. The id is also a chat session in the app, so you can hand the SAME id to the `ask` tool: reads and ask then share one ontology boundary. Pass the returned id explicitly as `sessionId` on every subsequent KnowledgeBase tool call (direct MCP calls and inside `call_tool_chain` alike). RETRYING IS SAFE: a call that fails created nothing, so retry it — there is no half-made session to clean up. If a retry lands after a success you simply hold two independent ids, which is harmless: keep passing the one id you have already used for the rest of the run and ignore the other. Returns `{ sessionId }`.',
|
|
376
1257
|
path: '/api/agent/tools/start_session',
|
|
377
1258
|
inputs: { type: 'object', properties: {}, additionalProperties: false },
|
|
378
1259
|
outputs: {
|
|
@@ -411,7 +1292,7 @@ export function registerWorkspaceTools(
|
|
|
411
1292
|
type: 'object',
|
|
412
1293
|
properties: {
|
|
413
1294
|
branch: BRANCH_INPUT,
|
|
414
|
-
path: str(`Path to read,
|
|
1295
|
+
path: str(`Path to read, under \`${kbDirName}/\` (e.g. \`${kbDirName}/KnowledgeBase/Foo.md\`), with or without a leading slash — a path without that prefix is placed under \`${kbDirName}/\` — or a \`__tool_chain_spill__/…\` ref from a truncated \`call_tool_chain\`.`),
|
|
415
1296
|
offset: int('Start character index (default 0).'),
|
|
416
1297
|
limit: int('Max characters to return from `offset`.'),
|
|
417
1298
|
sessionId: SESSION_ID_INPUT,
|
|
@@ -440,7 +1321,8 @@ export function registerWorkspaceTools(
|
|
|
440
1321
|
// read is still a KB read. ONE registry dispatch picks the reader by
|
|
441
1322
|
// extension; everything below just maps its ReadResult onto the tool's
|
|
442
1323
|
// result shape.
|
|
443
|
-
const
|
|
1324
|
+
const bytes = await orNotFound(p, async () => asBytes(await fs.readFile(p)));
|
|
1325
|
+
const result = await readers.readerFor(p).read(bytes, p);
|
|
444
1326
|
// Images return the picture itself as an MCP image content block, so a
|
|
445
1327
|
// multimodal model SEES it. The handler returns the `McpImageResult`
|
|
446
1328
|
// sentinel; the MCP result shaping (`toCallToolResult` in
|
|
@@ -470,13 +1352,13 @@ export function registerWorkspaceTools(
|
|
|
470
1352
|
mount({
|
|
471
1353
|
name: 'list_files',
|
|
472
1354
|
description:
|
|
473
|
-
`List a directory. Returns \`{ path, entries: [{ name, type, size? }] }\`. Omit \`path\` for the workspace root, which holds the repository as the \`${kbDirName}/\` folder: every content path
|
|
1355
|
+
`List a directory. Returns \`{ path, entries: [{ name, type, size? }] }\`. Omit \`path\` for the workspace root, which holds the repository as the \`${kbDirName}/\` folder: every content path is under it (e.g. \`${kbDirName}/KnowledgeBase\`), and a path given without that prefix is placed under it.` +
|
|
474
1356
|
ONTOLOGY_BOUNDARY_NOTE,
|
|
475
1357
|
inputs: {
|
|
476
1358
|
type: 'object',
|
|
477
1359
|
properties: {
|
|
478
1360
|
branch: BRANCH_INPUT,
|
|
479
|
-
path: str(`Directory to list,
|
|
1361
|
+
path: str(`Directory to list, under \`${kbDirName}/\`, with or without a leading slash — a path without that prefix is placed under \`${kbDirName}/\` (default: the workspace root, where the repository is the \`${kbDirName}/\` folder).`),
|
|
480
1362
|
sessionId: SESSION_ID_INPUT,
|
|
481
1363
|
},
|
|
482
1364
|
required: ['branch'],
|
|
@@ -503,7 +1385,7 @@ export function registerWorkspaceTools(
|
|
|
503
1385
|
const dir = (a.path as string) || '';
|
|
504
1386
|
await recordOntologyRead(sessionOntologyGate, ctx, dir);
|
|
505
1387
|
const fs = await ctx.getFilesystem(a.branch as string);
|
|
506
|
-
const entries = (await fs.readdir(dir || '.')) as DirEntry[];
|
|
1388
|
+
const entries = withoutPlaceholder((await fs.readdir(dir || '.')) as DirEntry[]);
|
|
507
1389
|
const filtered = await filterReadableEntries(readGateFor(a.branch as string, ctx), dir, entries);
|
|
508
1390
|
return { path: a.path ?? '', entries: filtered };
|
|
509
1391
|
},
|
|
@@ -511,14 +1393,24 @@ export function registerWorkspaceTools(
|
|
|
511
1393
|
|
|
512
1394
|
mount({
|
|
513
1395
|
name: 'file_stat',
|
|
514
|
-
description:
|
|
515
|
-
'Get a file/directory\'s metadata (name, type, size, …) without
|
|
1396
|
+
description: () =>
|
|
1397
|
+
'Get a file/directory\'s metadata (name, type, size, …) without returning content. A file also reports `contentMode`: `text` (read, write and edit it as text), `document` (read returns an extraction; replace it by upload) or `binary` (bytes: copy, move, delete, or replace by upload), plus `kind` (`text` | `document` | `image` | `binary`), `mime`, `mimeSource` and `textEditable` — decided by the same file readers read_file, grep and the write tools use, so an extensionless text file is `text/plain`.' +
|
|
1398
|
+
' Every entry also reports what you may DO with it. ' +
|
|
1399
|
+
`\`managed\` is true for a platform item — a platform file (${platformFileList()}) or a platform folder (the repository root or a reserved root folder such as \`KnowledgeBase/\`); managed items are never movable or deletable through these tools. ` +
|
|
1400
|
+
'`access: { read, write, download, owner }` is your own verdict under the access rules; pass `explainAccess: true` to learn why, and who else holds each verb. `movable` and `deletable` say whether `move_file` / `delete_file` / `delete_folder` would be allowed for you, judged like their dry runs: not managed, no symbolic link, and on a protected branch you hold write on the item AND on every file under a folder (on a draft branch writes are not gated). `movable` judges the source side only; the destination is judged by a `move_file` dry run. ' +
|
|
1401
|
+
'For a folder, `descendants` is the number of files under it at any depth; counting stops at 10000 and `descendantsTruncated` says so, and past that point `movable` and `deletable` are false because a folder that large was not judged in full — run the `move_file` or `delete_folder` dry run for the real verdict. ' +
|
|
1402
|
+
'Call this before a move or delete to see what it would touch.' +
|
|
516
1403
|
ONTOLOGY_BOUNDARY_NOTE,
|
|
517
1404
|
inputs: {
|
|
518
1405
|
type: 'object',
|
|
519
1406
|
properties: {
|
|
520
1407
|
branch: BRANCH_INPUT,
|
|
521
1408
|
path: wsPath(kbDirName, 'Path'),
|
|
1409
|
+
explainAccess: {
|
|
1410
|
+
type: 'boolean',
|
|
1411
|
+
description:
|
|
1412
|
+
'Also explain `access` (default false): `access.why` says what decided each of your verdicts — the folder rules or file frontmatter and whether that is inherited; `access.roster` lists who holds each verb and where each grant is written, given only when you can manage this path\'s access (otherwise null, with `access.rosterReason`).',
|
|
1413
|
+
},
|
|
522
1414
|
sessionId: SESSION_ID_INPUT,
|
|
523
1415
|
},
|
|
524
1416
|
required: ['branch', 'path'],
|
|
@@ -527,29 +1419,173 @@ export function registerWorkspaceTools(
|
|
|
527
1419
|
outputs: {
|
|
528
1420
|
type: 'object',
|
|
529
1421
|
description: "The filesystem entry's metadata.",
|
|
530
|
-
properties: {
|
|
1422
|
+
properties: {
|
|
1423
|
+
name: str('Entry name.'),
|
|
1424
|
+
type: str('`file` or `directory`.'),
|
|
1425
|
+
size: int('Size in bytes.'),
|
|
1426
|
+
managed: { type: 'boolean', description: 'True for a platform file or platform folder.' },
|
|
1427
|
+
movable: { type: 'boolean', description: 'Whether `move_file` would be allowed for you.' },
|
|
1428
|
+
deletable: { type: 'boolean', description: 'Whether `delete_file` (a file) or `delete_folder` (a folder) would be allowed for you.' },
|
|
1429
|
+
access: {
|
|
1430
|
+
type: 'object',
|
|
1431
|
+
description: 'Your verdict per access verb on this path.',
|
|
1432
|
+
properties: {
|
|
1433
|
+
read: { type: 'boolean', description: 'You may read it.' },
|
|
1434
|
+
write: { type: 'boolean', description: 'You may write it.' },
|
|
1435
|
+
download: { type: 'boolean', description: 'You may download it.' },
|
|
1436
|
+
owner: { type: 'boolean', description: 'You own it.' },
|
|
1437
|
+
why: {
|
|
1438
|
+
type: ['object', 'null'],
|
|
1439
|
+
description:
|
|
1440
|
+
'Only with `explainAccess: true`. `why.<verb>` is `{ source, via, principal }`: `source` is `{ kind: folder | frontmatter, path, inherited }` (null when no rule decided it — default-deny, admin rescue, a machine-owned file, or Admin\'s write at a root with no rules); `via` is `person`, `group`, `role`, `plugin`, `everyone`, `admin-rescue`, `admin-floor` (Admin always keeps write at the repository root), `machine-owned` or `default-deny`. Null for a path outside the repository.',
|
|
1441
|
+
},
|
|
1442
|
+
roster: {
|
|
1443
|
+
type: ['object', 'null'],
|
|
1444
|
+
description:
|
|
1445
|
+
'Only with `explainAccess: true`. `roster.<verb>` lists `{ kind: group | role | plugin | person, name, email?, sources }` as the Manage access dialog does; null with `rosterReason` when you cannot manage this path\'s access. Paths start with the repository folder.',
|
|
1446
|
+
},
|
|
1447
|
+
rosterReason: str('Present when `roster` is null: why only your own access is shown.'),
|
|
1448
|
+
},
|
|
1449
|
+
required: ['read', 'write', 'download', 'owner'],
|
|
1450
|
+
},
|
|
1451
|
+
descendants: int('Folders only: files under it at any depth.'),
|
|
1452
|
+
descendantsTruncated: { type: 'boolean', description: 'Folders only: true when counting stopped at the cap.' },
|
|
1453
|
+
contentMode: {
|
|
1454
|
+
type: 'string',
|
|
1455
|
+
enum: ['text', 'document', 'binary'],
|
|
1456
|
+
description: 'Files only: what the file tools can do with the content — `text` (read/write/edit as text), `document` (read extracts; replace by upload), `binary` (bytes: copy/move/delete; replace by upload).',
|
|
1457
|
+
},
|
|
1458
|
+
kind: {
|
|
1459
|
+
type: 'string',
|
|
1460
|
+
enum: ['text', 'document', 'image', 'binary'],
|
|
1461
|
+
description: 'Files only: what the file is, as read_file treats it (archives are `binary`; `mime` names them).',
|
|
1462
|
+
},
|
|
1463
|
+
mime: str('Files only: the MIME type — named by the extension, `text/plain` for text content, else `application/octet-stream`.'),
|
|
1464
|
+
mimeSource: {
|
|
1465
|
+
type: 'string',
|
|
1466
|
+
enum: ['extension', 'sniff', 'fallback'],
|
|
1467
|
+
description: 'Files only: where `mime` came from. `fallback` means no type was detected.',
|
|
1468
|
+
},
|
|
1469
|
+
textEditable: { type: 'boolean', description: 'Files only: whether write_file/write_files/edit_file accept this file as it is now.' },
|
|
1470
|
+
mimeNote: str('Present when `mimeSource` is `fallback`: says the MIME type is a fallback, not a detected type.'),
|
|
1471
|
+
},
|
|
1472
|
+
required: ['managed', 'movable', 'deletable', 'access'],
|
|
531
1473
|
additionalProperties: true,
|
|
532
1474
|
},
|
|
533
1475
|
write: false,
|
|
534
1476
|
handler: async (a, ctx: ToolContext) => {
|
|
535
1477
|
const p = a.path as string;
|
|
1478
|
+
const branch = a.branch as string;
|
|
536
1479
|
await recordOntologyRead(sessionOntologyGate, ctx, p);
|
|
537
|
-
await assertCanRead(readGateFor(
|
|
538
|
-
|
|
1480
|
+
await assertCanRead(readGateFor(branch, ctx), p);
|
|
1481
|
+
// Nothing there is a 404, and the placeholder — never content — gets
|
|
1482
|
+
// exactly that answer: the one every file tool gives (see not-found.ts).
|
|
1483
|
+
if (isFolderPlaceholder(p)) throw notFound(p);
|
|
1484
|
+
const fs = await ctx.getFilesystem(branch);
|
|
1485
|
+
const root = await workspaceRoot(branch, ctx);
|
|
1486
|
+
// Judged before `stat`, which follows links: a link anywhere on the path
|
|
1487
|
+
// (or a path move_file and delete_file would refuse as not plain) is
|
|
1488
|
+
// never movable or deletable, and a dangling one is named, not a 404.
|
|
1489
|
+
const segments = p.replace(/^\.?\/+/, '').replace(/\/+$/, '').split('/');
|
|
1490
|
+
const plain = !p.includes('\\') && !segments.some((seg) => seg === '.' || seg === '..');
|
|
1491
|
+
const viaLink = plain ? await symlinkOnPath(root, p) : undefined;
|
|
1492
|
+
let stat: Awaited<ReturnType<LocalFilesystem['stat']>>;
|
|
1493
|
+
try {
|
|
1494
|
+
stat = await fs.stat(p);
|
|
1495
|
+
} catch (err) {
|
|
1496
|
+
if (viaLink !== undefined) {
|
|
1497
|
+
throw new ToolError(`"${p}" goes through the symbolic link "${viaLink}", which leads nowhere; the agent tools never follow links.`, 400);
|
|
1498
|
+
}
|
|
1499
|
+
if (isAbsence(err)) throw notFound(p);
|
|
1500
|
+
throw err;
|
|
1501
|
+
}
|
|
1502
|
+
// The filesystem's own `mimeType` comes from a second extension table
|
|
1503
|
+
// (octet-stream for an extensionless text file) and would contradict
|
|
1504
|
+
// `mime` below, so it is never passed through.
|
|
1505
|
+
delete stat.mimeType;
|
|
1506
|
+
const kind = stat.type === 'directory' ? 'folder' : 'file';
|
|
1507
|
+
const managed = managedReason(await onDiskSpelling(root, p), kind) !== undefined;
|
|
1508
|
+
const verdicts = await accessAt(branch, ctx, p);
|
|
1509
|
+
const access =
|
|
1510
|
+
a.explainAccess === true ? { ...verdicts, ...(await explainAccessAt(branch, ctx, p, kind)) } : verdicts;
|
|
1511
|
+
const link = !plain || viaLink !== undefined;
|
|
1512
|
+
// Judged on the same paths move_file and delete_folder judge: a folder
|
|
1513
|
+
// move or delete touches every file under it, so a file its own rules
|
|
1514
|
+
// deny you makes the folder neither movable nor deletable, however
|
|
1515
|
+
// writable the folder is.
|
|
1516
|
+
//
|
|
1517
|
+
// Two bounds, because this is a READ tool the description tells agents
|
|
1518
|
+
// to call before every move and delete, and the folder it is asked about
|
|
1519
|
+
// may be the repository root:
|
|
1520
|
+
// - a platform item or a path through a link is already not movable
|
|
1521
|
+
// and not deletable, so no access verdict is asked for any file
|
|
1522
|
+
// under it (the count below is a plain directory walk);
|
|
1523
|
+
// - the walk stops at the cap, and a truncated walk answers
|
|
1524
|
+
// `movable`/`deletable` false rather than judging part of a folder
|
|
1525
|
+
// and calling it the whole (`delete_folder`'s dry run, which walks
|
|
1526
|
+
// uncapped, remains the authority for a folder that large).
|
|
1527
|
+
const decided = managed || link;
|
|
1528
|
+
const { files, links, truncated } =
|
|
1529
|
+
kind === 'folder'
|
|
1530
|
+
? await filesUnder(fs, p, DESCENDANTS_CAP)
|
|
1531
|
+
: { files: [p], links: [] as string[], truncated: false };
|
|
1532
|
+
const judged = kind === 'folder' ? [p, ...files] : [p];
|
|
1533
|
+
const writable = decided ? false : (await writeBlocked(branch, ctx, judged)).length === 0;
|
|
1534
|
+
// A restricted run (see IRoutineWritePolicy) is refused per file by both tools.
|
|
1535
|
+
const policyAllows =
|
|
1536
|
+
!decided &&
|
|
1537
|
+
files.every((file) => {
|
|
1538
|
+
try {
|
|
1539
|
+
writePolicy.assertPathWritable(ctx.sessionId, file);
|
|
1540
|
+
return true;
|
|
1541
|
+
} catch {
|
|
1542
|
+
return false;
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
const open = !decided && !truncated && writable && policyAllows;
|
|
1546
|
+
const out: Record<string, unknown> = {
|
|
1547
|
+
...stat,
|
|
1548
|
+
managed,
|
|
1549
|
+
movable: open,
|
|
1550
|
+
// delete_folder also refuses a folder holding a link.
|
|
1551
|
+
deletable: open && links.length === 0,
|
|
1552
|
+
access,
|
|
1553
|
+
};
|
|
1554
|
+
if (kind === 'folder') {
|
|
1555
|
+
// The placeholder is never content: a folder holding only it has none.
|
|
1556
|
+
out.descendants = files.filter((f) => !isFolderPlaceholder(f)).length;
|
|
1557
|
+
if (truncated) out.descendantsTruncated = true;
|
|
1558
|
+
return out;
|
|
1559
|
+
}
|
|
1560
|
+
// A FILE also reports what the file tools can do with its content. The
|
|
1561
|
+
// mode is decided by the same registry the write gates consult, so what
|
|
1562
|
+
// stat reports is what write_file will do. Only a reader whose answer
|
|
1563
|
+
// depends on the bytes (the text fallback) costs a read — one full read,
|
|
1564
|
+
// the same one write_file/edit_file already pay on the same file. A
|
|
1565
|
+
// head-only sniff would be cheaper but wrong: invalid UTF-8 or a NUL
|
|
1566
|
+
// anywhere makes the write gate refuse, so stat must judge the same
|
|
1567
|
+
// bytes or it would report `text` for a file the write then refuses.
|
|
1568
|
+
// `kind` and `mime` come from that same reader too, so stat never calls
|
|
1569
|
+
// a file binary that read_file returns as text.
|
|
1570
|
+
const reader = readers.readerFor(p);
|
|
1571
|
+
const bytes = needsContent(reader)
|
|
1572
|
+
? await orNotFound(p, async () => asBytes(await fs.readFile(p)))
|
|
1573
|
+
: undefined;
|
|
1574
|
+
return { ...out, ...fileTypeOf(reader, p, bytes) };
|
|
539
1575
|
},
|
|
540
1576
|
});
|
|
541
1577
|
|
|
542
1578
|
mount({
|
|
543
1579
|
name: 'grep',
|
|
544
1580
|
description:
|
|
545
|
-
'Regex content search across the workspace. Returns `{ matches: [{ path, line, text }] }` (capped). Use to find where something is defined/referenced. 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).' +
|
|
1581
|
+
'Regex content 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).' +
|
|
546
1582
|
ONTOLOGY_BOUNDARY_NOTE,
|
|
547
1583
|
inputs: {
|
|
548
1584
|
type: 'object',
|
|
549
1585
|
properties: {
|
|
550
1586
|
branch: BRANCH_INPUT,
|
|
551
1587
|
pattern: str('JavaScript regular expression.'),
|
|
552
|
-
path: str(
|
|
1588
|
+
path: str(`Subtree to search, or a single file to search on its own, with or without a leading slash — a path without the \`${kbDirName}/\` prefix is placed under \`${kbDirName}/\` (default: the whole repository, \`${kbDirName}/\`).`),
|
|
553
1589
|
ignore_case: { type: 'boolean', description: 'Case-insensitive match.' },
|
|
554
1590
|
max_results: { type: 'integer', minimum: 1, maximum: 1000, description: 'Cap on matches (default 200).' },
|
|
555
1591
|
sessionId: SESSION_ID_INPUT,
|
|
@@ -570,7 +1606,7 @@ export function registerWorkspaceTools(
|
|
|
570
1606
|
},
|
|
571
1607
|
},
|
|
572
1608
|
truncated: { type: 'boolean', description: 'True if the match cap was hit and results may be incomplete.' },
|
|
573
|
-
note: str('Present when
|
|
1609
|
+
note: str('Present when the empty/partial result needs explaining: a `path` naming a file with no searchable text (image, binary, corrupt document), or documents (office/PDF/email files) left unsearched because their text was not yet extracted and the per-call extraction budget ran out — re-run grep to cover those.'),
|
|
574
1610
|
},
|
|
575
1611
|
required: ['matches', 'truncated'],
|
|
576
1612
|
},
|
|
@@ -582,37 +1618,86 @@ export function registerWorkspaceTools(
|
|
|
582
1618
|
} catch (err) {
|
|
583
1619
|
throw new ToolError(`Invalid regex: ${(err as Error).message}`, 400);
|
|
584
1620
|
}
|
|
585
|
-
|
|
1621
|
+
// No path means the whole REPOSITORY, not the workspace directory above
|
|
1622
|
+
// it. The workspace root also holds whatever an older build left beside
|
|
1623
|
+
// the checkout, and the read gate has no rules for a path outside the
|
|
1624
|
+
// repository — it answers "readable" — so a walk from there would hand
|
|
1625
|
+
// any caller the contents of every stray, including documents that were
|
|
1626
|
+
// uploaded to a restricted folder and landed beside it instead. The
|
|
1627
|
+
// boot note names those for an operator; no tool reads them.
|
|
1628
|
+
// An EMPTY string is the same absence: the normaliser leaves it alone
|
|
1629
|
+
// (an empty path is the handler's to explain), and here it would
|
|
1630
|
+
// otherwise name the workspace directory by another spelling.
|
|
1631
|
+
const searchRoot = typeof a.path === 'string' && a.path.length > 0 ? a.path : kbDirName;
|
|
586
1632
|
// The search root itself is checked here (fail-closed for an agent grep on
|
|
587
1633
|
// a named subtree with no sessionId); each file the walk actually opens is
|
|
588
1634
|
// recorded per-file below, so a root-level grep that reaches into multiple
|
|
589
1635
|
// ontologies still records each one (and can poison later writes).
|
|
590
1636
|
await recordOntologyRead(sessionOntologyGate, ctx, searchRoot);
|
|
1637
|
+
const fs = await ctx.getFilesystem(a.branch as string);
|
|
1638
|
+
const gate = readGateFor(a.branch as string, ctx);
|
|
591
1639
|
const out: { path: string; line: number; text: string }[] = [];
|
|
592
1640
|
const max = typeof a.max_results === 'number' ? Math.min(a.max_results, 1000) : 200;
|
|
593
1641
|
const docs: DocGrepState = { readers, uncachedBudget: UNCACHED_DOCS_PER_GREP, skippedUncached: 0 };
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1642
|
+
// The empty root is the workspace itself — always a directory, and never
|
|
1643
|
+
// worth a stat.
|
|
1644
|
+
// A placeholder named on its own is searched as what it is to every
|
|
1645
|
+
// other tool: nothing.
|
|
1646
|
+
const kind =
|
|
1647
|
+
searchRoot === ''
|
|
1648
|
+
? 'directory'
|
|
1649
|
+
: isFolderPlaceholder(searchRoot)
|
|
1650
|
+
? 'missing'
|
|
1651
|
+
: await searchRootKind(fs, searchRoot);
|
|
1652
|
+
/** Why a single-file search found nothing, when "no matches" would be a lie. */
|
|
1653
|
+
let fileNote: string | undefined;
|
|
1654
|
+
if (kind === 'directory') {
|
|
1655
|
+
await grepWalk(
|
|
1656
|
+
fs,
|
|
1657
|
+
searchRoot,
|
|
1658
|
+
re,
|
|
1659
|
+
out,
|
|
1660
|
+
max,
|
|
1661
|
+
0,
|
|
1662
|
+
gate,
|
|
1663
|
+
(p) => recordOntologyRead(sessionOntologyGate, ctx, p),
|
|
1664
|
+
docs,
|
|
1665
|
+
);
|
|
1666
|
+
} else {
|
|
1667
|
+
// Not a directory: the permission verdict comes BEFORE every other
|
|
1668
|
+
// one, and it is `read_file`'s own gate on the same path — so grep
|
|
1669
|
+
// answers a path the caller may not read exactly as read_file does,
|
|
1670
|
+
// and can never confirm the existence of one read_file would hide.
|
|
1671
|
+
// That ordering holds even when the stat itself failed: a denied path
|
|
1672
|
+
// gets the 403, never the filesystem's complaint about it.
|
|
1673
|
+
await assertCanRead(gate, searchRoot);
|
|
1674
|
+
if (kind === 'missing') throw notFound(searchRoot, 'Nothing to search');
|
|
1675
|
+
// A named FILE is searched directly: routing it through the walk would
|
|
1676
|
+
// fail its readdir and answer an empty match list, which the caller
|
|
1677
|
+
// cannot tell from "the pattern is not in this file".
|
|
1678
|
+
const outcome = await orNotFound(
|
|
1679
|
+
searchRoot,
|
|
1680
|
+
() => grepOneFile(fs, searchRoot, re, out, max, docs),
|
|
1681
|
+
'Nothing to search',
|
|
1682
|
+
);
|
|
1683
|
+
if (outcome === 'no-text') {
|
|
1684
|
+
fileNote =
|
|
1685
|
+
`"${displayPath(searchRoot)}" has no searchable text — it is an image, binary content, or a document ` +
|
|
1686
|
+
'whose text could not be extracted. There were no matches because there was nothing to search, not ' +
|
|
1687
|
+
'because the pattern is absent.';
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
const note =
|
|
1691
|
+
fileNote ??
|
|
1692
|
+
(docs.skippedUncached > 0
|
|
1693
|
+
? `${docs.skippedUncached} document(s) (office/PDF/email files) were not searched: their text was not yet ` +
|
|
1694
|
+
`extracted and this call's extraction budget (${UNCACHED_DOCS_PER_GREP}) ran out. Re-run the ` +
|
|
1695
|
+
'same grep to extract and search the next batch.'
|
|
1696
|
+
: undefined);
|
|
605
1697
|
return {
|
|
606
1698
|
matches: out,
|
|
607
1699
|
truncated: out.length >= max,
|
|
608
|
-
...(
|
|
609
|
-
? {
|
|
610
|
-
note:
|
|
611
|
-
`${docs.skippedUncached} document(s) (office/PDF/email files) were not searched: their text was not yet ` +
|
|
612
|
-
`extracted and this call's extraction budget (${UNCACHED_DOCS_PER_GREP}) ran out. Re-run the ` +
|
|
613
|
-
'same grep to extract and search the next batch.',
|
|
614
|
-
}
|
|
615
|
-
: {}),
|
|
1700
|
+
...(note !== undefined ? { note } : {}),
|
|
616
1701
|
};
|
|
617
1702
|
},
|
|
618
1703
|
});
|
|
@@ -621,7 +1706,10 @@ export function registerWorkspaceTools(
|
|
|
621
1706
|
mount({
|
|
622
1707
|
name: 'write_file',
|
|
623
1708
|
description:
|
|
624
|
-
'Write
|
|
1709
|
+
'Write a workspace TEXT file. The change is committed + pushed as you. Returns `{ path, bytes, outcome }`, where `outcome` is ' +
|
|
1710
|
+
'`created`, `replaced` or `updated`.' +
|
|
1711
|
+
WRITE_MODE_NOTE +
|
|
1712
|
+
IMAGE_CONVENTION_NOTE +
|
|
625
1713
|
ONTOLOGY_BOUNDARY_NOTE,
|
|
626
1714
|
inputs: {
|
|
627
1715
|
type: 'object',
|
|
@@ -629,6 +1717,7 @@ export function registerWorkspaceTools(
|
|
|
629
1717
|
branch: BRANCH_INPUT,
|
|
630
1718
|
path: wsPath(kbDirName, 'Path'),
|
|
631
1719
|
content: str('Full file content.'),
|
|
1720
|
+
mode: WRITE_MODE_INPUT,
|
|
632
1721
|
sessionId: SESSION_ID_INPUT,
|
|
633
1722
|
},
|
|
634
1723
|
required: ['branch', 'path', 'content'],
|
|
@@ -636,10 +1725,20 @@ export function registerWorkspaceTools(
|
|
|
636
1725
|
},
|
|
637
1726
|
outputs: {
|
|
638
1727
|
type: 'object',
|
|
639
|
-
properties: {
|
|
640
|
-
|
|
1728
|
+
properties: {
|
|
1729
|
+
path: str('The path written (echoes the input).'),
|
|
1730
|
+
bytes: int('Number of bytes written.'),
|
|
1731
|
+
outcome: {
|
|
1732
|
+
type: 'string',
|
|
1733
|
+
enum: ['created', 'replaced', 'updated'],
|
|
1734
|
+
description: 'What the write did: `created` (nothing was there), `replaced` (`mode: overwrite` over an existing file), `updated` (`mode: update`).',
|
|
1735
|
+
},
|
|
1736
|
+
warnings: SAVE_WARNINGS_OUTPUT,
|
|
1737
|
+
},
|
|
1738
|
+
required: ['path', 'bytes', 'outcome'],
|
|
641
1739
|
},
|
|
642
1740
|
write: true,
|
|
1741
|
+
proposable: true,
|
|
643
1742
|
handler: async (a, ctx: ToolContext) => {
|
|
644
1743
|
assertNotDocumentEdit(readers, a.path as string);
|
|
645
1744
|
// NB: this is a no-op for chat + `ontology_ingest` — it only bites when a
|
|
@@ -648,10 +1747,42 @@ export function registerWorkspaceTools(
|
|
|
648
1747
|
// through (see `assertPathWritable`), so it does not limit other agents.
|
|
649
1748
|
writePolicy.assertPathWritable(ctx.sessionId, a.path as string);
|
|
650
1749
|
await assertOntologyWriteAllowed(sessionOntologyGate, ctx, a.path as string);
|
|
1750
|
+
const mode = modeOf(a);
|
|
651
1751
|
const fs = await ctx.getFilesystem(a.branch as string);
|
|
652
|
-
await assertNotBinaryOverwrite(readers,
|
|
653
|
-
|
|
654
|
-
|
|
1752
|
+
await assertNotBinaryOverwrite(readers,a.path as string, fs);
|
|
1753
|
+
// The mode is judged AFTER the content gates, so a file the tools may
|
|
1754
|
+
// not write as text is still answered with the capability refusal that
|
|
1755
|
+
// names the tool to use instead — not with "it already exists".
|
|
1756
|
+
const decide = async (): Promise<WriteOutcome> =>
|
|
1757
|
+
decideWrite(mode, a.path as string, (await kindOf(fs, a.path as string)) !== null);
|
|
1758
|
+
// Judged twice, on purpose. This first verdict is the cheap one, taken
|
|
1759
|
+
// before any lock so an ordinary refusal never contends for one — but it
|
|
1760
|
+
// is a verdict about a path anyone may still change. The one the answer
|
|
1761
|
+
// carries is taken again inside `writeFile`, with the path's lock HELD
|
|
1762
|
+
// (`write: true` guarantees the locking filesystem, as the batch cast
|
|
1763
|
+
// below does): only there can `create` be sure it is not about to
|
|
1764
|
+
// replace a file a human editor saved a moment ago, and `update` sure it
|
|
1765
|
+
// is not recreating one somebody just deleted. A filesystem without the
|
|
1766
|
+
// hook runs no second verdict, so the preflight one stands.
|
|
1767
|
+
const preflight = await decide();
|
|
1768
|
+
let locked: WriteOutcome | null = null;
|
|
1769
|
+
const locking = fs as unknown as {
|
|
1770
|
+
writeFile(
|
|
1771
|
+
path: string,
|
|
1772
|
+
content: string,
|
|
1773
|
+
options: undefined,
|
|
1774
|
+
check: () => Promise<void>,
|
|
1775
|
+
): Promise<void>;
|
|
1776
|
+
};
|
|
1777
|
+
await locking.writeFile(a.path as string, a.content as string, undefined, async () => {
|
|
1778
|
+
locked = await decide();
|
|
1779
|
+
});
|
|
1780
|
+
return {
|
|
1781
|
+
path: a.path,
|
|
1782
|
+
bytes: Buffer.byteLength(a.content as string, 'utf8'),
|
|
1783
|
+
outcome: (locked ?? preflight) as WriteOutcome,
|
|
1784
|
+
...(await saveWarnings(ctx, a.path as string, a.content as string)),
|
|
1785
|
+
};
|
|
655
1786
|
},
|
|
656
1787
|
});
|
|
657
1788
|
|
|
@@ -659,11 +1790,15 @@ export function registerWorkspaceTools(
|
|
|
659
1790
|
name: 'write_files',
|
|
660
1791
|
description:
|
|
661
1792
|
'Batch-write many files in ONE commit — far faster than calling write_file once per file when ' +
|
|
662
|
-
'creating many files at once (e.g. seeding a knowledge base). Each entry is `{ path, content }
|
|
663
|
-
'are
|
|
664
|
-
'calls. All files must be in the SAME ontology (the boundary below applies to the batch).
|
|
665
|
-
'
|
|
666
|
-
'
|
|
1793
|
+
'creating many files at once (e.g. seeding a knowledge base). Each entry is `{ path, content }`, and the files it ' +
|
|
1794
|
+
'writes are committed + pushed together as you. Prefer this over many write_file ' +
|
|
1795
|
+
'calls. All files must be in the SAME ontology (the boundary below applies to the batch). Text files only. ' +
|
|
1796
|
+
'Returns `{ count, files }`: one entry per REQUESTED path, in the order you gave them, each `{ path, outcome }` — ' +
|
|
1797
|
+
'`created` / `replaced` / `updated` for a path it wrote, or `refused` with `error` (the code) and `message` (why) for a ' +
|
|
1798
|
+
'path it could not. `count` is how many were written. A path it refuses — the mode said no, or the file is not text — ' +
|
|
1799
|
+
'does not stop the others; read `files` to see what landed.' +
|
|
1800
|
+
WRITE_MODE_NOTE +
|
|
1801
|
+
IMAGE_CONVENTION_NOTE +
|
|
667
1802
|
ONTOLOGY_BOUNDARY_NOTE,
|
|
668
1803
|
inputs: {
|
|
669
1804
|
type: 'object',
|
|
@@ -671,7 +1806,7 @@ export function registerWorkspaceTools(
|
|
|
671
1806
|
branch: BRANCH_INPUT,
|
|
672
1807
|
files: {
|
|
673
1808
|
type: 'array',
|
|
674
|
-
description: 'Files to write; each
|
|
1809
|
+
description: 'Files to write; `mode` decides what each one may do at its path.',
|
|
675
1810
|
items: {
|
|
676
1811
|
type: 'object',
|
|
677
1812
|
properties: { path: wsPath(kbDirName, 'Path'), content: str('Full file content.') },
|
|
@@ -679,6 +1814,7 @@ export function registerWorkspaceTools(
|
|
|
679
1814
|
additionalProperties: false,
|
|
680
1815
|
},
|
|
681
1816
|
},
|
|
1817
|
+
mode: WRITE_MODE_INPUT,
|
|
682
1818
|
sessionId: SESSION_ID_INPUT,
|
|
683
1819
|
},
|
|
684
1820
|
required: ['branch', 'files'],
|
|
@@ -686,37 +1822,140 @@ export function registerWorkspaceTools(
|
|
|
686
1822
|
},
|
|
687
1823
|
outputs: {
|
|
688
1824
|
type: 'object',
|
|
689
|
-
properties: {
|
|
690
|
-
|
|
1825
|
+
properties: {
|
|
1826
|
+
count: int('Number of files written — the entries in `files` whose `outcome` is not `refused`.'),
|
|
1827
|
+
files: {
|
|
1828
|
+
type: 'array',
|
|
1829
|
+
description: 'One entry per REQUESTED path, in input order.',
|
|
1830
|
+
items: {
|
|
1831
|
+
type: 'object',
|
|
1832
|
+
properties: {
|
|
1833
|
+
path: str('The requested path (echoes the input).'),
|
|
1834
|
+
outcome: {
|
|
1835
|
+
type: 'string',
|
|
1836
|
+
enum: ['created', 'replaced', 'updated', 'refused'],
|
|
1837
|
+
description: 'What happened at this path. `refused` means nothing was written there and the file is untouched.',
|
|
1838
|
+
},
|
|
1839
|
+
error: str('Present when `outcome` is `refused`: the refusal code — `exists`, `missing` or `binary_not_writable`.'),
|
|
1840
|
+
message: str('Present when `outcome` is `refused`: the full refusal, the same one write_file would have given.'),
|
|
1841
|
+
},
|
|
1842
|
+
required: ['path', 'outcome'],
|
|
1843
|
+
},
|
|
1844
|
+
},
|
|
1845
|
+
warnings: BATCH_SAVE_WARNINGS_OUTPUT,
|
|
1846
|
+
},
|
|
1847
|
+
required: ['count', 'files'],
|
|
691
1848
|
},
|
|
692
1849
|
write: true,
|
|
1850
|
+
proposable: true,
|
|
693
1851
|
handler: async (a, ctx: ToolContext) => {
|
|
694
1852
|
const files = (a.files as Array<{ path: string; content: string }>) ?? [];
|
|
695
|
-
if (files.length === 0) return { count: 0 };
|
|
696
|
-
|
|
697
|
-
//
|
|
698
|
-
|
|
1853
|
+
if (files.length === 0) return { count: 0, files: [] };
|
|
1854
|
+
const mode = modeOf(a);
|
|
1855
|
+
// The POLICY gates still judge the whole batch: a restricted run or a
|
|
1856
|
+
// cross-ontology batch is a call that should not have been made at all,
|
|
1857
|
+
// not a per-path outcome, and the ontology gate must see every path
|
|
1858
|
+
// before anything lands. What a single FILE is (not text) or what its
|
|
1859
|
+
// path already holds (the mode) is decided per path, below.
|
|
699
1860
|
for (const f of files) writePolicy.assertPathWritable(ctx.sessionId, f.path);
|
|
700
1861
|
for (const f of files) await assertOntologyWriteAllowed(sessionOntologyGate, ctx, f.path);
|
|
1862
|
+
const fs = await ctx.getFilesystem(a.branch as string);
|
|
701
1863
|
// `write: true` guarantees a LockingFilesystem here; `writeFiles` lands the
|
|
702
|
-
//
|
|
703
|
-
const
|
|
704
|
-
writeFiles(
|
|
705
|
-
|
|
1864
|
+
// batch as one commit. Structural cast avoids a workflow-internal import.
|
|
1865
|
+
const batching = fs as unknown as {
|
|
1866
|
+
writeFiles(
|
|
1867
|
+
writes: { path: string; content: string }[],
|
|
1868
|
+
summary: string,
|
|
1869
|
+
deletes: string[],
|
|
1870
|
+
check: (
|
|
1871
|
+
pending: readonly { path: string; content: string }[],
|
|
1872
|
+
) => Promise<{ path: string; content: string }[]>,
|
|
1873
|
+
): Promise<void>;
|
|
1874
|
+
};
|
|
1875
|
+
const writes: { path: string; content: string }[] = [];
|
|
1876
|
+
const outcomes: Record<string, unknown>[] = [];
|
|
1877
|
+
/** The `files` entry for `writes[i]`, so the re-judgement can revise it. */
|
|
1878
|
+
const entryOf: Record<string, unknown>[] = [];
|
|
1879
|
+
/** Record on `entry` that this path was refused, as write_file would say it. */
|
|
1880
|
+
const refuse = (entry: Record<string, unknown>, err: unknown): void => {
|
|
1881
|
+
if (!(err instanceof ToolError)) throw err;
|
|
1882
|
+
const details = (err.details ?? {}) as { code?: string; kind?: string };
|
|
1883
|
+
entry.outcome = 'refused';
|
|
1884
|
+
entry.error = details.code ?? details.kind ?? 'refused';
|
|
1885
|
+
entry.message = err.message;
|
|
1886
|
+
};
|
|
1887
|
+
for (const f of files) {
|
|
1888
|
+
const entry: Record<string, unknown> = { path: f.path };
|
|
1889
|
+
outcomes.push(entry);
|
|
1890
|
+
try {
|
|
1891
|
+
assertNotDocumentEdit(readers, f.path);
|
|
1892
|
+
await assertNotBinaryOverwrite(readers, f.path, fs);
|
|
1893
|
+
// An earlier entry in this same batch counts as existing: two `create`
|
|
1894
|
+
// entries for one path are a mistake the commit would otherwise hide.
|
|
1895
|
+
const exists = writes.some((w) => w.path === f.path) || (await kindOf(fs, f.path)) !== null;
|
|
1896
|
+
entry.outcome = decideWrite(mode, f.path, exists);
|
|
1897
|
+
writes.push({ path: f.path, content: f.content });
|
|
1898
|
+
entryOf.push(entry);
|
|
1899
|
+
} catch (err) {
|
|
1900
|
+
refuse(entry, err);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
// The same mode gate again, run by `writeFiles` once EVERY path's lock is
|
|
1904
|
+
// held — the verdict the answer carries, for the reason write_file states
|
|
1905
|
+
// above. `pending` is this batch's writes in the order they were handed
|
|
1906
|
+
// over, so `pending[i]` is `writes[i]` and `entryOf[i]` is its `files`
|
|
1907
|
+
// entry. A path whose verdict changed under the lock is dropped from the
|
|
1908
|
+
// batch and reported refused, leaving the rest of the batch to land.
|
|
1909
|
+
const recheck = async (
|
|
1910
|
+
pending: readonly { path: string; content: string }[],
|
|
1911
|
+
): Promise<{ path: string; content: string }[]> => {
|
|
1912
|
+
const kept: { path: string; content: string }[] = [];
|
|
1913
|
+
for (let i = 0; i < pending.length; i++) {
|
|
1914
|
+
const entry = entryOf[i];
|
|
1915
|
+
try {
|
|
1916
|
+
const exists =
|
|
1917
|
+
kept.some((k) => k.path === pending[i].path) || (await kindOf(fs, pending[i].path)) !== null;
|
|
1918
|
+
entry.outcome = decideWrite(mode, pending[i].path, exists);
|
|
1919
|
+
kept.push(pending[i]);
|
|
1920
|
+
} catch (err) {
|
|
1921
|
+
refuse(entry, err);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
return kept;
|
|
1925
|
+
};
|
|
1926
|
+
if (writes.length > 0) {
|
|
1927
|
+
await batching.writeFiles(writes, `Write ${writes.length} file(s)`, [], recheck);
|
|
1928
|
+
}
|
|
1929
|
+
// Only the content that actually landed is checked: a refused entry wrote
|
|
1930
|
+
// nothing, and a path the batch names twice (which `overwrite` allows) is
|
|
1931
|
+
// judged by its LAST landed entry — the earlier one is not in the branch,
|
|
1932
|
+
// so warning about it would describe text nobody can find. `outcomes[i]`
|
|
1933
|
+
// is the entry for `files[i]`. One catalog read for the whole batch.
|
|
1934
|
+
const landed: number[] = [];
|
|
1935
|
+
for (let i = 0; i < files.length; i++) {
|
|
1936
|
+
if (outcomes[i].outcome === 'refused') continue;
|
|
1937
|
+
if (files.some((f, j) => j > i && f.path === files[i].path && outcomes[j].outcome !== 'refused')) continue;
|
|
1938
|
+
landed.push(i);
|
|
1939
|
+
}
|
|
1940
|
+
const warnings: (SkillSaveWarning & { path: string })[] = [];
|
|
1941
|
+
if (skillSaveCheck && landed.length > 0) {
|
|
1942
|
+
const perFile = await skillSaveCheck.checkSaves(ctx.user.email, landed.map((i) => files[i]));
|
|
1943
|
+
landed.forEach((i, k) => {
|
|
1944
|
+
warnings.push(...(perFile[k] ?? []).map((w) => ({ path: files[i].path, ...w })));
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
return {
|
|
1948
|
+
count: outcomes.filter((o) => o.outcome !== 'refused').length,
|
|
1949
|
+
files: outcomes,
|
|
1950
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
706
1951
|
};
|
|
707
|
-
for (const f of files) await assertNotBinaryOverwrite(readers, f.path, fs);
|
|
708
|
-
await fs.writeFiles(
|
|
709
|
-
files.map((f) => ({ path: f.path, content: f.content })),
|
|
710
|
-
`Write ${files.length} file(s)`,
|
|
711
|
-
);
|
|
712
|
-
return { count: files.length };
|
|
713
1952
|
},
|
|
714
1953
|
});
|
|
715
1954
|
|
|
716
1955
|
mount({
|
|
717
1956
|
name: 'edit_file',
|
|
718
1957
|
description:
|
|
719
|
-
'Replace an exact string in a workspace file. `old_string` must appear exactly once unless `replace_all`. Committed + pushed as you.
|
|
1958
|
+
'Replace an exact string in a workspace TEXT file. `old_string` must appear exactly once unless `replace_all`. Committed + pushed as you.' +
|
|
720
1959
|
ONTOLOGY_BOUNDARY_NOTE,
|
|
721
1960
|
inputs: {
|
|
722
1961
|
type: 'object',
|
|
@@ -733,10 +1972,11 @@ export function registerWorkspaceTools(
|
|
|
733
1972
|
},
|
|
734
1973
|
outputs: {
|
|
735
1974
|
type: 'object',
|
|
736
|
-
properties: { path: str('The path edited (echoes the input).'), replaced: int('Number of occurrences replaced.') },
|
|
1975
|
+
properties: { path: str('The path edited (echoes the input).'), replaced: int('Number of occurrences replaced.'), warnings: SAVE_WARNINGS_OUTPUT },
|
|
737
1976
|
required: ['path', 'replaced'],
|
|
738
1977
|
},
|
|
739
1978
|
write: true,
|
|
1979
|
+
proposable: true,
|
|
740
1980
|
handler: async (a, ctx: ToolContext) => {
|
|
741
1981
|
assertNotDocumentEdit(readers, a.path as string);
|
|
742
1982
|
writePolicy.assertPathWritable(ctx.sessionId, a.path as string);
|
|
@@ -747,8 +1987,10 @@ export function registerWorkspaceTools(
|
|
|
747
1987
|
const newStr = a.new_string as string;
|
|
748
1988
|
// The overwrite gate already read the file when its reader asked the
|
|
749
1989
|
// binary question — reuse those bytes instead of reading twice.
|
|
750
|
-
const
|
|
751
|
-
|
|
1990
|
+
const content = await orNotFound(path, async () => {
|
|
1991
|
+
const existing = await assertNotBinaryOverwrite(readers, path, fs);
|
|
1992
|
+
return asText(existing ?? (await fs.readFile(path)));
|
|
1993
|
+
});
|
|
752
1994
|
const count = oldStr ? content.split(oldStr).length - 1 : 0;
|
|
753
1995
|
if (count === 0) throw new ToolError('old_string not found in the file.', 400);
|
|
754
1996
|
if (count > 1 && a.replace_all !== true) {
|
|
@@ -756,18 +1998,21 @@ export function registerWorkspaceTools(
|
|
|
756
1998
|
}
|
|
757
1999
|
const updated = a.replace_all === true ? content.split(oldStr).join(newStr) : content.replace(oldStr, newStr);
|
|
758
2000
|
await fs.writeFile(path, updated);
|
|
759
|
-
return { path, replaced: a.replace_all === true ? count : 1 };
|
|
2001
|
+
return { path, replaced: a.replace_all === true ? count : 1, ...(await saveWarnings(ctx, path, updated)) };
|
|
760
2002
|
},
|
|
761
2003
|
});
|
|
762
2004
|
|
|
763
2005
|
mount({
|
|
764
2006
|
name: 'delete_file',
|
|
765
|
-
description:
|
|
2007
|
+
description: () =>
|
|
2008
|
+
'Delete ONE workspace file (a symbolic link is refused: links are never followed or removed). Committed + pushed as you. Its folder stays, even when this was its last file. Files only: a folder is refused with a pointer to `delete_folder`. ' +
|
|
2009
|
+
`A platform file (\`access.md\` or \`.bevelignore\` in any folder, \`roles.yaml\` or \`${AGENTS_FILE}\` at the repository root) and git metadata are refused.` +
|
|
2010
|
+
ONTOLOGY_BOUNDARY_NOTE,
|
|
766
2011
|
inputs: {
|
|
767
2012
|
type: 'object',
|
|
768
2013
|
properties: {
|
|
769
2014
|
branch: BRANCH_INPUT,
|
|
770
|
-
path: wsPath(kbDirName, 'Path to the file'
|
|
2015
|
+
path: wsPath(kbDirName, 'Path to the file'),
|
|
771
2016
|
sessionId: SESSION_ID_INPUT,
|
|
772
2017
|
},
|
|
773
2018
|
required: ['branch', 'path'],
|
|
@@ -779,21 +2024,179 @@ export function registerWorkspaceTools(
|
|
|
779
2024
|
required: ['path', 'deleted'],
|
|
780
2025
|
},
|
|
781
2026
|
write: true,
|
|
2027
|
+
proposable: true,
|
|
782
2028
|
handler: async (a, ctx: ToolContext) => {
|
|
783
2029
|
// A delete propagates no cross-ontology information (it removes a node, it
|
|
784
2030
|
// doesn't carry bytes from elsewhere), so it is NOT ontology-write-gated — it
|
|
785
2031
|
// only records the ontology it touched, like a read. The extension policy
|
|
786
2032
|
// DOES apply though: a dashboard-only run must not delete graph `.md` nodes.
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
2033
|
+
const path = a.path as string;
|
|
2034
|
+
const branch = a.branch as string;
|
|
2035
|
+
writePolicy.assertPathWritable(ctx.sessionId, path);
|
|
2036
|
+
await recordOntologyRead(sessionOntologyGate, ctx, path);
|
|
2037
|
+
const fs = await ctx.getFilesystem(branch);
|
|
2038
|
+
assertPlainPath(path);
|
|
2039
|
+
const root = await workspaceRoot(branch, ctx);
|
|
2040
|
+
if (await isSymlinkAt(root, path)) throw new ToolError(linkRefusal(path), 400);
|
|
2041
|
+
if ((await kindOf(fs, path)) === 'folder') {
|
|
2042
|
+
throw new ToolError(`"${path}" is a folder, not a file — use delete_folder to delete it and the files under it.`, 400);
|
|
2043
|
+
}
|
|
2044
|
+
const onDisk = await onDiskSpelling(root, path);
|
|
2045
|
+
if (isGitMetadata(onDisk)) throw new ToolError(managedReason(onDisk, 'file')!, 400);
|
|
2046
|
+
if (managedReason(onDisk, 'file') !== undefined) {
|
|
2047
|
+
throw new ToolError(`${onDisk.slice(onDisk.lastIndexOf('/') + 1)} is a platform file and cannot be deleted through the agent tools.`, 400);
|
|
2048
|
+
}
|
|
2049
|
+
await assertNoSymlinkOnPath(root, path, true);
|
|
2050
|
+
if ((await writeBlocked(branch, ctx, [path])).length > 0) throw await writeRefusal(branch, path);
|
|
2051
|
+
await orNotFound(path, () => fs.deleteFile(path), 'Nothing to delete');
|
|
2052
|
+
// Deleting content is not deleting structure: an emptied folder stays.
|
|
2053
|
+
await keepFolderOf(fs, ctx, branch, path, kbDirName);
|
|
2054
|
+
return { path, deleted: true };
|
|
2055
|
+
},
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
mount({
|
|
2059
|
+
name: 'delete_folder',
|
|
2060
|
+
description:
|
|
2061
|
+
'Delete a workspace FOLDER and every file under it, at any depth; the whole folder lands as ONE committed + pushed change as you — all of it or none of it — then the empty folder is removed. This is the one way a folder goes away: the folder that held it stays, even if this was all it had, and a folder holding nothing but its empty-folder placeholder counts as empty. ' +
|
|
2062
|
+
'Preflight first: `dryRun: true` changes nothing and answers `{ path, kind: "folder", descendants, files, filesTruncated, allowed, reason? }` — `descendants` is the file count, `files` names up to 100 of them. ' +
|
|
2063
|
+
'A non-empty folder is deleted only with `confirm: true`; without it the call deletes nothing and returns the same impact with `confirmationRequired: true`. Do NOT set `confirm: true` on your first call — dry-run, check the impact, then confirm. ' +
|
|
2064
|
+
'Refused (in a dry run as `allowed: false` with the `reason`): a platform folder (the repository root or a reserved root folder such as `KnowledgeBase/`), git metadata, a folder holding a symbolic link (links are never removed), and a folder holding any file you may not write. A path that is a file is refused with a pointer to `delete_file`, and a path through a symbolic link is refused (links are never followed). ' +
|
|
2065
|
+
'The folder\'s own platform files (`access.md`, `.bevelignore`) go with it in that same one change, so its files are never left ungoverned part-way; you must be able to write those platform files too.' +
|
|
2066
|
+
ONTOLOGY_BOUNDARY_NOTE,
|
|
2067
|
+
inputs: {
|
|
2068
|
+
type: 'object',
|
|
2069
|
+
properties: {
|
|
2070
|
+
branch: BRANCH_INPUT,
|
|
2071
|
+
path: wsPath(kbDirName, 'Folder to delete'),
|
|
2072
|
+
dryRun: { type: 'boolean', description: 'Answer with the impact and change nothing.' },
|
|
2073
|
+
confirm: { type: 'boolean', description: 'Required to delete a non-empty folder. Set it only after a dry run.' },
|
|
2074
|
+
sessionId: SESSION_ID_INPUT,
|
|
2075
|
+
},
|
|
2076
|
+
required: ['branch', 'path'],
|
|
2077
|
+
additionalProperties: false,
|
|
2078
|
+
},
|
|
2079
|
+
outputs: {
|
|
2080
|
+
type: 'object',
|
|
2081
|
+
properties: {
|
|
2082
|
+
path: str('The folder (echoes the input).'),
|
|
2083
|
+
kind: str('Always `folder`.'),
|
|
2084
|
+
descendants: int('Files under the folder at any depth.'),
|
|
2085
|
+
files: { type: 'array', items: { type: 'string' }, description: 'Up to 100 of those files.' },
|
|
2086
|
+
filesTruncated: { type: 'boolean', description: 'True when `files` does not name every file.' },
|
|
2087
|
+
allowed: { type: 'boolean', description: 'Whether the delete may run.' },
|
|
2088
|
+
reason: str('Why it may not, when `allowed` is false.'),
|
|
2089
|
+
dryRun: { type: 'boolean', description: 'True on a dry run.' },
|
|
2090
|
+
confirmationRequired: { type: 'boolean', description: 'True when the call stopped for want of `confirm: true`.' },
|
|
2091
|
+
message: str('One sentence on what happened (or did not).'),
|
|
2092
|
+
deleted: { type: 'boolean', description: 'True once the folder is gone.' },
|
|
2093
|
+
},
|
|
2094
|
+
required: ['path', 'kind', 'descendants', 'allowed'],
|
|
2095
|
+
},
|
|
2096
|
+
write: true,
|
|
2097
|
+
proposable: true,
|
|
2098
|
+
handler: async (a, ctx: ToolContext) => {
|
|
2099
|
+
const path = (a.path as string).replace(/\/+$/, '');
|
|
2100
|
+
const branch = a.branch as string;
|
|
2101
|
+
// The normaliser has already placed the path inside the repository; this
|
|
2102
|
+
// is the check that it really is in there before a folder is walked.
|
|
2103
|
+
assertInsideRepo(path, kbDirName);
|
|
2104
|
+
await recordOntologyRead(sessionOntologyGate, ctx, path);
|
|
2105
|
+
const fs = await ctx.getFilesystem(branch);
|
|
2106
|
+
const kind = await kindOf(fs, path);
|
|
2107
|
+
if (kind === null) throw new ToolError(`"${path}" does not exist.`, 404);
|
|
2108
|
+
if (kind === 'file') {
|
|
2109
|
+
throw new ToolError(`"${path}" is a file, not a folder — use delete_file to delete it.`, 400);
|
|
2110
|
+
}
|
|
2111
|
+
const root = await workspaceRoot(branch, ctx);
|
|
2112
|
+
await assertNoSymlinkOnPath(root, path);
|
|
2113
|
+
/**
|
|
2114
|
+
* Everything the delete is judged on. `files` is every file that goes,
|
|
2115
|
+
* the empty-folder placeholders included; `content` leaves those out,
|
|
2116
|
+
* because a placeholder is never content — a folder holding nothing but
|
|
2117
|
+
* its placeholder is EMPTY, needs no confirmation, and reports no files.
|
|
2118
|
+
*/
|
|
2119
|
+
const judge = async () => {
|
|
2120
|
+
const { files, links } = await filesUnder(fs, path);
|
|
2121
|
+
const content = files.filter((f) => !isFolderPlaceholder(f));
|
|
2122
|
+
// A restricted run is judged on what it would actually delete: the files.
|
|
2123
|
+
for (const f of files) writePolicy.assertPathWritable(ctx.sessionId, f);
|
|
2124
|
+
const managed = managedReason(await onDiskSpelling(root, path), 'folder');
|
|
2125
|
+
const blocked = managed !== undefined ? [] : await writeBlocked(branch, ctx, [path, ...files]);
|
|
2126
|
+
const linked = managed === undefined && links.length > 0 ? linksRefusal(path, links) : undefined;
|
|
2127
|
+
const reason = managed ?? linked ?? (blocked.length > 0
|
|
2128
|
+
? `You may not write ${blocked.length === 1 ? `"${blocked[0]}"` : `${blocked.length} of the paths, e.g. "${blocked[0]}"`}, so the folder cannot be deleted.`
|
|
2129
|
+
: undefined);
|
|
2130
|
+
const impact = {
|
|
2131
|
+
path,
|
|
2132
|
+
kind: 'folder' as const,
|
|
2133
|
+
descendants: content.length,
|
|
2134
|
+
files: content.slice(0, LISTED_FILES_CAP),
|
|
2135
|
+
filesTruncated: content.length > LISTED_FILES_CAP,
|
|
2136
|
+
allowed: reason === undefined,
|
|
2137
|
+
...(reason !== undefined ? { reason } : {}),
|
|
2138
|
+
};
|
|
2139
|
+
return { files, content, managed, linked, blocked, impact };
|
|
2140
|
+
};
|
|
2141
|
+
if (a.dryRun === true) return { ...(await judge()).impact, dryRun: true };
|
|
2142
|
+
|
|
2143
|
+
// The real delete runs in the folder's TURN, judged again inside it: an
|
|
2144
|
+
// emptied folder being kept (`keepFolderOf`, which takes the turn of
|
|
2145
|
+
// the folder it keeps) can never write its placeholder into this folder
|
|
2146
|
+
// after the files below were enumerated, and so bring it back.
|
|
2147
|
+
const outcome = await ctx.workspaceService.withFolderTurn(workspaceIdForBranch(branch), path, async () => {
|
|
2148
|
+
const { files, content, managed, linked, blocked, impact } = await judge();
|
|
2149
|
+
if (managed !== undefined) throw new ToolError(managed, 400);
|
|
2150
|
+
if (linked !== undefined) throw new ToolError(linked, 400);
|
|
2151
|
+
if (blocked.length > 0) throw await writeRefusal(branch, blocked[0], blocked[0] === path ? 'dir' : 'file');
|
|
2152
|
+
if (content.length > 0 && a.confirm !== true) {
|
|
2153
|
+
return {
|
|
2154
|
+
...impact,
|
|
2155
|
+
confirmationRequired: true,
|
|
2156
|
+
deleted: false,
|
|
2157
|
+
message: `Nothing was deleted: "${path}" holds ${content.length} ${content.length === 1 ? 'file' : 'files'}, so deleting it requires confirm: true.`,
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
// ONE commit for the whole folder. `write: true` guarantees a
|
|
2161
|
+
// LockingFilesystem here, and its `writeFiles` takes every path's lock
|
|
2162
|
+
// BEFORE touching disk, deletes inside those locks and commits the set
|
|
2163
|
+
// as a single change (fail-closed: a refusal commits nothing). A folder
|
|
2164
|
+
// therefore never half-disappears, and its own `access.md` needs no
|
|
2165
|
+
// ordering trick to keep the rest governed on the way — nothing lands
|
|
2166
|
+
// until all of it does. The placeholders go too: this is the one
|
|
2167
|
+
// operation that removes a folder. Structural cast, as `write_files`
|
|
2168
|
+
// does, to avoid importing the workflow-internal class here.
|
|
2169
|
+
if (files.length > 0) {
|
|
2170
|
+
const batch = fs as unknown as {
|
|
2171
|
+
writeFiles(
|
|
2172
|
+
writes: { path: string; content: string }[],
|
|
2173
|
+
summary: string,
|
|
2174
|
+
deletes: string[],
|
|
2175
|
+
): Promise<unknown>;
|
|
2176
|
+
};
|
|
2177
|
+
await batch.writeFiles([], `Delete ${path} and its ${content.length} file(s)`, files);
|
|
2178
|
+
}
|
|
2179
|
+
// Git tracks no folders: once the files are gone, the shells left on
|
|
2180
|
+
// disk are swept so the folder stops appearing in listings. Only empty
|
|
2181
|
+
// folders go, so a file a concurrent writer just dropped in survives.
|
|
2182
|
+
await removeEmptyDirs(join(root, path));
|
|
2183
|
+
return {
|
|
2184
|
+
...impact,
|
|
2185
|
+
deleted: true,
|
|
2186
|
+
message: `Deleted "${path}" and its ${content.length} ${content.length === 1 ? 'file' : 'files'}.`,
|
|
2187
|
+
};
|
|
2188
|
+
});
|
|
2189
|
+
// The folder that HELD this one is not being deleted: if this was all it
|
|
2190
|
+
// had, it stays, with its placeholder. Outside the turn above — the
|
|
2191
|
+
// parent's turn overlaps it, and a folder turn is never nested.
|
|
2192
|
+
if (outcome.deleted) await keepFolderOf(fs, ctx, branch, path, kbDirName);
|
|
2193
|
+
return outcome;
|
|
791
2194
|
},
|
|
792
2195
|
});
|
|
793
2196
|
|
|
794
2197
|
mount({
|
|
795
2198
|
name: 'mkdir',
|
|
796
|
-
description: 'Create a directory (recursive).
|
|
2199
|
+
description: 'Create a directory (recursive). It lists as an empty folder and persists in git until it is deleted explicitly.' + ONTOLOGY_BOUNDARY_NOTE,
|
|
797
2200
|
inputs: {
|
|
798
2201
|
type: 'object',
|
|
799
2202
|
properties: {
|
|
@@ -810,6 +2213,7 @@ export function registerWorkspaceTools(
|
|
|
810
2213
|
required: ['path', 'created'],
|
|
811
2214
|
},
|
|
812
2215
|
write: true,
|
|
2216
|
+
proposable: true,
|
|
813
2217
|
handler: async (a, ctx: ToolContext) => {
|
|
814
2218
|
writePolicy.assertPathWritable(ctx.sessionId, a.path as string);
|
|
815
2219
|
await assertOntologyWriteAllowed(sessionOntologyGate, ctx, a.path as string);
|
|
@@ -820,13 +2224,20 @@ export function registerWorkspaceTools(
|
|
|
820
2224
|
|
|
821
2225
|
mount({
|
|
822
2226
|
name: 'move_file',
|
|
823
|
-
description:
|
|
2227
|
+
description: () =>
|
|
2228
|
+
'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. Lands as a delete + create, committed + pushed as you. ' +
|
|
2229
|
+
`Rules: the destination must not exist — a move never overwrites a file or merges into a folder; a platform file (\`access.md\` or \`.bevelignore\` in any folder, \`roles.yaml\` or \`${AGENTS_FILE}\` at the repository root) is refused with "<name> is a platform file and stays in its folder." — a folder that moves takes its own platform files along, still in their folder; a platform folder (the repository root or a reserved root folder such as \`KnowledgeBase/\`) and git metadata are refused; a move cannot create a platform file or folder at \`dest\` either (renaming a note to \`access.md\` is refused); a path through a symbolic link is refused, since links are never followed; on a protected branch you must be able to write both ends — for a folder, every file under it at its old and its new path. ` +
|
|
2230
|
+
'Access follows the destination folder. Preflight first: `dryRun: true` changes nothing and answers `{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }` — `access` is your own `{ read, write, download, owner }` at the source and at the destination. ' +
|
|
2231
|
+
'A move whose `accessChanges` is true runs only with `confirm: true`; without it the call moves nothing and returns the same impact with `confirmationRequired: true`. Do NOT set `confirm: true` on your first call — dry-run, check the impact, then confirm.' +
|
|
2232
|
+
ONTOLOGY_BOUNDARY_NOTE,
|
|
824
2233
|
inputs: {
|
|
825
2234
|
type: 'object',
|
|
826
2235
|
properties: {
|
|
827
2236
|
branch: BRANCH_INPUT,
|
|
828
|
-
src: wsPath(kbDirName, 'Source path
|
|
829
|
-
dest: wsPath(kbDirName, 'Destination path'),
|
|
2237
|
+
src: wsPath(kbDirName, 'Source path (file or folder)'),
|
|
2238
|
+
dest: wsPath(kbDirName, 'Destination path — the full new path; must not exist yet'),
|
|
2239
|
+
dryRun: { type: 'boolean', description: 'Answer with the impact and change nothing.' },
|
|
2240
|
+
confirm: { type: 'boolean', description: 'Required when the move changes your access. Set it only after a dry run.' },
|
|
830
2241
|
sessionId: SESSION_ID_INPUT,
|
|
831
2242
|
},
|
|
832
2243
|
required: ['branch', 'src', 'dest'],
|
|
@@ -834,33 +2245,155 @@ export function registerWorkspaceTools(
|
|
|
834
2245
|
},
|
|
835
2246
|
outputs: {
|
|
836
2247
|
type: 'object',
|
|
837
|
-
properties: {
|
|
2248
|
+
properties: {
|
|
2249
|
+
src: str('Source path (echoes the input).'),
|
|
2250
|
+
dest: str('Destination path (echoes the input).'),
|
|
2251
|
+
kind: str('`file` or `folder`.'),
|
|
2252
|
+
descendants: int('Files that move: 1 for a file, the file count under a folder.'),
|
|
2253
|
+
access: { type: 'object', description: 'Your `{ read, write, download, owner }` at the source (`before`) and destination (`after`).' },
|
|
2254
|
+
accessChanges: { type: 'boolean', description: 'True when any of your verdicts differs between source and destination.' },
|
|
2255
|
+
allowed: { type: 'boolean', description: 'Whether the move may run.' },
|
|
2256
|
+
reason: str('Why it may not, when `allowed` is false.'),
|
|
2257
|
+
dryRun: { type: 'boolean', description: 'True on a dry run.' },
|
|
2258
|
+
confirmationRequired: { type: 'boolean', description: 'True when the call stopped for want of `confirm: true`.' },
|
|
2259
|
+
message: str('One sentence on what happened (or did not).'),
|
|
2260
|
+
moved: { type: 'boolean', description: 'True once the move landed.' },
|
|
2261
|
+
},
|
|
838
2262
|
required: ['src', 'dest', 'moved'],
|
|
839
2263
|
},
|
|
840
2264
|
write: true,
|
|
2265
|
+
proposable: true,
|
|
841
2266
|
handler: async (a, ctx: ToolContext) => {
|
|
2267
|
+
// Without trailing slashes: every path under a folder is derived from
|
|
2268
|
+
// these by prefix, and `filesUnder` names children without the slash.
|
|
2269
|
+
const src = (a.src as string).replace(/\/+$/, '');
|
|
2270
|
+
const dest = (a.dest as string).replace(/\/+$/, '');
|
|
2271
|
+
const branch = a.branch as string;
|
|
842
2272
|
// A move CARRIES the source content into the destination — a genuine
|
|
843
2273
|
// cross-ontology flow if the two differ — so BOTH endpoints are write-gated
|
|
844
2274
|
// (unlike a plain delete, which moves no content). Check both BEFORE
|
|
845
2275
|
// touching disk so a blocked endpoint can't leave the source already deleted.
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
await
|
|
849
|
-
await
|
|
850
|
-
|
|
851
|
-
|
|
2276
|
+
await assertOntologyWriteAllowed(sessionOntologyGate, ctx, src);
|
|
2277
|
+
await assertOntologyWriteAllowed(sessionOntologyGate, ctx, dest);
|
|
2278
|
+
const fs = await ctx.getFilesystem(branch);
|
|
2279
|
+
const root = await workspaceRoot(branch, ctx);
|
|
2280
|
+
// Before the kind check, which stats THROUGH a link: a dangling link at
|
|
2281
|
+
// `src` would otherwise be answered "does not exist".
|
|
2282
|
+
await assertNoSymlinkOnPath(root, src);
|
|
2283
|
+
await assertNoSymlinkOnPath(root, dest);
|
|
2284
|
+
const kind = await kindOf(fs, src);
|
|
2285
|
+
if (kind === null) throw notFound(src, 'Nothing to move');
|
|
2286
|
+
const srcFiles = kind === 'folder' ? (await filesUnder(fs, src)).files : [src];
|
|
2287
|
+
// A restricted run is judged on what it would actually write: each file
|
|
2288
|
+
// at its old and its new path, not an extensionless folder path.
|
|
2289
|
+
for (const f of srcFiles) {
|
|
2290
|
+
writePolicy.assertPathWritable(ctx.sessionId, f);
|
|
2291
|
+
writePolicy.assertPathWritable(ctx.sessionId, dest + f.slice(src.length));
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
const [before, after] = await Promise.all([accessAt(branch, ctx, src), accessAt(branch, ctx, dest)]);
|
|
2295
|
+
const accessChanges = (Object.keys(before) as (keyof AccessVerbs)[]).some((v) => before[v] !== after[v]);
|
|
2296
|
+
// The placeholder moves with its folder, but it is never content.
|
|
2297
|
+
const descendants = srcFiles.filter((f) => !isFolderPlaceholder(f)).length;
|
|
2298
|
+
// Neither end may be the platform's own: a move neither takes a platform
|
|
2299
|
+
// item away nor makes one (a note renamed to `access.md` would start
|
|
2300
|
+
// governing its folder). The source end is judged here; the destination
|
|
2301
|
+
// end waits for the write verdict below, because reading it at all is
|
|
2302
|
+
// what the caller has to have earned.
|
|
2303
|
+
const srcManaged = managedReason(await onDiskSpelling(root, src), kind);
|
|
2304
|
+
// A folder move deletes every file under `src` and creates it again under
|
|
2305
|
+
// `dest`, so every one of them is judged at both paths — a file its own
|
|
2306
|
+
// rules deny you is not carried off because its folder is writable. The
|
|
2307
|
+
// lock gate locks only the two folder paths, so this is the check.
|
|
2308
|
+
const destFiles = srcFiles.map((f) => dest + f.slice(src.length));
|
|
2309
|
+
// The write verdict comes FIRST, before anything that looks at the
|
|
2310
|
+
// destination. "A file named Notes.md already exists in Sales." is a
|
|
2311
|
+
// fact about a folder, and on a protected branch a caller who may not
|
|
2312
|
+
// write there must not learn it from a refusal — answering existence
|
|
2313
|
+
// first would make this tool an existence oracle for folders whose
|
|
2314
|
+
// contents the caller cannot otherwise see. A source the platform owns
|
|
2315
|
+
// is refused on the source alone and needs no destination at all.
|
|
2316
|
+
const blocked = srcManaged !== undefined
|
|
2317
|
+
? []
|
|
2318
|
+
: await writeBlocked(branch, ctx, [src, dest, ...srcFiles, ...destFiles]);
|
|
2319
|
+
const mayReadDestination = blocked.length === 0;
|
|
2320
|
+
const destOnDisk = mayReadDestination ? await onDiskSpelling(root, dest) : dest;
|
|
2321
|
+
const destManaged = mayReadDestination ? managedReason(destOnDisk, kind) : undefined;
|
|
2322
|
+
const occupiedBy = srcManaged === undefined && mayReadDestination
|
|
2323
|
+
? await existingAt(root, dest, src)
|
|
2324
|
+
: null;
|
|
2325
|
+
const collision = occupiedBy !== null;
|
|
2326
|
+
// Checked after the collision: onto an existing platform file, "already
|
|
2327
|
+
// exists" is the plainer answer.
|
|
2328
|
+
const createsManaged = srcManaged !== undefined || collision || destManaged === undefined
|
|
2329
|
+
? undefined
|
|
2330
|
+
: isGitMetadata(destOnDisk)
|
|
2331
|
+
? destManaged
|
|
2332
|
+
: kind === 'file'
|
|
2333
|
+
? platformFileCreationRefusal(destOnDisk)
|
|
2334
|
+
: `"${destOnDisk}" is a platform folder; a move cannot create one.`;
|
|
2335
|
+
const managedWhy = srcManaged ?? createsManaged;
|
|
2336
|
+
const managed = managedWhy !== undefined;
|
|
2337
|
+
// Same order as the checks above: the write refusal outranks every
|
|
2338
|
+
// answer that had to look at the destination to be written.
|
|
2339
|
+
const reason = managed
|
|
2340
|
+
? managedWhy
|
|
2341
|
+
: blocked.length > 0
|
|
2342
|
+
? `You may not write ${blocked.length === 1 ? `"${blocked[0]}"` : `${blocked.length} of the paths, e.g. "${blocked[0]}"`}, so the move cannot run.`
|
|
2343
|
+
: occupiedBy !== null
|
|
2344
|
+
? entryExistsMessage(occupiedBy, dest)
|
|
2345
|
+
: undefined;
|
|
2346
|
+
const impact = {
|
|
2347
|
+
src,
|
|
2348
|
+
dest,
|
|
2349
|
+
kind,
|
|
2350
|
+
descendants,
|
|
2351
|
+
access: { before, after },
|
|
2352
|
+
accessChanges,
|
|
2353
|
+
allowed: reason === undefined,
|
|
2354
|
+
...(reason !== undefined ? { reason } : {}),
|
|
2355
|
+
};
|
|
2356
|
+
if (a.dryRun === true) return { ...impact, dryRun: true, moved: false };
|
|
2357
|
+
if (managed) throw new ToolError(reason!, 400);
|
|
2358
|
+
// A folder move is judged on its two folder paths and every file under
|
|
2359
|
+
// them: a refusal on one of the folder paths says so, because a folder
|
|
2360
|
+
// directly under a root is proposable where a file there is not.
|
|
2361
|
+
if (blocked.length > 0) {
|
|
2362
|
+
const folderPath = kind === 'folder' && (blocked[0] === src || blocked[0] === dest);
|
|
2363
|
+
throw await writeRefusal(branch, blocked[0], folderPath ? 'dir' : 'file');
|
|
2364
|
+
}
|
|
2365
|
+
if (collision) throw new ToolError(reason!, 409);
|
|
2366
|
+
if (accessChanges && a.confirm !== true) {
|
|
2367
|
+
return {
|
|
2368
|
+
...impact,
|
|
2369
|
+
confirmationRequired: true,
|
|
2370
|
+
moved: false,
|
|
2371
|
+
message: `Nothing was moved: your access at "${dest}" differs from "${src}", so this move requires confirm: true.`,
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
// The look above is what produces the sentence; the filesystem's own
|
|
2375
|
+
// no-replace move is what guarantees it. A destination created between
|
|
2376
|
+
// the two — by another agent, or by the sidebar, which moves without
|
|
2377
|
+
// taking this lock — comes back here as a refusal, not an overwrite.
|
|
2378
|
+
await asEntryExists(() => fs.moveFile(src, dest));
|
|
2379
|
+
// Moving the last file — or a whole folder — out leaves the folder it
|
|
2380
|
+
// came from in place, like a delete.
|
|
2381
|
+
await keepFolderOf(fs, ctx, branch, src, kbDirName);
|
|
2382
|
+
return { ...impact, moved: true };
|
|
852
2383
|
},
|
|
853
2384
|
});
|
|
854
2385
|
|
|
855
2386
|
mount({
|
|
856
2387
|
name: 'copy_file',
|
|
857
|
-
description:
|
|
2388
|
+
description:
|
|
2389
|
+
'Copy a workspace file to a new path. The destination must not exist — like a move, a copy never overwrites a file or a folder; to change what is in a file that already exists, write it. Committed + pushed as you.'
|
|
2390
|
+
+ ONTOLOGY_BOUNDARY_NOTE,
|
|
858
2391
|
inputs: {
|
|
859
2392
|
type: 'object',
|
|
860
2393
|
properties: {
|
|
861
2394
|
branch: BRANCH_INPUT,
|
|
862
|
-
src: wsPath(kbDirName, 'Source path'
|
|
863
|
-
dest: wsPath(kbDirName, 'Destination path'),
|
|
2395
|
+
src: wsPath(kbDirName, 'Source path'),
|
|
2396
|
+
dest: wsPath(kbDirName, 'Destination path — must not exist yet'),
|
|
864
2397
|
sessionId: SESSION_ID_INPUT,
|
|
865
2398
|
},
|
|
866
2399
|
required: ['branch', 'src', 'dest'],
|
|
@@ -872,6 +2405,7 @@ export function registerWorkspaceTools(
|
|
|
872
2405
|
required: ['src', 'dest', 'copied'],
|
|
873
2406
|
},
|
|
874
2407
|
write: true,
|
|
2408
|
+
proposable: true,
|
|
875
2409
|
handler: async (a, ctx: ToolContext) => {
|
|
876
2410
|
// A copy CARRIES the source content into the destination — a genuine
|
|
877
2411
|
// cross-ontology flow if the two differ — so BOTH endpoints are write-gated.
|
|
@@ -880,8 +2414,63 @@ export function registerWorkspaceTools(
|
|
|
880
2414
|
writePolicy.assertPathWritable(ctx.sessionId, a.dest as string);
|
|
881
2415
|
await assertOntologyWriteAllowed(sessionOntologyGate, ctx, a.src as string);
|
|
882
2416
|
await assertOntologyWriteAllowed(sessionOntologyGate, ctx, a.dest as string);
|
|
883
|
-
|
|
884
|
-
|
|
2417
|
+
const branch = a.branch as string;
|
|
2418
|
+
const src = a.src as string;
|
|
2419
|
+
const dest = a.dest as string;
|
|
2420
|
+
// A copy lands bytes at a name of its own, so it is refused by the same
|
|
2421
|
+
// rule a move is: nothing already at `dest` is replaced. To put new
|
|
2422
|
+
// content into a file that exists, write it.
|
|
2423
|
+
//
|
|
2424
|
+
// The same plain-path rule a move applies to both its ends, applied
|
|
2425
|
+
// here because the look at the destination comes before the filesystem's
|
|
2426
|
+
// own containment check: a path with a `..` segment must not reach
|
|
2427
|
+
// `lstat` outside the workspace, even to be told a name is taken.
|
|
2428
|
+
assertPlainPath(dest);
|
|
2429
|
+
// The write verdict comes FIRST, for the reason `move_file` gives at
|
|
2430
|
+
// length: "already exists" is a fact about the destination folder, and a
|
|
2431
|
+
// caller who may not write there must not be told it. The lock gate
|
|
2432
|
+
// refuses this copy anyway — but only after the copy had already
|
|
2433
|
+
// answered, which is exactly the oracle.
|
|
2434
|
+
const blockedDest = await writeBlocked(branch, ctx, [dest]);
|
|
2435
|
+
if (blockedDest.length > 0) throw await writeRefusal(branch, blockedDest[0]);
|
|
2436
|
+
const occupiedBy = await existingAt(await workspaceRoot(branch, ctx), dest);
|
|
2437
|
+
if (occupiedBy !== null) throw new ToolError(entryExistsMessage(occupiedBy, dest), 409);
|
|
2438
|
+
// The copy itself lands exclusively (`COPYFILE_EXCL`, under the
|
|
2439
|
+
// destination's lock), so a name taken between the look and the landing
|
|
2440
|
+
// is refused with the same sentence rather than overwritten.
|
|
2441
|
+
//
|
|
2442
|
+
// Absence is only asked about once the copy has FAILED, and after the
|
|
2443
|
+
// write verdict above: a caller who may not write here gets the same
|
|
2444
|
+
// `write-denied` whether the source is there or not, exactly as from
|
|
2445
|
+
// write_file, edit_file, move_file and delete_file. Probing the source up
|
|
2446
|
+
// front would put a 404 in front of that 403 and make the refusal report
|
|
2447
|
+
// whether a path the caller could not copy from exists.
|
|
2448
|
+
//
|
|
2449
|
+
// WHICH end the absence belongs to is then decided by probing the
|
|
2450
|
+
// SOURCE, as move_file probes its own. A copy has exactly two ends, and
|
|
2451
|
+
// the filesystem blames the source for both: `LocalFilesystem.copyFile`
|
|
2452
|
+
// re-throws every ENOENT as `FileNotFoundError(src)`, and a destination
|
|
2453
|
+
// segment that is a file escapes raw as ENOTDIR from the parent mkdir
|
|
2454
|
+
// (`existingAt` above reads that as "nothing there" and lets the copy
|
|
2455
|
+
// go on to say so). So a source that is really gone gets the 404 naming
|
|
2456
|
+
// the source; a source sitting right there means the absence was the
|
|
2457
|
+
// DESTINATION's, and it gets the same 404 naming the destination.
|
|
2458
|
+
// Neither may escape as a 500 — that is the answer whose message carries
|
|
2459
|
+
// the server's own absolute path. Anything that is not absence travels
|
|
2460
|
+
// on as it always did.
|
|
2461
|
+
const fs = await ctx.getFilesystem(branch);
|
|
2462
|
+
try {
|
|
2463
|
+
await asEntryExists(() => fs.copyFile(src, dest));
|
|
2464
|
+
} catch (err) {
|
|
2465
|
+
const missing = isAbsence(err) || (err as { name?: string }).name === 'FileNotFoundError';
|
|
2466
|
+
if (missing) {
|
|
2467
|
+
throw (await kindOf(fs, src)) === null
|
|
2468
|
+
? notFound(src, 'Nothing to copy')
|
|
2469
|
+
: notFound(dest, 'Nowhere to copy to');
|
|
2470
|
+
}
|
|
2471
|
+
throw err;
|
|
2472
|
+
}
|
|
2473
|
+
return { src, dest, copied: true };
|
|
885
2474
|
},
|
|
886
2475
|
});
|
|
887
2476
|
|
|
@@ -894,7 +2483,7 @@ export function registerWorkspaceTools(
|
|
|
894
2483
|
type: 'object',
|
|
895
2484
|
properties: {
|
|
896
2485
|
branch: BRANCH_INPUT,
|
|
897
|
-
path: wsPath(kbDirName, 'Path to the .zip archive'
|
|
2486
|
+
path: wsPath(kbDirName, 'Path to the .zip archive'),
|
|
898
2487
|
destination: wsPath(kbDirName, "Directory to extract into (default: the zip's parent)"),
|
|
899
2488
|
sessionId: SESSION_ID_INPUT,
|
|
900
2489
|
},
|
|
@@ -924,21 +2513,38 @@ export function registerWorkspaceTools(
|
|
|
924
2513
|
// Reading the source archive pins/records the source ontology, so a session
|
|
925
2514
|
// can't unzip from ontology A into ontology B without the A read counting.
|
|
926
2515
|
await recordOntologyRead(sessionOntologyGate, ctx, zipPath);
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
2516
|
+
// A .zip that is not there is a missing PATH, not an unreadable archive:
|
|
2517
|
+
// the service now says so (PathNotFoundError) and the helper turns it
|
|
2518
|
+
// into the same 404 every other file tool answers. Only that declared
|
|
2519
|
+
// answer maps — a failure part-way through an extraction is not the
|
|
2520
|
+
// archive going missing.
|
|
2521
|
+
return orDeclaredNotFound(
|
|
2522
|
+
() =>
|
|
2523
|
+
ctx.workspaceService.unzipFile(
|
|
2524
|
+
workspaceIdForBranch(a.branch as string),
|
|
2525
|
+
zipPath,
|
|
2526
|
+
typeof a.destination === 'string' ? a.destination : undefined,
|
|
2527
|
+
// Each extracted file is a write: a cross-ontology or write-blocked entry
|
|
2528
|
+
// is skipped (not extracted), so an archive can't bypass the boundary — the
|
|
2529
|
+
// extension policy applies per entry too, so a restricted run can't unzip a
|
|
2530
|
+
// `.md` into the graph.
|
|
2531
|
+
(wsRelPath) => {
|
|
2532
|
+
// An entry that would land beside the repository is skipped with the
|
|
2533
|
+
// corrected-path reason, like any other refused entry.
|
|
2534
|
+
assertInsideRepo(wsRelPath, kbDirName);
|
|
2535
|
+
// Extraction writes straight to disk, past the filesystem's roles.yaml
|
|
2536
|
+
// gate — so an archive may not carry one at all.
|
|
2537
|
+
if (isRolesYamlPath(wsRelPath, kbDirName)) {
|
|
2538
|
+
throw new ToolError(
|
|
2539
|
+
'roles.yaml is never extracted from an archive — change it with edit_file or write_file, where the change is checked.',
|
|
2540
|
+
422,
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
writePolicy.assertPathWritable(ctx.sessionId, wsRelPath);
|
|
2544
|
+
return assertOntologyWriteAllowed(sessionOntologyGate, ctx, wsRelPath);
|
|
2545
|
+
},
|
|
2546
|
+
),
|
|
2547
|
+
'Nothing to extract',
|
|
942
2548
|
);
|
|
943
2549
|
},
|
|
944
2550
|
});
|
|
@@ -950,6 +2556,7 @@ export function registerWorkspaceTools(
|
|
|
950
2556
|
'Run a shell command in the workspace directory. Returns `{ stdout, stderr, exitCode }` (output capped). Use for git status/log, grep/rg, build/test commands.' +
|
|
951
2557
|
ONTOLOGY_BOUNDARY_NOTE,
|
|
952
2558
|
internalOnly: true,
|
|
2559
|
+
fileTool: false,
|
|
953
2560
|
inputs: {
|
|
954
2561
|
type: 'object',
|
|
955
2562
|
properties: {
|