@bevel-software/platform-core-backend 0.33.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-guide/access-control.md +146 -289
- package/agent-guide/conventions.md +23 -25
- package/agent-guide/directory-structure.md +72 -120
- package/agent-guide/drafts-and-change-requests.md +92 -0
- package/agent-guide/for-git-users.md +47 -0
- package/agent-guide/glossary.md +36 -0
- package/agent-guide/how-agents-find-things.md +49 -0
- package/agent-guide/html-views.md +239 -8
- package/agent-guide/new-knowledge-base.md +9 -8
- package/agent-guide/skills.md +67 -42
- package/agent-guide/start-here.md +65 -0
- package/agent-guide/tool-manuals.md +133 -155
- package/agent-guide/tools.md +67 -0
- package/agent-guide/where-a-new-file-goes.md +45 -28
- package/dist/core/catalog-revision.d.ts +12 -1
- package/dist/core/catalog-revision.d.ts.map +1 -1
- package/dist/core/catalog-revision.js +23 -3
- package/dist/core/catalog-revision.js.map +1 -1
- package/dist/core/core-ports.d.ts +41 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +15 -8
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +22 -5
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +44 -16
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +16 -0
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +12 -2
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +39 -9
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +15 -3
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +239 -75
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-mutation.service.d.ts +22 -24
- package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
- package/dist/modules/access/access-mutation.service.js +48 -51
- package/dist/modules/access/access-mutation.service.js.map +1 -1
- package/dist/modules/access/access-view.d.ts.map +1 -1
- package/dist/modules/access/access-view.js +12 -11
- package/dist/modules/access/access-view.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts +3 -0
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +58 -33
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/groups-admin.routes.d.ts.map +1 -1
- package/dist/modules/access/groups-admin.routes.js +4 -2
- package/dist/modules/access/groups-admin.routes.js.map +1 -1
- package/dist/modules/access/groups-admin.service.d.ts +44 -13
- package/dist/modules/access/groups-admin.service.d.ts.map +1 -1
- package/dist/modules/access/groups-admin.service.js +80 -38
- package/dist/modules/access/groups-admin.service.js.map +1 -1
- package/dist/modules/access/groups-edit.d.ts +1 -1
- package/dist/modules/access/groups-edit.d.ts.map +1 -1
- package/dist/modules/access/groups-edit.js +1 -1
- package/dist/modules/access/groups-edit.js.map +1 -1
- package/dist/modules/access/index.d.ts +1 -1
- package/dist/modules/access/index.d.ts.map +1 -1
- package/dist/modules/access/reference-scan.d.ts +116 -24
- package/dist/modules/access/reference-scan.d.ts.map +1 -1
- package/dist/modules/access/reference-scan.js +305 -44
- package/dist/modules/access/reference-scan.js.map +1 -1
- package/dist/modules/access/roles-admin.service.d.ts +17 -8
- package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
- package/dist/modules/access/roles-admin.service.js +32 -23
- package/dist/modules/access/roles-admin.service.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +35 -10
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +60 -20
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/access-model/access-splice.d.ts +22 -18
- package/dist/modules/access-model/access-splice.d.ts.map +1 -1
- package/dist/modules/access-model/access-splice.js +20 -14
- package/dist/modules/access-model/access-splice.js.map +1 -1
- package/dist/modules/access-model/group-files.d.ts +1 -0
- package/dist/modules/access-model/group-files.d.ts.map +1 -1
- package/dist/modules/access-model/group-files.js +5 -1
- package/dist/modules/access-model/group-files.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.d.ts +47 -7
- package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
- package/dist/modules/agent-guide/agent-guide.js +103 -13
- package/dist/modules/agent-guide/agent-guide.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.tools.d.ts +6 -2
- package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -1
- package/dist/modules/agent-guide/agent-guide.tools.js +46 -22
- package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -1
- package/dist/modules/agent-guide/guide-budget.d.ts +48 -0
- package/dist/modules/agent-guide/guide-budget.d.ts.map +1 -0
- package/dist/modules/agent-guide/guide-budget.js +67 -0
- package/dist/modules/agent-guide/guide-budget.js.map +1 -0
- package/dist/modules/agent-guide/index.d.ts +2 -1
- package/dist/modules/agent-guide/index.d.ts.map +1 -1
- package/dist/modules/agent-guide/index.js +2 -1
- package/dist/modules/agent-guide/index.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +3 -1
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +7 -1
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +5 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +26 -9
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
- package/dist/modules/agent-scope/agent-scope.contract.d.ts +67 -0
- package/dist/modules/agent-scope/agent-scope.contract.d.ts.map +1 -0
- package/dist/modules/agent-scope/agent-scope.contract.js +27 -0
- package/dist/modules/agent-scope/agent-scope.contract.js.map +1 -0
- package/dist/modules/agent-scope/connection-scope.d.ts +111 -0
- package/dist/modules/agent-scope/connection-scope.d.ts.map +1 -0
- package/dist/modules/agent-scope/connection-scope.js +165 -0
- package/dist/modules/agent-scope/connection-scope.js.map +1 -0
- package/dist/modules/agent-scope/index.d.ts +3 -0
- package/dist/modules/agent-scope/index.d.ts.map +1 -0
- package/dist/modules/agent-scope/index.js +3 -0
- package/dist/modules/agent-scope/index.js.map +1 -0
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +2 -0
- package/dist/modules/audit/agent-audit.service.js.map +1 -1
- package/dist/modules/audit/audit.contract.d.ts +6 -0
- package/dist/modules/audit/audit.contract.d.ts.map +1 -1
- package/dist/modules/audit/audit.contract.js.map +1 -1
- package/dist/modules/auth/account-admission.d.ts +12 -0
- package/dist/modules/auth/account-admission.d.ts.map +1 -1
- package/dist/modules/auth/account-admission.js +15 -0
- package/dist/modules/auth/account-admission.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +27 -1
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +42 -4
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts +13 -9
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +35 -29
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +55 -6
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +128 -34
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/index.d.ts +1 -1
- package/dist/modules/auth/index.d.ts.map +1 -1
- package/dist/modules/auth/index.js +1 -1
- package/dist/modules/auth/index.js.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.js +2 -9
- package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +2 -2
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/connection-probe/connection-probe.service.d.ts +0 -12
- package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
- package/dist/modules/connection-probe/connection-probe.service.js +1 -38
- package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +68 -0
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +33 -0
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/declared-variables/declared-variables.routes.d.ts +8 -1
- package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
- package/dist/modules/declared-variables/declared-variables.routes.js +13 -2
- package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
- package/dist/modules/embed/embed.tools.d.ts.map +1 -1
- package/dist/modules/embed/embed.tools.js +5 -0
- package/dist/modules/embed/embed.tools.js.map +1 -1
- package/dist/modules/kb-fs/index.d.ts +2 -0
- package/dist/modules/kb-fs/index.d.ts.map +1 -1
- package/dist/modules/kb-fs/index.js +1 -0
- package/dist/modules/kb-fs/index.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +7 -1
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/kb-fs/repo-path.d.ts +26 -6
- package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
- package/dist/modules/kb-fs/repo-path.js +22 -7
- package/dist/modules/kb-fs/repo-path.js.map +1 -1
- package/dist/modules/kb-fs/tmp-ref.d.ts +36 -0
- package/dist/modules/kb-fs/tmp-ref.d.ts.map +1 -0
- package/dist/modules/kb-fs/tmp-ref.js +76 -0
- package/dist/modules/kb-fs/tmp-ref.js.map +1 -0
- package/dist/modules/mcp/mcp-auth.middleware.d.ts +7 -0
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +14 -1
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/mcp.routes.d.ts +7 -1
- package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.routes.js +39 -4
- package/dist/modules/mcp/mcp.routes.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +11 -0
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +74 -48
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +45 -10
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +9 -0
- package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js +34 -1
- package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
- package/dist/modules/plugins/index.d.ts +1 -1
- package/dist/modules/plugins/index.d.ts.map +1 -1
- package/dist/modules/plugins/index.js.map +1 -1
- package/dist/modules/plugins/plugin-links.d.ts +18 -2
- package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
- package/dist/modules/plugins/plugin-links.js.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts +5 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +32 -5
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +11 -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 +159 -24
- 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 +1 -1
- package/dist/modules/secrets-vault/index.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +53 -11
- package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.contract.js +27 -0
- package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +9 -0
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js +100 -42
- package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
- package/dist/modules/settings/oidc-check.d.ts.map +1 -1
- package/dist/modules/settings/oidc-check.js +5 -17
- package/dist/modules/settings/oidc-check.js.map +1 -1
- package/dist/modules/skills/index.d.ts +3 -3
- package/dist/modules/skills/index.d.ts.map +1 -1
- package/dist/modules/skills/index.js +0 -2
- package/dist/modules/skills/index.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +16 -0
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +6 -1
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +25 -13
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.interface.d.ts +11 -0
- 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 +1 -0
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +4 -1
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/index.d.ts +1 -1
- package/dist/modules/tool-auth/index.d.ts.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +9 -0
- package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.js +1 -0
- package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +27 -3
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +32 -6
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/tool-context.d.ts +7 -0
- package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-context.js +3 -0
- package/dist/modules/tool-helpers/tool-context.js.map +1 -1
- package/dist/modules/tool-helpers/tool-def.js +1 -1
- package/dist/modules/tool-helpers/tool-def.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +15 -0
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +19 -0
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +10 -0
- package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
- package/dist/modules/tool-helpers/validate-token.d.ts +3 -1
- package/dist/modules/tool-helpers/validate-token.d.ts.map +1 -1
- package/dist/modules/tool-helpers/validate-token.js +1 -1
- package/dist/modules/tool-helpers/validate-token.js.map +1 -1
- package/dist/modules/tool-manuals/index.d.ts +3 -3
- package/dist/modules/tool-manuals/index.d.ts.map +1 -1
- package/dist/modules/tool-manuals/index.js +0 -2
- package/dist/modules/tool-manuals/index.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +7 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.js +69 -38
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +46 -6
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +7 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.routes.js +41 -4
- package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +24 -13
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +138 -59
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +8 -2
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +56 -20
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/manual.routes.d.ts +8 -1
- package/dist/modules/tool-registry/manual.routes.d.ts.map +1 -1
- package/dist/modules/tool-registry/manual.routes.js +27 -7
- package/dist/modules/tool-registry/manual.routes.js.map +1 -1
- package/dist/modules/tool-registry/tool.contract.d.ts +7 -0
- package/dist/modules/tool-registry/tool.contract.d.ts.map +1 -1
- package/dist/modules/tool-registry/tool.contract.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 +36 -27
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +2 -2
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +20 -35
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +2 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +3 -0
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -1
- package/dist/modules/workspace/agent-access.gate.js +1 -1
- package/dist/modules/workspace/agent-access.gate.js.map +1 -1
- package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +28 -4
- package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
- package/dist/modules/workspace/file-readers/doc-extract.service.js +61 -40
- package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
- package/dist/modules/workspace/index.d.ts +8 -0
- package/dist/modules/workspace/index.d.ts.map +1 -1
- package/dist/modules/workspace/index.js +8 -0
- package/dist/modules/workspace/index.js.map +1 -1
- package/dist/modules/workspace/nul-scan.d.ts +19 -0
- package/dist/modules/workspace/nul-scan.d.ts.map +1 -0
- package/dist/modules/workspace/nul-scan.js +55 -0
- package/dist/modules/workspace/nul-scan.js.map +1 -0
- package/dist/modules/workspace/pattern-matcher.contract.d.ts +110 -27
- package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -1
- package/dist/modules/workspace/pattern-matcher.contract.js +30 -13
- package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -1
- package/dist/modules/workspace/ripgrep-search.d.ts +54 -0
- package/dist/modules/workspace/ripgrep-search.d.ts.map +1 -0
- package/dist/modules/workspace/ripgrep-search.js +657 -0
- package/dist/modules/workspace/ripgrep-search.js.map +1 -0
- package/dist/modules/workspace/ripgrep-version.d.ts +15 -0
- package/dist/modules/workspace/ripgrep-version.d.ts.map +1 -0
- package/dist/modules/workspace/ripgrep-version.js +31 -0
- package/dist/modules/workspace/ripgrep-version.js.map +1 -0
- package/dist/modules/workspace/startup/beside-checkout.d.ts +8 -5
- package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -1
- package/dist/modules/workspace/startup/beside-checkout.js +14 -5
- package/dist/modules/workspace/startup/beside-checkout.js.map +1 -1
- package/dist/modules/workspace/worker-pool.d.ts +3 -4
- package/dist/modules/workspace/worker-pool.d.ts.map +1 -1
- package/dist/modules/workspace/worker-pool.js.map +1 -1
- package/dist/modules/workspace/workspace-tmp.d.ts +146 -0
- package/dist/modules/workspace/workspace-tmp.d.ts.map +1 -0
- package/dist/modules/workspace/workspace-tmp.js +435 -0
- package/dist/modules/workspace/workspace-tmp.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +5 -26
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +47 -6
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +86 -6
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +3 -2
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +531 -137
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/oauth-token-error.d.ts +23 -0
- package/dist/shared/oauth-token-error.d.ts.map +1 -0
- package/dist/shared/oauth-token-error.js +47 -0
- package/dist/shared/oauth-token-error.js.map +1 -0
- package/dist/shared/path-order.d.ts +28 -8
- package/dist/shared/path-order.d.ts.map +1 -1
- package/dist/shared/path-order.js +37 -8
- package/dist/shared/path-order.js.map +1 -1
- package/dist/shared/read-capped.d.ts +19 -0
- package/dist/shared/read-capped.d.ts.map +1 -0
- package/dist/shared/read-capped.js +45 -0
- package/dist/shared/read-capped.js.map +1 -0
- package/migrations/0022_session_version.sql +6 -0
- package/migrations/0023_agent_path.sql +3 -0
- package/migrations/meta/0022_snapshot.json +2360 -0
- package/migrations/meta/0023_snapshot.json +2378 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +4 -3
- package/src/__tests__/hexis-catalog.ts +144 -0
- package/src/__tests__/pg-scratch.test.ts +69 -0
- package/src/__tests__/pg-scratch.ts +31 -0
- package/src/__tests__/real-disk.ts +21 -0
- package/src/__tests__/slow-ripgrep.ts +65 -0
- package/src/__tests__/tokens.ts +19 -0
- package/src/__tests__/tool-manuals.ts +32 -0
- package/src/__tests__/tool-wiring.ts +25 -0
- package/src/core/__tests__/boot-without-ripgrep.test.ts +34 -0
- package/src/core/__tests__/catalog-revision.test.ts +89 -0
- package/src/core/__tests__/damaged-document.tools.test.ts +2 -2
- package/src/core/__tests__/gated-boot.test.ts +8 -1
- package/src/core/__tests__/lifecycle.test.ts +46 -13
- package/src/core/__tests__/oauth-sign-in-real-file.test.ts +179 -0
- package/src/core/catalog-revision.ts +24 -2
- package/src/core/core-ports.ts +42 -0
- package/src/core/create-core-server.ts +14 -1
- package/src/core/create-core-services.ts +62 -15
- package/src/core/lifecycle.ts +26 -2
- package/src/index.ts +1 -1
- package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +12 -0
- package/src/modules/access/__tests__/access-control.colliding-name-cases.test.ts +113 -0
- package/src/modules/access/__tests__/access-control.frontmatter-head.test.ts +117 -0
- package/src/modules/access/__tests__/access-control.group-named-admin.test.ts +122 -0
- package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +50 -0
- package/src/modules/access/__tests__/access-declarations.test.ts +19 -0
- package/src/modules/access/__tests__/access-groups.test.ts +55 -22
- package/src/modules/access/__tests__/access-mutation.service.test.ts +85 -59
- package/src/modules/access/__tests__/access-restrict-here.test.ts +7 -5
- package/src/modules/access/__tests__/access.routes.admins.test.ts +1 -0
- package/src/modules/access/__tests__/access.routes.batch.test.ts +2 -0
- package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +2 -0
- package/src/modules/access/__tests__/access.routes.group-grant.test.ts +50 -14
- package/src/modules/access/__tests__/access.routes.overrides.test.ts +2 -1
- package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +1 -0
- package/src/modules/access/__tests__/access.routes.prospective.test.ts +52 -10
- package/src/modules/access/__tests__/access.routes.revoke.test.ts +18 -13
- package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
- package/src/modules/access/__tests__/grant-sources.test.ts +20 -15
- package/src/modules/access/__tests__/groups-admin.routes.test.ts +61 -0
- package/src/modules/access/__tests__/groups-admin.service.test.ts +211 -18
- package/src/modules/access/__tests__/holder-changes.test.ts +127 -0
- package/src/modules/access/__tests__/reference-scan.test.ts +366 -14
- package/src/modules/access/__tests__/roles-admin.security.test.ts +1 -1
- package/src/modules/access/__tests__/roles-admin.service.test.ts +43 -22
- package/src/modules/access/__tests__/roles-capabilities.test.ts +6 -2
- package/src/modules/access/__tests__/roles.routes.test.ts +1 -1
- package/src/modules/access/access-control.interface.ts +43 -10
- package/src/modules/access/access-control.service.ts +250 -72
- package/src/modules/access/access-mutation.service.ts +48 -53
- package/src/modules/access/access-view.ts +12 -10
- package/src/modules/access/access.routes.ts +66 -37
- package/src/modules/access/groups-admin.routes.ts +4 -2
- package/src/modules/access/groups-admin.service.ts +114 -44
- package/src/modules/access/groups-edit.ts +1 -1
- package/src/modules/access/index.ts +1 -1
- package/src/modules/access/reference-scan.ts +319 -46
- package/src/modules/access/roles-admin.service.ts +43 -26
- package/src/modules/access/synced-groups-writer.ts +1 -1
- package/src/modules/access-model/__tests__/access-grammar.test.ts +38 -1
- package/src/modules/access-model/__tests__/access-splice.test.ts +50 -8
- package/src/modules/access-model/access-grammar.ts +65 -20
- package/src/modules/access-model/access-splice.ts +31 -23
- package/src/modules/access-model/group-files.ts +5 -0
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +110 -25
- package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +38 -11
- package/src/modules/agent-guide/__tests__/guide-budget.test.ts +95 -0
- package/src/modules/agent-guide/__tests__/guide-eval.test.ts +182 -0
- package/src/modules/agent-guide/__tests__/guide-matches-code.test.ts +263 -0
- package/src/modules/agent-guide/__tests__/tool-surface.json +514 -0
- package/src/modules/agent-guide/__tests__/tool-surface.test.ts +75 -0
- package/src/modules/agent-guide/agent-guide.tools.ts +51 -22
- package/src/modules/agent-guide/agent-guide.ts +120 -13
- package/src/modules/agent-guide/guide-budget.ts +73 -0
- package/src/modules/agent-guide/index.ts +12 -0
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +5 -4
- package/src/modules/agent-instructions/compose.ts +7 -1
- package/src/modules/agent-instructions/shared-file-rules.ts +27 -9
- package/src/modules/agent-scope/__tests__/agent-surfaces.connect-as.test.ts +421 -0
- package/src/modules/agent-scope/__tests__/connection-scope.test.ts +46 -0
- package/src/modules/agent-scope/__tests__/in-memory-kb.ts +148 -0
- package/src/modules/agent-scope/agent-scope.contract.ts +83 -0
- package/src/modules/agent-scope/connection-scope.ts +199 -0
- package/src/modules/agent-scope/index.ts +15 -0
- package/src/modules/audit/agent-audit.service.ts +2 -0
- package/src/modules/audit/audit.contract.ts +6 -0
- package/src/modules/auth/__tests__/auth.middleware.test.ts +30 -2
- package/src/modules/auth/__tests__/auth.routes.test.ts +71 -5
- package/src/modules/auth/__tests__/auth.service.test.ts +90 -9
- package/src/modules/auth/__tests__/password-change-sessions.pg.test.ts +269 -0
- package/src/modules/auth/account-admission.ts +19 -0
- package/src/modules/auth/auth.middleware.ts +45 -4
- package/src/modules/auth/auth.routes.ts +46 -29
- package/src/modules/auth/auth.service.ts +135 -34
- package/src/modules/auth/index.ts +2 -0
- package/src/modules/auth/oidc-auth-provider.ts +2 -9
- package/src/modules/code-mode/code-mode.tool.ts +2 -2
- package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +5 -24
- package/src/modules/connection-probe/connection-probe.service.ts +1 -34
- package/src/modules/database/__tests__/session-version-migration.pg.test.ts +133 -0
- package/src/modules/database/core-schema.ts +33 -0
- package/src/modules/declared-variables/__tests__/declared-variables.route.test.ts +44 -1
- package/src/modules/declared-variables/declared-variables.routes.ts +12 -1
- package/src/modules/embed/__tests__/embed.tools.test.ts +12 -0
- package/src/modules/embed/embed.tools.ts +5 -0
- package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +25 -0
- package/src/modules/kb-fs/__tests__/repo-path.test.ts +53 -8
- package/src/modules/kb-fs/index.ts +2 -0
- package/src/modules/kb-fs/locking-filesystem.ts +7 -1
- package/src/modules/kb-fs/repo-path.ts +33 -7
- package/src/modules/kb-fs/tmp-ref.ts +81 -0
- package/src/modules/mcp/__tests__/connect-as.pg.test.ts +137 -0
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +29 -5
- package/src/modules/mcp/__tests__/mcp-routes-harness.ts +8 -0
- package/src/modules/mcp/__tests__/mcp.connect-as.test.ts +195 -0
- package/src/modules/mcp/__tests__/mcp.routes.connect-as.test.ts +192 -0
- package/src/modules/mcp/__tests__/oauth-consent.connect-as.test.ts +146 -0
- package/src/modules/mcp/mcp-auth.middleware.ts +19 -2
- package/src/modules/mcp/mcp.routes.ts +39 -3
- package/src/modules/mcp/mcp.service.ts +112 -52
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +46 -8
- package/src/modules/mcp/oauth/oauth-consent.routes.ts +41 -1
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +38 -2
- package/src/modules/plugins/index.ts +1 -1
- package/src/modules/plugins/plugin-links.ts +19 -2
- package/src/modules/plugins/plugins.routes.ts +35 -4
- package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +113 -0
- package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +430 -3
- package/src/modules/secrets-vault/__tests__/oauth-live-declaration.route.test.ts +218 -0
- package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +39 -6
- package/src/modules/secrets-vault/db-secrets-vault.service.ts +170 -27
- package/src/modules/secrets-vault/index.ts +1 -1
- package/src/modules/secrets-vault/secrets-vault.contract.ts +61 -11
- package/src/modules/secrets-vault/secrets-vault.routes.ts +114 -42
- package/src/modules/settings/oidc-check.ts +5 -18
- package/src/modules/skills/__tests__/skills.service.test.ts +34 -0
- package/src/modules/skills/index.ts +7 -2
- package/src/modules/skills/skills.contract.ts +17 -0
- package/src/modules/skills/skills.service.ts +5 -1
- package/src/modules/skills/skills.tools.ts +45 -14
- package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +4 -0
- package/src/modules/tool-auth/external-api-key.interface.ts +11 -1
- package/src/modules/tool-auth/external-api-key.service.ts +5 -2
- package/src/modules/tool-auth/index.ts +1 -1
- package/src/modules/tool-auth/internal-token.service.ts +10 -0
- package/src/modules/tool-auth/tool-auth.middleware.ts +45 -3
- package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +19 -11
- package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +3 -1
- package/src/modules/tool-helpers/__tests__/route-tool-schemas.catalog.test.ts +38 -0
- package/src/modules/tool-helpers/tool-context.ts +10 -0
- package/src/modules/tool-helpers/tool-def.ts +1 -1
- package/src/modules/tool-helpers/tool-handler.ts +34 -0
- package/src/modules/tool-helpers/tool.contract.ts +10 -0
- package/src/modules/tool-helpers/validate-token.ts +4 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +80 -4
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +197 -11
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +42 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +99 -8
- package/src/modules/tool-manuals/index.ts +6 -2
- package/src/modules/tool-manuals/mcp-json-discovery.ts +73 -34
- package/src/modules/tool-manuals/tool-manuals.contract.ts +40 -6
- package/src/modules/tool-manuals/tool-manuals.routes.ts +40 -3
- package/src/modules/tool-manuals/tool-manuals.service.ts +169 -61
- package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -25
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +6 -113
- package/src/modules/tool-registry/manual.routes.ts +26 -5
- package/src/modules/tool-registry/tool.contract.ts +7 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
- package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +1 -1
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +54 -3
- package/src/modules/workflow/agent-tools/workflow.tools.ts +36 -27
- package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +130 -3
- package/src/modules/workflow/git/git.service.ts +29 -48
- package/src/modules/workflow/workflow.service.ts +5 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -2
- package/src/modules/workspace/__tests__/holds-nul.test.ts +66 -0
- package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +3 -3
- package/src/modules/workspace/__tests__/ripgrep-search.test.ts +570 -0
- package/src/modules/workspace/__tests__/ripgrep-version.test.ts +29 -0
- package/src/modules/workspace/__tests__/workspace-tmp.tools.test.ts +928 -0
- package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +17 -37
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +20 -8
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +10 -12
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +22 -10
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +549 -162
- package/src/modules/workspace/agent-access.gate.ts +1 -1
- package/src/modules/workspace/file-readers/__tests__/doc-extract.service.test.ts +10 -0
- package/src/modules/workspace/file-readers/doc-extract.service.ts +71 -38
- package/src/modules/workspace/index.ts +20 -0
- package/src/modules/workspace/nul-scan.ts +50 -0
- package/src/modules/workspace/pattern-matcher.contract.ts +123 -32
- package/src/modules/workspace/ripgrep-search.ts +715 -0
- package/src/modules/workspace/ripgrep-version.ts +35 -0
- package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +11 -0
- package/src/modules/workspace/startup/beside-checkout.ts +13 -5
- package/src/modules/workspace/worker-pool.ts +3 -4
- package/src/modules/workspace/workspace-tmp.ts +457 -0
- package/src/modules/workspace/workspace.routes.ts +6 -27
- package/src/modules/workspace/workspace.service.ts +92 -15
- package/src/modules/workspace/workspace.tools.ts +574 -176
- package/src/shared/__tests__/read-capped.test.ts +68 -0
- package/src/shared/oauth-token-error.ts +46 -0
- package/src/shared/path-order.ts +37 -8
- package/src/shared/read-capped.ts +40 -0
- package/starter-packs/engineering/Plugins/engineering-starter/access.md +1 -1
- package/starter-packs/general/Plugins/general-starter/access.md +1 -1
- package/starter-packs/operations/Plugins/operations-starter/access.md +1 -1
- package/starter-packs/sales/Plugins/sales-starter/access.md +1 -1
- package/starter-packs/support/Plugins/support-starter/access.md +1 -1
- package/agent-guide/finding-things.md +0 -7
- package/agent-guide/introduction.md +0 -27
- package/dist/modules/workspace/bounded-matcher.d.ts +0 -73
- package/dist/modules/workspace/bounded-matcher.d.ts.map +0 -1
- package/dist/modules/workspace/bounded-matcher.js +0 -200
- package/dist/modules/workspace/bounded-matcher.js.map +0 -1
- package/src/modules/workspace/__tests__/bounded-matcher.test.ts +0 -213
- package/src/modules/workspace/bounded-matcher.ts +0 -271
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import nodeFs from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { isAbsolute, join, relative, sep } from 'node:path';
|
|
3
3
|
import AdmZip from 'adm-zip';
|
|
4
4
|
import { hasHttpStatus, ToolError, BRANCH_INPUT, toolDef, CAN_WRITE_CLAUSE, defaultBranchWriteVerdicts } from '../tool-helpers/index.js';
|
|
5
5
|
import { notifyAgentRead, assertAgentWriteAllowed, SESSION_ID_INPUT, } from './agent-access.gate.js';
|
|
@@ -8,7 +8,7 @@ import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
|
8
8
|
import { GitInternalsError, WorkflowValidationError } from '../../shared/domain-errors.js';
|
|
9
9
|
// Leaf-level shared primitive (same exception `workspace.service.ts` already
|
|
10
10
|
// relies on) — not a workflow service, so this stays inside the module boundary.
|
|
11
|
-
import { assertInsideRepo, assertRepoRootNameFree, assertRepoRootNameFreeArgs, isInsideRepo, normalizePathArgs, normalizeWorkspacePath, } from '../kb-fs/index.js';
|
|
11
|
+
import { assertInsideRepo, assertRepoRootNameFree, assertRepoRootNameFreeArgs, hasControlCharacter, isInsideRepo, isTmpRef, normalizePathArgs, normalizeWorkspacePath, } from '../kb-fs/index.js';
|
|
12
12
|
import { GitGuardedFilesystem, MoveLockedError, MoveRacedError } from '../kb-fs/index.js';
|
|
13
13
|
import { assertNoGitInternalsSegment, assertNotGitInternals, hasGitInternalsSegment } from '../../shared/git-internals.js';
|
|
14
14
|
import { isRolesYamlPath, toKbRelative, resolveReadableMap, AccessDeniedError } from '../access-model/index.js';
|
|
@@ -18,11 +18,13 @@ import { displayPath } from './file-readers/file-reader.js';
|
|
|
18
18
|
import { fileTypeOf, needsContent } from './file-readers/content-mode.js';
|
|
19
19
|
import { createFileReaderRegistry } from './file-readers/file-reader.registry.js';
|
|
20
20
|
import { DocumentReader } from './file-readers/document-reader.js';
|
|
21
|
+
import { TextReader } from './file-readers/text-reader.js';
|
|
21
22
|
import { mcpImageResult } from '@bevel-software/platform-mcp-core';
|
|
22
23
|
import { folderPlaceholderPath, isFolderPlaceholder, isPlatformFile, isPlatformFolder, platformFileCreationRefusal, platformFileRefusal, platformFileUploadRefusal, platformFolderRefusal, isRepositoryOwnFile, repositoryOwnFileDeleteRefusal, entryExistsMessage, } from '@bevel-software/platform-shared';
|
|
23
24
|
import { AGENT_GUIDE_FILE, isAgentGuidePath, isManagedGuide, withPlatformGuideAppended, } from '../agent-guide/index.js';
|
|
24
25
|
import { removeEmptyDirs } from './empty-dirs.js';
|
|
25
|
-
import {
|
|
26
|
+
import { compareSearchPaths, PATTERN_DIALECT, } from './pattern-matcher.contract.js';
|
|
27
|
+
import { holdsNul } from './nul-scan.js';
|
|
26
28
|
import { FIRST_RUN_SECTION_ID, firstRunNote, knowledgeFolderIsNew } from './first-run.js';
|
|
27
29
|
import { planMoveLinks } from './move-links.js';
|
|
28
30
|
import { rethrowAsWriteDenial } from './write-denial.js';
|
|
@@ -36,6 +38,12 @@ import { isSymlinkZipEntry, isZipNoiseEntry, readZipEntry, zipEntryName, zipEntr
|
|
|
36
38
|
const log = logger('workspace-tools');
|
|
37
39
|
/** Types that run scripts wherever they are opened, sent by `request_file_download` as plain bytes. */
|
|
38
40
|
const ACTIVE_CONTENT_TYPES = new Set(['image/svg+xml', 'text/html', 'application/xhtml+xml']);
|
|
41
|
+
/**
|
|
42
|
+
* The caller's verdicts on a file you staged in the workspace's tmp folder: it
|
|
43
|
+
* may be read (and copied out), nothing else — the folder is read-only
|
|
44
|
+
* through the ref, and `request_file_download` does not take one.
|
|
45
|
+
*/
|
|
46
|
+
const TMP_ACCESS = { read: true, write: false, download: false, owner: false };
|
|
39
47
|
/** How many files `file_stat` counts under a folder before it stops and says so. */
|
|
40
48
|
const DESCENDANTS_CAP = 10_000;
|
|
41
49
|
/** How many of a folder's files a `delete_folder` answer names. */
|
|
@@ -67,13 +75,19 @@ async function filterReadableEntries(gate, dir, entries) {
|
|
|
67
75
|
}
|
|
68
76
|
if (fileWsPaths.length === 0)
|
|
69
77
|
return entries;
|
|
70
|
-
const
|
|
71
|
-
return entries.filter((e) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
const readable = await readableFiles(gate, fileWsPaths);
|
|
79
|
+
return entries.filter((e) => e.type === 'directory' || readable.has(dir ? `${dir}/${e.name}` : e.name));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The FILES among `wsPaths` the caller may read, judged in one batched ACL
|
|
83
|
+
* load by the FULL `canReadBatch` (per-node frontmatter honoured), via the
|
|
84
|
+
* shared `resolveReadableMap`. A path the verdict does not name is unreadable.
|
|
85
|
+
*/
|
|
86
|
+
async function readableFiles(gate, wsPaths) {
|
|
87
|
+
if (wsPaths.length === 0)
|
|
88
|
+
return new Set();
|
|
89
|
+
const verdict = await resolveReadableMap((wid, email, rels) => gate.accessControl.canReadBatch(wid, email, rels), gate.workspaceId, gate.userEmail, gate.kbDirName, wsPaths);
|
|
90
|
+
return new Set(wsPaths.filter((p) => verdict.get(p) === true));
|
|
77
91
|
}
|
|
78
92
|
/**
|
|
79
93
|
* Drop the empty-folder placeholder from a directory listing: it keeps a
|
|
@@ -128,6 +142,12 @@ async function keepFolderOf(fs, ctx, branch, removedPath, kbDirName) {
|
|
|
128
142
|
}
|
|
129
143
|
const int = (description) => ({ type: 'integer', description });
|
|
130
144
|
const str = (description) => ({ type: 'string', description });
|
|
145
|
+
/**
|
|
146
|
+
* A write to the default branch is live at once, so the agent asks first. Said on the four
|
|
147
|
+
* tools behind 87 of the 91 writes there in the agent-guide benchmark (2026-10-26); `start-here`
|
|
148
|
+
* says it for the other write tools.
|
|
149
|
+
*/
|
|
150
|
+
const ASK_BEFORE_DEFAULT_BRANCH = ' On the default branch a write is live for everyone at once: unless the person already said where, ask them first whether to write there or propose it on a draft.';
|
|
131
151
|
/**
|
|
132
152
|
* The upload route, named on every tool that takes content as a JSON string.
|
|
133
153
|
*
|
|
@@ -180,6 +200,30 @@ const RESERVED_ROOT_NAME_TARGETS = {
|
|
|
180
200
|
// inside the handler — an archive chooses its entry names, not the caller.
|
|
181
201
|
apply_file_upload: ['destination'],
|
|
182
202
|
};
|
|
203
|
+
/**
|
|
204
|
+
* The inputs that take a tmp ref (`__tmp__/<path>`, see `workspace-tmp.ts`)
|
|
205
|
+
* beside a repository path — the four reads of the one file it names. Every
|
|
206
|
+
* other tool, and every other input of these four (`copy_file`'s `dest`
|
|
207
|
+
* among them), refuses a ref before anything else runs — in
|
|
208
|
+
* `normalizePathArgs`, through the one admission gate (`kb-fs/tmp-ref.ts`):
|
|
209
|
+
* the folder is read-only through the ref, and `copy_file` to a repository
|
|
210
|
+
* path is how a file is kept.
|
|
211
|
+
*/
|
|
212
|
+
const TMP_REF_INPUTS = {
|
|
213
|
+
read_file: ['path'],
|
|
214
|
+
file_stat: ['path'],
|
|
215
|
+
grep: ['path'],
|
|
216
|
+
copy_file: ['src'],
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* A path input that also takes a tmp ref (see `TMP_REF_INPUTS`): said on the
|
|
220
|
+
* input, because the descriptions of the tools that take one have no room
|
|
221
|
+
* left, and what the ref is is in the shared rules.
|
|
222
|
+
*/
|
|
223
|
+
const orTmpRef = (schema) => ({
|
|
224
|
+
...schema,
|
|
225
|
+
description: `${schema.description ?? ''} Or a \`__tmp__/…\` ref to one file you staged in the workspace's tmp folder.`,
|
|
226
|
+
});
|
|
183
227
|
function asText(content) {
|
|
184
228
|
return typeof content === 'string' ? content : content.toString('utf8');
|
|
185
229
|
}
|
|
@@ -467,35 +511,49 @@ function modeOf(a) {
|
|
|
467
511
|
return a.mode;
|
|
468
512
|
throw new ToolError(`"${String(a.mode)}" is not a write mode: use ${WRITE_MODES.map((m) => `\`${m}\``).join(', ')} (default \`create\`).`, 400, { code: 'bad_mode' });
|
|
469
513
|
}
|
|
514
|
+
/** The longest stretch of a matching line a grep answer carries. */
|
|
515
|
+
const GREP_LINE_CHARS = 300;
|
|
516
|
+
/** {@link searchableText}'s `bytesOf` for a repository path: the workspace filesystem's own read. */
|
|
517
|
+
function fileBytes(fs) {
|
|
518
|
+
return async (path) => asBytes(await fs.readFile(path));
|
|
519
|
+
}
|
|
470
520
|
/**
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
521
|
+
* Files the text pass judges, then searches, at a time: first this many, then
|
|
522
|
+
* twice as many each time, up to {@link GREP_MAX_BATCH}. A verdict reads the
|
|
523
|
+
* file's own access rules, so a batch costs one read of each of its files.
|
|
524
|
+
* Its names also go to ripgrep on the command line, so a batch also ends
|
|
525
|
+
* once they reach {@link GREP_BATCH_CHARS}, well inside the system's limit.
|
|
526
|
+
*/
|
|
527
|
+
const GREP_FIRST_BATCH = 256;
|
|
528
|
+
const GREP_MAX_BATCH = 2_048;
|
|
529
|
+
const GREP_BATCH_CHARS = 256 * 1024;
|
|
530
|
+
/**
|
|
531
|
+
* The text ONE file holds for `grep`, read through its reader: the text
|
|
532
|
+
* content for a text reader (none on NUL bytes / invalid UTF-8 — binary is not
|
|
533
|
+
* searchable), the EXTRACTION for a document reader (marker line included, so
|
|
534
|
+
* the `[slide N]`/`[sheet: …]`/`[page N]` lines are themselves searchable and
|
|
535
|
+
* line numbers match what read_file returns), nothing for images.
|
|
480
536
|
*
|
|
481
537
|
* Throws whatever reading the file throws — the caller decides whether that is
|
|
482
|
-
* a file to skip (
|
|
538
|
+
* a file to skip (a tree search) or an error to surface (a single-file grep).
|
|
539
|
+
* `bytesOf` is where the file's bytes come from: the workspace filesystem for
|
|
540
|
+
* a repository path, the tmp resolver for a tmp ref.
|
|
483
541
|
*/
|
|
484
|
-
async function
|
|
542
|
+
async function searchableText(bytesOf, path, docs) {
|
|
485
543
|
const reader = docs.readers.readerFor(path);
|
|
486
|
-
const bytes =
|
|
544
|
+
const bytes = await bytesOf(path);
|
|
487
545
|
let content;
|
|
488
546
|
if (reader instanceof DocumentReader) {
|
|
489
547
|
// What the cache remembers is free — an extraction is searched, a failure
|
|
490
548
|
// (a damaged file, or one that overran its budget within the hour) is
|
|
491
549
|
// skipped without extracting it again. A cold document draws on the
|
|
492
|
-
// per-
|
|
550
|
+
// per-call budget (see UNCACHED_DOCS_PER_GREP) — beyond it the search
|
|
493
551
|
// skips the document and counts it.
|
|
494
552
|
let res = await reader.cachedRead(bytes, path);
|
|
495
553
|
if (res === undefined) {
|
|
496
554
|
if (docs.uncachedBudget <= 0) {
|
|
497
555
|
docs.skippedUncached++;
|
|
498
|
-
return 'budget-skipped';
|
|
556
|
+
return { kind: 'budget-skipped' };
|
|
499
557
|
}
|
|
500
558
|
docs.uncachedBudget--;
|
|
501
559
|
res = await reader.read(bytes, path, docs.origin);
|
|
@@ -506,15 +564,41 @@ async function grepOneFile(fs, path, matcher, out, max, docs) {
|
|
|
506
564
|
else {
|
|
507
565
|
content = reader.greppableText ? await reader.greppableText(bytes, path) : null;
|
|
508
566
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
567
|
+
return content === null ? { kind: 'no-text' } : { kind: 'text', text: content };
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* grep over the one text file `path`, by ripgrep from the folder holding it,
|
|
571
|
+
* its lines pushed onto `out`, when it is a regular file; `null`, with
|
|
572
|
+
* nothing searched, for anything else (a link, one that loops, a file gone
|
|
573
|
+
* since), which is read as read_file reads it instead. A file that answers no
|
|
574
|
+
* line has no text when ripgrep finds it binary: it holds a NUL, which
|
|
575
|
+
* {@link holdsNul} looks for in the very file that was judged.
|
|
576
|
+
*/
|
|
577
|
+
async function grepTextFile(fs, path, search, max, out) {
|
|
578
|
+
const slash = path.lastIndexOf('/');
|
|
579
|
+
const scope = await treeScope(fs, slash < 0 ? '' : path.slice(0, slash));
|
|
580
|
+
const name = path.slice(slash + 1);
|
|
581
|
+
const absolute = join(scope.cwd, scope.root, name);
|
|
582
|
+
const judged = await nodeFs.lstat(absolute).catch(() => null);
|
|
583
|
+
if (judged === null || !judged.isFile())
|
|
584
|
+
return null;
|
|
585
|
+
const from = out.length;
|
|
586
|
+
for await (const file of search.files(scope, { only: [name], maxLinesPerFile: max })) {
|
|
587
|
+
for (const l of file.lines)
|
|
588
|
+
out.push({ path, line: l.line, text: l.text.slice(0, GREP_LINE_CHARS) });
|
|
516
589
|
}
|
|
517
|
-
|
|
590
|
+
if (out.length > from)
|
|
591
|
+
return { kind: 'searched' };
|
|
592
|
+
const binary = await orNotFound(path, () => holdsNul(absolute, judged, search), 'Nothing to search');
|
|
593
|
+
return binary ? { kind: 'no-text' } : { kind: 'searched' };
|
|
594
|
+
}
|
|
595
|
+
/** Append the lines `hits` names (0-based indices into `text`'s lines) to `out`, as matches in `path`. */
|
|
596
|
+
function pushMatches(out, path, text, hits) {
|
|
597
|
+
if (hits.length === 0)
|
|
598
|
+
return;
|
|
599
|
+
const lines = text.split('\n');
|
|
600
|
+
for (const i of hits)
|
|
601
|
+
out.push({ path, line: i + 1, text: lines[i].slice(0, GREP_LINE_CHARS) });
|
|
518
602
|
}
|
|
519
603
|
/**
|
|
520
604
|
* What a grep's `path` actually names. The walk starts with `readdir`, which
|
|
@@ -548,55 +632,145 @@ async function searchRootKind(fs, path) {
|
|
|
548
632
|
return 'file';
|
|
549
633
|
}
|
|
550
634
|
}
|
|
551
|
-
/**
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
635
|
+
/**
|
|
636
|
+
* Where ripgrep searches the directory `root`: from the workspace's canonical
|
|
637
|
+
* directory, with the root's canonical path relative to it. The stat that
|
|
638
|
+
* called `root` a directory already refused one outside the workspace; this
|
|
639
|
+
* refuses it again where the search is started, so ripgrep, which follows the
|
|
640
|
+
* root it is given, is never started on anything outside.
|
|
641
|
+
*/
|
|
642
|
+
async function treeScope(fs, root) {
|
|
643
|
+
const cwd = await nodeFs.realpath(fs.basePath);
|
|
644
|
+
const absolute = fs.resolveAbsolutePath(root);
|
|
645
|
+
const rel = absolute === undefined ? '..' : relative(cwd, await nodeFs.realpath(absolute));
|
|
646
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
647
|
+
throw new ToolError(`"${displayPath(root)}" is outside the workspace.`, 403);
|
|
563
648
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
649
|
+
return { cwd, root: rel === '' ? '.' : rel.split(sep).join('/') };
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* grep over the directory `root`: the tree is listed, then searched in two
|
|
653
|
+
* passes, and no file is opened before it is admitted.
|
|
654
|
+
*
|
|
655
|
+
* The listing opens no file. Admission is the ONE step a file passes before
|
|
656
|
+
* either pass opens it: it is judged readable by the gate, and then — being a
|
|
657
|
+
* file the search will read, as `read_file` reads one — it is told to the
|
|
658
|
+
* read hook, which may refuse it, and with it the search. A file the caller
|
|
659
|
+
* may not read is never opened and never told of (there is no read to tell):
|
|
660
|
+
* what it holds can change neither the answer nor how long the search takes,
|
|
661
|
+
* and so not whether it ends within its budget. Judging and telling are both
|
|
662
|
+
* spent from that budget. A file that was not in the listing (it appeared
|
|
663
|
+
* since) is never admitted, so never searched.
|
|
664
|
+
*
|
|
665
|
+
* The text pass is ripgrep's own: every file whose reader greps its bytes as
|
|
666
|
+
* text (a TextReader) is searched by it, in path order, and read nowhere else.
|
|
667
|
+
* It admits a batch at a time, before ripgrep opens any of it, so the hook
|
|
668
|
+
* hears of each file of a batch whether or not a line of it is answered —
|
|
669
|
+
* and, past the line that fills the cap, of the rest of that batch, which the
|
|
670
|
+
* pass then never opens. Each file is answered as ripgrep finishes it, and
|
|
671
|
+
* the pass stops at the `max`-th line.
|
|
672
|
+
*
|
|
673
|
+
* The document pass reads the files every other reader owns through that
|
|
674
|
+
* reader, as `read_file` does — extractions under the per-call budget — and
|
|
675
|
+
* has their text matched by the same engine. A document that sorts after the
|
|
676
|
+
* text pass's last line cannot be among the first `max`, so it is not read.
|
|
677
|
+
*
|
|
678
|
+
* Answers what both found, unsorted: the caller merges, sorts and caps.
|
|
679
|
+
*/
|
|
680
|
+
async function grepTree(fs, root, search, max, gate, notifyRead, docs,
|
|
681
|
+
/** A file the search leaves to its caller: never answered, never counted against `max`. */
|
|
682
|
+
skip = () => false) {
|
|
683
|
+
const scope = await treeScope(fs, root);
|
|
684
|
+
// A folder named with a trailing slash answers the paths it is spelled
|
|
685
|
+
// with everywhere else (`a/b.md`, never `a//b.md`), so the guide's own
|
|
686
|
+
// path and every verdict meet the one spelling.
|
|
687
|
+
const base = root.replace(/\/+$/, '');
|
|
688
|
+
const at = (p) => (base === '' ? p : `${base}/${p}`);
|
|
689
|
+
// ripgrep already stays out of `.git`; the folder's every other spelling,
|
|
690
|
+
// and the folder placeholder, are left out as everywhere else.
|
|
691
|
+
const shown = (p) => !hasGitInternalsSegment(p) && !isFolderPlaceholder(p) && !skip(p);
|
|
692
|
+
// Which reader owns an extension is the registry's answer, asked of a name
|
|
693
|
+
// carrying it; a TextReader greps the bytes themselves, which is ripgrep's
|
|
694
|
+
// pass. Extensions any other reader owns are left out of it.
|
|
695
|
+
const readerOf = (ext) => docs.readers.readerFor(`file${ext}`);
|
|
696
|
+
const notRaw = docs.readers.ownedExtensions().filter((ext) => !(readerOf(ext) instanceof TextReader));
|
|
697
|
+
const ownedBy = (p, exts) => {
|
|
698
|
+
const lower = p.toLowerCase();
|
|
699
|
+
return exts.some((ext) => lower.endsWith(ext));
|
|
700
|
+
};
|
|
701
|
+
const readerOwned = notRaw.filter((ext) => {
|
|
702
|
+
const reader = readerOf(ext);
|
|
703
|
+
return reader instanceof DocumentReader || reader.greppableText !== undefined;
|
|
704
|
+
});
|
|
705
|
+
const out = [];
|
|
706
|
+
// Admission (see above): the gate's verdict on `paths`, then the hook told
|
|
707
|
+
// of each readable one in path order — a refusal ends the search here, as
|
|
708
|
+
// it ends a `read_file`, before anything is opened. Spent from the budget.
|
|
709
|
+
const admit = (paths) => search.charged(async () => {
|
|
710
|
+
const readable = await readableFiles(gate, paths);
|
|
711
|
+
for (const p of paths) {
|
|
712
|
+
if (readable.has(p))
|
|
713
|
+
await notifyRead(p);
|
|
577
714
|
}
|
|
578
|
-
|
|
579
|
-
|
|
715
|
+
return readable;
|
|
716
|
+
});
|
|
717
|
+
// Every file the walk covers, in path order. Only what is shown is ever
|
|
718
|
+
// admitted, so what is admitted is what may be answered. A name holding a
|
|
719
|
+
// control character is no file name to the platform — no tool reads or
|
|
720
|
+
// writes one — so it is not searched either.
|
|
721
|
+
const all = (await search.list(scope)).filter((p) => shown(at(p)) && !hasControlCharacter(p));
|
|
722
|
+
const raw = all.filter((p) => !ownedBy(p, notRaw));
|
|
723
|
+
// The text pass: a batch of files is admitted, then its admitted files, and
|
|
724
|
+
// only they, are searched. A batch is admitted before ripgrep opens any of
|
|
725
|
+
// it, and the pass stops at the `max`-th line, so the verdicts asked for are
|
|
726
|
+
// those of the files the answer reached — few for a common word — and the
|
|
727
|
+
// batches double so a rare one still walks the tree in a handful of runs.
|
|
728
|
+
let size = GREP_FIRST_BATCH;
|
|
729
|
+
for (let from = 0; from < raw.length && out.length < max; size = Math.min(size * 2, GREP_MAX_BATCH)) {
|
|
730
|
+
const batch = [];
|
|
731
|
+
for (let chars = 0; from < raw.length && batch.length < size && (batch.length === 0 || chars < GREP_BATCH_CHARS); from++) {
|
|
732
|
+
batch.push(raw[from]);
|
|
733
|
+
chars += raw[from].length;
|
|
580
734
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
// must not fail a search over the whole tree.
|
|
588
|
-
// A pattern too expensive to finish, or no worker free to run it, is not
|
|
589
|
-
// an unreadable file: either ends the whole search.
|
|
590
|
-
try {
|
|
591
|
-
await grepOneFile(fs, p, matcher, out, max, docs);
|
|
592
|
-
}
|
|
593
|
-
catch (err) {
|
|
594
|
-
if (endsTheSearch(err))
|
|
595
|
-
throw err;
|
|
735
|
+
const readable = await admit(batch.map(at));
|
|
736
|
+
const only = batch.filter((p) => readable.has(at(p)));
|
|
737
|
+
for await (const file of search.files(scope, { only, maxLinesPerFile: max - out.length })) {
|
|
738
|
+
const p = at(file.path);
|
|
739
|
+
// Only what was admitted answers, whatever ripgrep printed.
|
|
740
|
+
if (!readable.has(p))
|
|
596
741
|
continue;
|
|
742
|
+
for (const l of file.lines.slice(0, max - out.length)) {
|
|
743
|
+
out.push({ path: p, line: l.line, text: l.text.slice(0, GREP_LINE_CHARS) });
|
|
597
744
|
}
|
|
745
|
+
// Enough: leaving the loop kills ripgrep at the file that filled the cap.
|
|
746
|
+
if (out.length >= max)
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const last = out.length >= max ? out[out.length - 1].path : undefined;
|
|
751
|
+
const candidates = all
|
|
752
|
+
.filter((p) => ownedBy(p, readerOwned))
|
|
753
|
+
.map(at)
|
|
754
|
+
.filter((p) => last === undefined || compareSearchPaths(p, last) <= 0);
|
|
755
|
+
const readable = await admit(candidates);
|
|
756
|
+
const texts = [];
|
|
757
|
+
for (const p of candidates) {
|
|
758
|
+
if (!readable.has(p))
|
|
759
|
+
continue;
|
|
760
|
+
let found;
|
|
761
|
+
try {
|
|
762
|
+
found = await searchableText(fileBytes(fs), p, docs);
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
// One document that cannot be read must not fail a search over the tree.
|
|
766
|
+
continue;
|
|
598
767
|
}
|
|
768
|
+
if (found.kind === 'text')
|
|
769
|
+
texts.push({ path: p, text: found.text });
|
|
599
770
|
}
|
|
771
|
+
const hits = await search.matchingLines(texts.map((t) => t.text));
|
|
772
|
+
texts.forEach((t, i) => pushMatches(out, t.path, t.text, hits[i]));
|
|
773
|
+
return out;
|
|
600
774
|
}
|
|
601
775
|
const SAVE_WARNINGS_OUTPUT = {
|
|
602
776
|
type: 'array',
|
|
@@ -661,8 +835,9 @@ changeGate,
|
|
|
661
835
|
*/
|
|
662
836
|
uploads,
|
|
663
837
|
/**
|
|
664
|
-
* The platform's agent guide
|
|
665
|
-
*
|
|
838
|
+
* The start of the platform's agent guide and the list of its sections
|
|
839
|
+
* (`guideStartText` in `modules/agent-guide`), which `read_file` and
|
|
840
|
+
* `file_stat` answer at the guide's name in the repository root — after
|
|
666
841
|
* the knowledge base's own file of that name, when it has one. Optional for
|
|
667
842
|
* the harnesses that are about the file primitives; without it the two
|
|
668
843
|
* tools read the disk and nothing else.
|
|
@@ -842,7 +1017,9 @@ patternMatcher) {
|
|
|
842
1017
|
const paths = toolPathArgs(args);
|
|
843
1018
|
for (const p of paths)
|
|
844
1019
|
assertNoGitInternalsSegment(p);
|
|
845
|
-
|
|
1020
|
+
// A spill ref belongs to no workspace, and a tmp ref to the folder beside
|
|
1021
|
+
// the checkout: neither is judged against the checkout's git folder.
|
|
1022
|
+
const onDisk = paths.filter((p) => !spillStore.isSpillRef(p) && !isTmpRef(p));
|
|
846
1023
|
if (onDisk.length === 0)
|
|
847
1024
|
return;
|
|
848
1025
|
const root = await gitCheckRootFor(args, ctx);
|
|
@@ -896,9 +1073,14 @@ patternMatcher) {
|
|
|
896
1073
|
* caller WILL have, never whether they may do it. The write verdicts that
|
|
897
1074
|
* gate the move are `writeBlocked` and the lock gate, both of which read
|
|
898
1075
|
* the tree as it is.
|
|
1076
|
+
*
|
|
1077
|
+
* `sourceContent` is the text of a source that is no repository path (a
|
|
1078
|
+
* tmp ref): it carries rules in exactly as a repository file would, so it
|
|
1079
|
+
* is previewed through the same relocation reading, never as "nothing
|
|
1080
|
+
* travels".
|
|
899
1081
|
*/
|
|
900
1082
|
const accessAfter = async (branch, ctx, src, dest, opts) => {
|
|
901
|
-
const from = toKbRelative(src, kbDirName);
|
|
1083
|
+
const from = opts?.sourceContent !== undefined ? src : toKbRelative(src, kbDirName);
|
|
902
1084
|
const to = toKbRelative(dest, kbDirName);
|
|
903
1085
|
// Outside the repository there are no rules to carry, and none to land
|
|
904
1086
|
// among — the same answer `accessAt` gives for such a path.
|
|
@@ -906,6 +1088,16 @@ patternMatcher) {
|
|
|
906
1088
|
return accessAt(branch, ctx, dest);
|
|
907
1089
|
return accessControl.previewAccessAfterRelocation(workspaceIdForBranch(branch), ctx.user.email, from, to, opts);
|
|
908
1090
|
};
|
|
1091
|
+
/**
|
|
1092
|
+
* The text a tmp source carries into the repository, for the preview only:
|
|
1093
|
+
* read as a repository source is read there — a file that cannot be read
|
|
1094
|
+
* carries no rules — so the preview never answers before the write
|
|
1095
|
+
* verdict. Whether the file is there at all is `statTmpFile`'s to say.
|
|
1096
|
+
*/
|
|
1097
|
+
const tmpSourceText = (branch, ctx, ref) => ctx.workspaceService
|
|
1098
|
+
.readTmpFile(workspaceIdForBranch(branch), ctx.user.id, ref)
|
|
1099
|
+
.then((bytes) => bytes.toString('utf-8'))
|
|
1100
|
+
.catch(() => '');
|
|
909
1101
|
/**
|
|
910
1102
|
* What `copy_file`'s dry run answers: the same impact shape `move_file`
|
|
911
1103
|
* previews, over a copy's own rules.
|
|
@@ -933,10 +1125,16 @@ patternMatcher) {
|
|
|
933
1125
|
* them nothing they could not ask `file_stat` for.
|
|
934
1126
|
*/
|
|
935
1127
|
const copyImpact = async (branch, ctx, src, dest) => {
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1128
|
+
const fromTmp = isTmpRef(src);
|
|
1129
|
+
// The write verdict FIRST, and alone: a staged file's bytes are read only
|
|
1130
|
+
// for a caller who may write the destination. A denied preview carries
|
|
1131
|
+
// no rules from the source ('' — what an unreadable source carries), so
|
|
1132
|
+
// its `access.after` is the destination's own rules for the caller.
|
|
1133
|
+
const blocked = await writeBlocked(branch, ctx, [dest]);
|
|
1134
|
+
const sourceContent = fromTmp ? (blocked.length > 0 ? '' : await tmpSourceText(branch, ctx, src)) : undefined;
|
|
1135
|
+
const [before, after] = await Promise.all([
|
|
1136
|
+
fromTmp ? TMP_ACCESS : accessAt(branch, ctx, src),
|
|
1137
|
+
accessAfter(branch, ctx, src, dest, { sourceRemains: true, sourceContent }),
|
|
940
1138
|
]);
|
|
941
1139
|
const access = { before, after };
|
|
942
1140
|
const accessChanges = verbsDiffer(before, after);
|
|
@@ -953,7 +1151,11 @@ patternMatcher) {
|
|
|
953
1151
|
};
|
|
954
1152
|
}
|
|
955
1153
|
const fs = await ctx.getFilesystem(branch);
|
|
956
|
-
|
|
1154
|
+
// A tmp ref names one file by rule: resolving it is the whole look, and
|
|
1155
|
+
// a folder or nothing there is refused by the resolver itself.
|
|
1156
|
+
const kind = fromTmp
|
|
1157
|
+
? (await orNotFound(src, () => ctx.workspaceService.statTmpFile(workspaceIdForBranch(branch), ctx.user.id, src), 'Nothing to copy'), 'file')
|
|
1158
|
+
: await kindOf(fs, src);
|
|
957
1159
|
if (kind === null)
|
|
958
1160
|
throw notFound(src, 'Nothing to copy');
|
|
959
1161
|
const srcFiles = kind === 'folder' ? (await filesUnder(fs, src)).files : [src];
|
|
@@ -978,6 +1180,39 @@ patternMatcher) {
|
|
|
978
1180
|
copied: false,
|
|
979
1181
|
};
|
|
980
1182
|
};
|
|
1183
|
+
/**
|
|
1184
|
+
* `copy_file` out of the workspace's tmp folder: the bytes the ref names
|
|
1185
|
+
* (through the one tmp resolver), landed at `dest` by the ordinary write —
|
|
1186
|
+
* the lock, the pre-disk validation, the creator's read grant for a new
|
|
1187
|
+
* folder and the commit every write to the repository takes — and
|
|
1188
|
+
* exclusively, so a name taken since the handler's look is refused with
|
|
1189
|
+
* the same sentence rather than replaced. From then on the copy is a
|
|
1190
|
+
* repository file like any other; the staged file is left where it was.
|
|
1191
|
+
*
|
|
1192
|
+
* A missing source is the ordinary 404 naming the ref, asked only after
|
|
1193
|
+
* the write verdict (as for any copy); an absence met while landing is the
|
|
1194
|
+
* destination's.
|
|
1195
|
+
*/
|
|
1196
|
+
const copyFromTmp = async (fs, branch, ctx, src, dest) => {
|
|
1197
|
+
const bytes = await orNotFound(src, () => ctx.workspaceService.readTmpFile(workspaceIdForBranch(branch), ctx.user.id, src), 'Nothing to copy');
|
|
1198
|
+
try {
|
|
1199
|
+
// Exclusive, under the destination's lock: a name taken since the look
|
|
1200
|
+
// is refused there as `DestinationTakenError` — releasing the lock
|
|
1201
|
+
// untouched, so the winner's queued file stays — and answered as a
|
|
1202
|
+
// repository copy's lost race is (`asEntryExists`).
|
|
1203
|
+
await asEntryExists(() => fs.writeFile(dest, bytes, { overwrite: false }));
|
|
1204
|
+
}
|
|
1205
|
+
catch (err) {
|
|
1206
|
+
if (err instanceof ToolError)
|
|
1207
|
+
throw err;
|
|
1208
|
+
const occupiedBy = await existingAt(await workspaceRoot(branch, ctx), dest);
|
|
1209
|
+
if (occupiedBy !== null)
|
|
1210
|
+
throw new ToolError(entryExistsMessage(occupiedBy, dest), 409);
|
|
1211
|
+
if (isAbsence(err))
|
|
1212
|
+
throw notFound(dest, 'Nowhere to copy to');
|
|
1213
|
+
throw err;
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
981
1216
|
/**
|
|
982
1217
|
* The paths among `paths` the caller may NOT write, judged exactly as the
|
|
983
1218
|
* lock gate judges them (`WorkflowService.acquireLock`): on a protected
|
|
@@ -1342,11 +1577,21 @@ patternMatcher) {
|
|
|
1342
1577
|
// spill ref; for any other, `__tool_chain_spill__/x` is an ordinary
|
|
1343
1578
|
// path, and exempting it there would be a workspace-relative path that
|
|
1344
1579
|
// never reached the repository — the whole bug, spelled with a prefix.
|
|
1580
|
+
//
|
|
1581
|
+
// The other is a tmp ref (`__tmp__/…`), left as written in exactly the
|
|
1582
|
+
// inputs `TMP_REF_INPUTS` names and refused in every other one first,
|
|
1583
|
+
// so no tool sees one it does not resolve itself.
|
|
1345
1584
|
toolHandler(async (args, ctx) => {
|
|
1346
1585
|
// BEFORE the normaliser: see `assertToolPathsNotGitInternals`.
|
|
1347
1586
|
if (spec.fileTool !== false)
|
|
1348
1587
|
await assertToolPathsNotGitInternals(args, ctx);
|
|
1349
|
-
|
|
1588
|
+
// The normaliser admits a tmp ref in exactly the inputs
|
|
1589
|
+
// `TMP_REF_INPUTS` names and refuses it in every other one first.
|
|
1590
|
+
const normalized = normalizePathArgs(args, kbDirName, {
|
|
1591
|
+
tool: spec.name,
|
|
1592
|
+
tmpRefInputs: TMP_REF_INPUTS[spec.name],
|
|
1593
|
+
skip: spec.name === 'read_file' ? (v) => spillStore.isSpillRef(v) : undefined,
|
|
1594
|
+
});
|
|
1350
1595
|
// The checkout's own name is reserved at the repository root on this
|
|
1351
1596
|
// surface too. The routes meet that rule inside `WorkspaceService`;
|
|
1352
1597
|
// these tools write through the locking filesystem, which never
|
|
@@ -1398,13 +1643,13 @@ patternMatcher) {
|
|
|
1398
1643
|
// transport hiccup on its first call as an unrecoverable start.
|
|
1399
1644
|
const startSessionDef = toolDef({
|
|
1400
1645
|
name: 'start_session',
|
|
1401
|
-
description: 'Mint this conversation\'s id: the `sessionId` KnowledgeBase tools take. Call this ONCE, at the start of your work — minting a new id mid-run starts a second conversation as far as the server is concerned.
|
|
1646
|
+
description: 'Mint this conversation\'s id: the `sessionId` KnowledgeBase tools take. Call this ONCE, at the start of your work — minting a new id mid-run starts a second conversation as far as the server is concerned. Pass the returned id as `sessionId` to every tool whose arguments list it, inside `call_tool_chain` too; the others refuse it. Where the deployment offers an `ask` tool, hand it the SAME id: your reads and your questions are then one conversation. RETRYING IS SAFE: a call that fails created nothing, so retry it. If a retry lands after a success you hold two independent ids, which is harmless: keep passing the one you already used and ignore the other. Returns `{ sessionId }`, plus `firstRun` while the knowledge base is still empty: a note to act on.',
|
|
1402
1647
|
path: '/api/agent/tools/start_session',
|
|
1403
1648
|
inputs: { type: 'object', properties: {}, additionalProperties: false },
|
|
1404
1649
|
outputs: {
|
|
1405
1650
|
type: 'object',
|
|
1406
1651
|
properties: {
|
|
1407
|
-
sessionId: str('The minted session id — pass it as `sessionId`
|
|
1652
|
+
sessionId: str('The minted session id — pass it as `sessionId` to the tools that take one, and to `ask` where there is one.'),
|
|
1408
1653
|
firstRun: str(`Present only while the knowledge base holds nothing but its starter guide (and a starter pack's untouched pages): what to offer the person (the guide's \`${FIRST_RUN_SECTION_ID}\` section says how).`),
|
|
1409
1654
|
},
|
|
1410
1655
|
required: ['sessionId'],
|
|
@@ -1497,6 +1742,61 @@ patternMatcher) {
|
|
|
1497
1742
|
* copy drifts on the first change to any of them.
|
|
1498
1743
|
*/
|
|
1499
1744
|
const readForTool = async (branch, p, ctx) => (await readServed(branch, p, ctx)).result;
|
|
1745
|
+
/**
|
|
1746
|
+
* What a tmp ref (see `workspace-tmp.ts`) reads as: the bytes of the one
|
|
1747
|
+
* file it names in the CALLER's own folder of the branch workspace's `tmp/`
|
|
1748
|
+
* — through `readTmpFile`, the resolver a distribution's tool reads it with
|
|
1749
|
+
* — put through the SAME reader a repository file of that name gets. No
|
|
1750
|
+
* access gate and no read hook: the folder is outside the repository, so
|
|
1751
|
+
* no rule governs it and no knowledge-base path is read; what keeps one
|
|
1752
|
+
* person's staged file from another is that the ref resolves inside the
|
|
1753
|
+
* caller's folder alone. A missing file — somebody else's included — is the
|
|
1754
|
+
* ordinary 404, naming the ref.
|
|
1755
|
+
*/
|
|
1756
|
+
const readTmp = async (branch, ref, ctx) => {
|
|
1757
|
+
const workspaceId = workspaceIdForBranch(branch);
|
|
1758
|
+
const bytes = await orNotFound(ref, () => ctx.workspaceService.readTmpFile(workspaceId, ctx.user.id, ref));
|
|
1759
|
+
return readers.readerFor(ref).read(bytes, ref, { workspaceId });
|
|
1760
|
+
};
|
|
1761
|
+
/**
|
|
1762
|
+
* `file_stat` on a tmp ref: the shape a repository file's stat has, its
|
|
1763
|
+
* content fields decided by the same reader as {@link readTmp} reads with —
|
|
1764
|
+
* and nothing to DO with it but read and copy out. Not written, moved,
|
|
1765
|
+
* deleted or downloaded through these tools (`managed` is false: the
|
|
1766
|
+
* platform's own files are a repository notion), and so not text-editable
|
|
1767
|
+
* either, whatever its content.
|
|
1768
|
+
*/
|
|
1769
|
+
const statTmp = async (branch, ref, ctx) => {
|
|
1770
|
+
const workspaceId = workspaceIdForBranch(branch);
|
|
1771
|
+
const stat = await orNotFound(ref, () => ctx.workspaceService.statTmpFile(workspaceId, ctx.user.id, ref));
|
|
1772
|
+
const reader = readers.readerFor(ref);
|
|
1773
|
+
// One read of the file at most: the type decision and the remembered
|
|
1774
|
+
// extraction below both want the bytes, and a staged document can be large.
|
|
1775
|
+
let bytes;
|
|
1776
|
+
const bytesOf = async () => (bytes ??= await orNotFound(ref, () => ctx.workspaceService.readTmpFile(workspaceId, ctx.user.id, ref)));
|
|
1777
|
+
const type = fileTypeOf(reader, ref, needsContent(reader) ? await bytesOf() : undefined);
|
|
1778
|
+
const out = {
|
|
1779
|
+
...stat,
|
|
1780
|
+
path: ref,
|
|
1781
|
+
type: 'file',
|
|
1782
|
+
managed: false,
|
|
1783
|
+
movable: false,
|
|
1784
|
+
deletable: false,
|
|
1785
|
+
access: TMP_ACCESS,
|
|
1786
|
+
canWrite: false,
|
|
1787
|
+
...type,
|
|
1788
|
+
textEditable: false,
|
|
1789
|
+
};
|
|
1790
|
+
// A document whose extraction failed answers that failure's notice, as a
|
|
1791
|
+
// repository document does: from what the cache remembers, never by
|
|
1792
|
+
// extracting.
|
|
1793
|
+
if (reader instanceof DocumentReader) {
|
|
1794
|
+
const remembered = await reader.cachedRead(await bytesOf(), ref);
|
|
1795
|
+
if (remembered?.kind === 'refusal')
|
|
1796
|
+
return { ...out, unreadable: remembered.message };
|
|
1797
|
+
}
|
|
1798
|
+
return out;
|
|
1799
|
+
};
|
|
1500
1800
|
/**
|
|
1501
1801
|
* {@link readForTool}, also saying, at the guide's name, whether a file of
|
|
1502
1802
|
* the knowledge base's own answered first on THIS branch (`guide`: `own`)
|
|
@@ -1550,7 +1850,7 @@ patternMatcher) {
|
|
|
1550
1850
|
description: 'Read a workspace file as text. Returns `{ path, content, canWrite }`. Per the shared content rule: text as ' +
|
|
1551
1851
|
'text, documents and email as extracted text, an image as the picture, anything else as a one-line ' +
|
|
1552
1852
|
'description. Also reads a `__tool_chain_spill__/…` ref back from a truncated `call_tool_chain`: it ' +
|
|
1553
|
-
'belongs to no workspace
|
|
1853
|
+
'belongs to no workspace: any existing `branch` will do, and no `canWrite` comes back. `offset`/`limit` slice the ' +
|
|
1554
1854
|
'content (characters; bytes for a spill ref; ignored for an image): page through large files and ' +
|
|
1555
1855
|
'spills, never read multi-MB whole. `canWrite`: may you change this file directly on the default branch. ' +
|
|
1556
1856
|
CAN_WRITE_CLAUSE,
|
|
@@ -1558,9 +1858,9 @@ patternMatcher) {
|
|
|
1558
1858
|
type: 'object',
|
|
1559
1859
|
properties: {
|
|
1560
1860
|
branch: BRANCH_INPUT,
|
|
1561
|
-
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
|
|
1562
|
-
offset: int('Start character index (default 0).'),
|
|
1563
|
-
limit: int('Max characters to return from `offset
|
|
1861
|
+
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\`, or a \`__tmp__/…\` ref to a file you staged in the workspace's tmp folder.`),
|
|
1862
|
+
offset: int('Start character index (bytes for a spill ref; default 0).'),
|
|
1863
|
+
limit: int('Max characters to return from `offset` (bytes for a spill ref).'),
|
|
1564
1864
|
sessionId: SESSION_ID_INPUT,
|
|
1565
1865
|
},
|
|
1566
1866
|
required: ['branch', 'path'],
|
|
@@ -1594,7 +1894,11 @@ patternMatcher) {
|
|
|
1594
1894
|
: content;
|
|
1595
1895
|
};
|
|
1596
1896
|
const branch = a.branch;
|
|
1597
|
-
|
|
1897
|
+
// A tmp ref is no repository path, so the `canWrite` below is false for
|
|
1898
|
+
// it by the same rule as for any path outside the repository.
|
|
1899
|
+
const { result, guide } = isTmpRef(p)
|
|
1900
|
+
? { result: await readTmp(branch, p, ctx), guide: null }
|
|
1901
|
+
: await readServed(branch, p, ctx);
|
|
1598
1902
|
// Judged after the read, so a path the caller may not read is refused
|
|
1599
1903
|
// before anything is said about writing it. At the guide's name this
|
|
1600
1904
|
// answers for the knowledge base's own file there, as `file_stat` does:
|
|
@@ -1798,7 +2102,7 @@ patternMatcher) {
|
|
|
1798
2102
|
type: 'object',
|
|
1799
2103
|
properties: {
|
|
1800
2104
|
branch: BRANCH_INPUT,
|
|
1801
|
-
path: wsPath(kbDirName, 'Path'),
|
|
2105
|
+
path: orTmpRef(wsPath(kbDirName, 'Path')),
|
|
1802
2106
|
explainAccess: {
|
|
1803
2107
|
type: 'boolean',
|
|
1804
2108
|
description: '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`).',
|
|
@@ -1871,6 +2175,8 @@ patternMatcher) {
|
|
|
1871
2175
|
handler: async (a, ctx) => {
|
|
1872
2176
|
const p = a.path;
|
|
1873
2177
|
const branch = a.branch;
|
|
2178
|
+
if (isTmpRef(p))
|
|
2179
|
+
return statTmp(branch, p, ctx);
|
|
1874
2180
|
// The guide's name with no file of the knowledge base's own under it —
|
|
1875
2181
|
// or one the caller may not read, or a copy of the guide an earlier
|
|
1876
2182
|
// release wrote, none of which `read_file` serves: what a read answers
|
|
@@ -2034,13 +2340,14 @@ patternMatcher) {
|
|
|
2034
2340
|
mount({
|
|
2035
2341
|
name: 'grep',
|
|
2036
2342
|
gated: true,
|
|
2037
|
-
description:
|
|
2343
|
+
description: `Regex search across the workspace. ${PATTERN_DIALECT} Covers hidden and git-ignored files. ` +
|
|
2344
|
+
'Returns `{ matches: [{ path, line, text }] }`, capped, in path then line order. `path` may name a DIRECTORY (its subtree) or a single FILE; a path with nothing at it is an error. Searches INSIDE Office/OpenDocument, PDF and email files (.eml/.msg) via their extracted text — matches 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; the result notes the rest (re-run to cover them).',
|
|
2038
2345
|
inputs: {
|
|
2039
2346
|
type: 'object',
|
|
2040
2347
|
properties: {
|
|
2041
2348
|
branch: BRANCH_INPUT,
|
|
2042
|
-
pattern: str('
|
|
2043
|
-
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}/\`).`),
|
|
2349
|
+
pattern: str('Regular expression, RE2-style (ripgrep): no look-around, no backreferences.'),
|
|
2350
|
+
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}/\`) — or a \`__tmp__/…\` ref to one staged file.`),
|
|
2044
2351
|
ignore_case: { type: 'boolean', description: 'Case-insensitive match.' },
|
|
2045
2352
|
max_results: { type: 'integer', minimum: 1, maximum: 1000, description: 'Cap on matches (default 200).' },
|
|
2046
2353
|
sessionId: SESSION_ID_INPUT,
|
|
@@ -2067,9 +2374,14 @@ patternMatcher) {
|
|
|
2067
2374
|
},
|
|
2068
2375
|
write: false,
|
|
2069
2376
|
handler: async (a, ctx) => {
|
|
2070
|
-
// The caller's pattern never
|
|
2071
|
-
//
|
|
2072
|
-
|
|
2377
|
+
// The caller's pattern runs in ripgrep, never on the request thread
|
|
2378
|
+
// (see IPatternMatcher); one the engine refuses is answered here,
|
|
2379
|
+
// before anything is searched. A caller that goes away takes the
|
|
2380
|
+
// search's runs with it.
|
|
2381
|
+
const search = await patterns.search(a.pattern, {
|
|
2382
|
+
ignoreCase: Boolean(a.ignore_case),
|
|
2383
|
+
signal: ctx.abortSignal,
|
|
2384
|
+
});
|
|
2073
2385
|
// No path means the whole REPOSITORY, not the workspace directory above
|
|
2074
2386
|
// it. The workspace root also holds whatever an older build left beside
|
|
2075
2387
|
// the checkout, and the read gate has no rules for a path outside the
|
|
@@ -2095,11 +2407,17 @@ patternMatcher) {
|
|
|
2095
2407
|
// worth a stat.
|
|
2096
2408
|
// A placeholder named on its own is searched as what it is to every
|
|
2097
2409
|
// other tool: nothing.
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2410
|
+
// A tmp ref names one file by rule (see `workspace-tmp.ts`), so it is
|
|
2411
|
+
// searched as a single file — never walked, and outside the repository
|
|
2412
|
+
// there is no read gate or read hook for it, as for `read_file`.
|
|
2413
|
+
const tmp = isTmpRef(searchRoot);
|
|
2414
|
+
const kind = tmp
|
|
2415
|
+
? 'file'
|
|
2416
|
+
: searchRoot === ''
|
|
2417
|
+
? 'directory'
|
|
2418
|
+
: isFolderPlaceholder(searchRoot)
|
|
2419
|
+
? 'missing'
|
|
2420
|
+
: await searchRootKind(fs, searchRoot);
|
|
2103
2421
|
/** Why a single-file search found nothing, when "no matches" would be a lie. */
|
|
2104
2422
|
let fileNote;
|
|
2105
2423
|
// The guide is searched where it is read: a search of the repository
|
|
@@ -2120,22 +2438,26 @@ patternMatcher) {
|
|
|
2120
2438
|
// The guide's own path is not told here, because `guideAt` tells the
|
|
2121
2439
|
// hook of the organisation's file there exactly as `read_file` does
|
|
2122
2440
|
// (after the gate), and the platform's guide alone is nobody's file to
|
|
2123
|
-
// note. Each file the
|
|
2124
|
-
//
|
|
2125
|
-
|
|
2441
|
+
// note. Each file the search opens goes to the hook per file below,
|
|
2442
|
+
// before it is opened and whether or not a line of it is answered
|
|
2443
|
+
// (`grepTree`'s admission), so a hook sees every file a grep reads
|
|
2444
|
+
// rather than only its root, and may refuse one as it refuses a
|
|
2445
|
+
// read_file. A tmp ref is no repository content, so there is nothing
|
|
2446
|
+
// to tell.
|
|
2447
|
+
if (!tmp && !(coversGuide && kind !== 'directory')) {
|
|
2126
2448
|
await notifyAgentRead(agentAccessGate, ctx, a.branch, searchRoot);
|
|
2127
2449
|
}
|
|
2450
|
+
const notifyRead = (p) => notifyAgentRead(agentAccessGate, ctx, a.branch, p);
|
|
2128
2451
|
if (coversGuide) {
|
|
2129
2452
|
if (kind === 'directory') {
|
|
2130
|
-
await
|
|
2131
|
-
}
|
|
2132
|
-
const lines = (await guideAt(a.branch, ctx, guidePath)).split('\n');
|
|
2133
|
-
for (const i of await matcher.matchingLines(lines, max - out.length)) {
|
|
2134
|
-
out.push({ path: guidePath, line: i + 1, text: lines[i].slice(0, 300) });
|
|
2453
|
+
out.push(...(await grepTree(fs, searchRoot, search, max, gate, notifyRead, docs, (p) => p === guidePath)));
|
|
2135
2454
|
}
|
|
2455
|
+
const guide = await guideAt(a.branch, ctx, guidePath);
|
|
2456
|
+
const [hits] = await search.matchingLines([guide]);
|
|
2457
|
+
pushMatches(out, guidePath, guide, hits);
|
|
2136
2458
|
}
|
|
2137
2459
|
else if (kind === 'directory') {
|
|
2138
|
-
await
|
|
2460
|
+
out.push(...(await grepTree(fs, searchRoot, search, max, gate, notifyRead, docs)));
|
|
2139
2461
|
}
|
|
2140
2462
|
else {
|
|
2141
2463
|
// Not a directory: the permission verdict comes BEFORE every other
|
|
@@ -2147,11 +2469,28 @@ patternMatcher) {
|
|
|
2147
2469
|
await assertCanRead(gate, searchRoot);
|
|
2148
2470
|
if (kind === 'missing')
|
|
2149
2471
|
throw notFound(searchRoot, 'Nothing to search');
|
|
2150
|
-
// A named FILE is searched
|
|
2151
|
-
//
|
|
2152
|
-
//
|
|
2153
|
-
|
|
2154
|
-
|
|
2472
|
+
// A named text FILE is searched as a tree search searches it: by
|
|
2473
|
+
// ripgrep, from its folder, so it answers the same lines either way
|
|
2474
|
+
// (the valid UTF-8 lines of a file holding some that are not, and
|
|
2475
|
+
// nothing from one ripgrep finds binary). Any other file, and any
|
|
2476
|
+
// path that is not a regular file, is read through its reader,
|
|
2477
|
+
// exactly as read_file reads it, its text searched by the same
|
|
2478
|
+
// engine: a tree search would find nothing to walk and answer an
|
|
2479
|
+
// empty match list, which the caller cannot tell from "the pattern
|
|
2480
|
+
// is not in this file". A tmp ref is not in the checkout ripgrep
|
|
2481
|
+
// searches, so it is always read through its reader, its bytes from
|
|
2482
|
+
// the tmp resolver.
|
|
2483
|
+
const bytesOf = tmp
|
|
2484
|
+
? (ref) => ctx.workspaceService.readTmpFile(docs.origin.workspaceId, ctx.user.id, ref)
|
|
2485
|
+
: fileBytes(fs);
|
|
2486
|
+
const found = (!tmp && readers.readerFor(searchRoot) instanceof TextReader
|
|
2487
|
+
? await grepTextFile(fs, searchRoot, search, max, out)
|
|
2488
|
+
: null) ?? (await orNotFound(searchRoot, () => searchableText(bytesOf, searchRoot, docs), 'Nothing to search'));
|
|
2489
|
+
if (found.kind === 'text') {
|
|
2490
|
+
const [hits] = await search.matchingLines([found.text]);
|
|
2491
|
+
pushMatches(out, searchRoot, found.text, hits);
|
|
2492
|
+
}
|
|
2493
|
+
else if (found.kind === 'no-text') {
|
|
2155
2494
|
fileNote =
|
|
2156
2495
|
`"${displayPath(searchRoot)}" has no searchable text — it is an image, binary content, or a document ` +
|
|
2157
2496
|
'whose text could not be extracted. There were no matches because there was nothing to search, not ' +
|
|
@@ -2164,8 +2503,11 @@ patternMatcher) {
|
|
|
2164
2503
|
`extracted and this call's extraction budget (${UNCACHED_DOCS_PER_GREP}) ran out. Re-run the ` +
|
|
2165
2504
|
'same grep to extract and search the next batch.'
|
|
2166
2505
|
: undefined);
|
|
2506
|
+
// One order whatever found the lines, then the cap: the first
|
|
2507
|
+
// `max_results` by path, then line.
|
|
2508
|
+
out.sort((x, y) => compareSearchPaths(x.path, y.path) || x.line - y.line);
|
|
2167
2509
|
return {
|
|
2168
|
-
matches: out,
|
|
2510
|
+
matches: out.slice(0, max),
|
|
2169
2511
|
truncated: out.length >= max,
|
|
2170
2512
|
...(note !== undefined ? { note } : {}),
|
|
2171
2513
|
};
|
|
@@ -2180,6 +2522,7 @@ patternMatcher) {
|
|
|
2180
2522
|
gated: true,
|
|
2181
2523
|
description: 'Write a workspace TEXT file. The change is committed + pushed as you. Returns `{ path, bytes, outcome }`, where `outcome` is ' +
|
|
2182
2524
|
'`created`, `replaced` or `updated`.' +
|
|
2525
|
+
ASK_BEFORE_DEFAULT_BRANCH +
|
|
2183
2526
|
UPLOAD_ROUTE_NOTE,
|
|
2184
2527
|
inputs: {
|
|
2185
2528
|
type: 'object',
|
|
@@ -2260,6 +2603,7 @@ patternMatcher) {
|
|
|
2260
2603
|
'`created` / `replaced` / `updated` for a path it wrote, or `refused` with `error` (the code) and `message` (why) for a ' +
|
|
2261
2604
|
'path it could not. `count` is how many were written. A path it refuses — the mode said no, or the file is not text — ' +
|
|
2262
2605
|
'does not stop the others; read `files` to see what landed.' +
|
|
2606
|
+
ASK_BEFORE_DEFAULT_BRANCH +
|
|
2263
2607
|
UPLOAD_ROUTE_NOTE,
|
|
2264
2608
|
inputs: {
|
|
2265
2609
|
type: 'object',
|
|
@@ -2297,7 +2641,7 @@ patternMatcher) {
|
|
|
2297
2641
|
enum: ['created', 'replaced', 'updated', 'refused'],
|
|
2298
2642
|
description: 'What happened at this path. `refused` means nothing was written there and the file is untouched.',
|
|
2299
2643
|
},
|
|
2300
|
-
error: str('Present when `outcome` is `refused`: the refusal code
|
|
2644
|
+
error: str('Present when `outcome` is `refused`: the refusal code, such as `exists`, `missing`, `binary_not_writable` or a code of the deployment\'s own write check.'),
|
|
2301
2645
|
message: str('Present when `outcome` is `refused`: the full refusal, the same one write_file would have given.'),
|
|
2302
2646
|
},
|
|
2303
2647
|
required: ['path', 'outcome'],
|
|
@@ -2426,6 +2770,7 @@ patternMatcher) {
|
|
|
2426
2770
|
name: 'edit_file',
|
|
2427
2771
|
gated: true,
|
|
2428
2772
|
description: 'Replace an exact string in a workspace TEXT file. `old_string` must appear exactly once unless `replace_all`. Committed + pushed as you.' +
|
|
2773
|
+
ASK_BEFORE_DEFAULT_BRANCH +
|
|
2429
2774
|
UPLOAD_ROUTE_NOTE,
|
|
2430
2775
|
inputs: {
|
|
2431
2776
|
type: 'object',
|
|
@@ -2504,7 +2849,8 @@ patternMatcher) {
|
|
|
2504
2849
|
mount({
|
|
2505
2850
|
name: 'delete_file',
|
|
2506
2851
|
gated: true,
|
|
2507
|
-
description: 'Delete ONE workspace file. 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`.'
|
|
2852
|
+
description: 'Delete ONE workspace file. 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`.' +
|
|
2853
|
+
ASK_BEFORE_DEFAULT_BRANCH,
|
|
2508
2854
|
inputs: {
|
|
2509
2855
|
type: 'object',
|
|
2510
2856
|
properties: {
|
|
@@ -2726,10 +3072,11 @@ patternMatcher) {
|
|
|
2726
3072
|
// the shared rules now, which are rebuilt from the layout where they live.
|
|
2727
3073
|
description: 'Move or rename a workspace FILE or FOLDER; a folder moves recursively, with everything under it. `dest` is the full new path, not the folder to move into. Committed + pushed as you. ' +
|
|
2728
3074
|
'The destination must not exist — a move never overwrites a file or merges into a folder. Access follows the ' +
|
|
2729
|
-
'DESTINATION folder, so a move can change
|
|
2730
|
-
'
|
|
2731
|
-
'
|
|
2732
|
-
'
|
|
3075
|
+
'DESTINATION folder, so a move can change who may open or edit the file. You must be able to read what moves and the folder it lands in, ' +
|
|
3076
|
+
'or it is refused as read_file refuses. The dry run answers ' +
|
|
3077
|
+
'`{ src, dest, kind, descendants, access: { before, after }, accessChanges, holderChanges?, allowed, reason? }`: your own ' +
|
|
3078
|
+
'access at both ends, and who else loses or gains it. A move that changes either wants `confirm: true`; tell the person ' +
|
|
3079
|
+
'who loses and who gains before you confirm. ' +
|
|
2733
3080
|
'Links are rewritten by default, in one commit with the move: those in the moved files and those in other markdown files pointing at them (`links` reports them). `rewriteLinks: false` turns that off. Path mentions in plain prose or code are never changed.',
|
|
2734
3081
|
inputs: {
|
|
2735
3082
|
type: 'object',
|
|
@@ -2738,7 +3085,7 @@ patternMatcher) {
|
|
|
2738
3085
|
src: wsPath(kbDirName, 'Source path (file or folder)'),
|
|
2739
3086
|
dest: wsPath(kbDirName, 'Destination path — the full new path; must not exist yet'),
|
|
2740
3087
|
dryRun: { type: 'boolean', description: 'Answer with the impact and change nothing.' },
|
|
2741
|
-
confirm: { type: 'boolean', description: 'Required when the move changes your access. Set it only after a dry run.' },
|
|
3088
|
+
confirm: { type: 'boolean', description: 'Required when the move changes your access or anyone else\'s. Set it only after a dry run.' },
|
|
2742
3089
|
rewriteLinks: { type: 'boolean', description: 'Rewrite the links into, out of and between the moved files (default true). `false` moves without touching any link.' },
|
|
2743
3090
|
sessionId: SESSION_ID_INPUT,
|
|
2744
3091
|
},
|
|
@@ -2752,8 +3099,13 @@ patternMatcher) {
|
|
|
2752
3099
|
dest: str('Destination path (echoes the input).'),
|
|
2753
3100
|
kind: str('`file` or `folder`.'),
|
|
2754
3101
|
descendants: int('Files that move: 1 for a file, the file count under a folder.'),
|
|
2755
|
-
access: { type: 'object', description: 'Your `{ read, write, download, owner }` at the source (`before`) and at the destination once the move has landed (`after`).' },
|
|
3102
|
+
access: { type: 'object', description: 'Your `{ read, write, download, owner }` at the source (`before`) and at the destination once the move has landed (`after`), with every `access.md` inside a moved folder counted at its new place.' },
|
|
2756
3103
|
accessChanges: { type: 'boolean', description: 'True when any of your verdicts differs between `before` and `after`.' },
|
|
3104
|
+
holderChanges: {
|
|
3105
|
+
type: 'object',
|
|
3106
|
+
description: 'Who loses and who gains opening (`read`) or editing (`write`) it once the move has landed: `{ lose, gain }`, each ' +
|
|
3107
|
+
'`[{ kind, name, email?, verb }]` with `kind` `group`, `role`, `plugin` or `person`. Absent when nobody\'s access changes.',
|
|
3108
|
+
},
|
|
2757
3109
|
allowed: { type: 'boolean', description: 'Whether the move may run.' },
|
|
2758
3110
|
reason: str('Why it may not, when `allowed` is false.'),
|
|
2759
3111
|
dryRun: { type: 'boolean', description: 'True on a dry run.' },
|
|
@@ -2782,6 +3134,23 @@ patternMatcher) {
|
|
|
2782
3134
|
// source already deleted.
|
|
2783
3135
|
await assertAgentWriteAllowed(agentAccessGate, ctx, branch, src);
|
|
2784
3136
|
await assertAgentWriteAllowed(agentAccessGate, ctx, branch, dest);
|
|
3137
|
+
// What moves and the folder it lands in must both be readable to the
|
|
3138
|
+
// caller, refused as read_file refuses them, on every branch: a move
|
|
3139
|
+
// preview reads both ends (the caller's own verdicts there, who else is
|
|
3140
|
+
// affected), and the folder's rules are what the names are made of. The
|
|
3141
|
+
// verdict comes BEFORE every other look, so neither end's existence is
|
|
3142
|
+
// told to someone who may not know it. The deployment's read hook is
|
|
3143
|
+
// asked first, as read_file asks it, and its refusal is the answer: a
|
|
3144
|
+
// move the hook would not let read is not previewed at all, since even
|
|
3145
|
+
// "this move asks for confirm" would tell what the comparison found. A
|
|
3146
|
+
// folder the caller may read moves with everything in it, files they
|
|
3147
|
+
// may not read included: those travel, they are not read.
|
|
3148
|
+
const destFolder = dest.slice(0, dest.lastIndexOf('/'));
|
|
3149
|
+
await notifyAgentRead(agentAccessGate, ctx, branch, src);
|
|
3150
|
+
await notifyAgentRead(agentAccessGate, ctx, branch, destFolder);
|
|
3151
|
+
const gate = readGateFor(branch, ctx);
|
|
3152
|
+
await assertCanRead(gate, src);
|
|
3153
|
+
await assertCanRead(gate, destFolder);
|
|
2785
3154
|
const fs = await ctx.getFilesystem(branch);
|
|
2786
3155
|
const root = await workspaceRoot(branch, ctx);
|
|
2787
3156
|
// Before the kind check, which stats THROUGH a link: a dangling link at
|
|
@@ -2805,6 +3174,17 @@ patternMatcher) {
|
|
|
2805
3174
|
accessAfter(branch, ctx, src, dest),
|
|
2806
3175
|
]);
|
|
2807
3176
|
const accessChanges = verbsDiffer(before, after);
|
|
3177
|
+
// Who ELSE gains or loses opening or editing it: the lists the app's move
|
|
3178
|
+
// confirmation shows people. The caller may read both ends (the hook and
|
|
3179
|
+
// the rules, gated above, as the app's route gates), so the names go to
|
|
3180
|
+
// them, and a change asks for `confirm`. Outside the repository no rules
|
|
3181
|
+
// travel.
|
|
3182
|
+
const fromRel = toKbRelative(src, kbDirName);
|
|
3183
|
+
const toRel = toKbRelative(dest, kbDirName);
|
|
3184
|
+
const changes = fromRel !== null && toRel !== null
|
|
3185
|
+
? await accessControl.prospectiveHolderChanges(workspaceIdForBranch(branch), fromRel, toRel)
|
|
3186
|
+
: undefined;
|
|
3187
|
+
const othersChange = changes !== undefined && (changes.lose.length > 0 || changes.gain.length > 0);
|
|
2808
3188
|
// The placeholder moves with its folder, but it is never content.
|
|
2809
3189
|
const descendants = srcFiles.filter((f) => !isFolderPlaceholder(f)).length;
|
|
2810
3190
|
// Neither end may be the platform's own: a move neither takes a platform
|
|
@@ -2908,6 +3288,7 @@ patternMatcher) {
|
|
|
2908
3288
|
descendants,
|
|
2909
3289
|
access: { before, after },
|
|
2910
3290
|
accessChanges,
|
|
3291
|
+
...(othersChange ? { holderChanges: changes } : {}),
|
|
2911
3292
|
allowed: moveReason === undefined,
|
|
2912
3293
|
...(moveReason !== undefined ? { reason: moveReason } : {}),
|
|
2913
3294
|
...(linkPlan ? { links: linkPlan.report } : {}),
|
|
@@ -2927,12 +3308,16 @@ patternMatcher) {
|
|
|
2927
3308
|
throw new ToolError(reason, 409);
|
|
2928
3309
|
if (linkPlan?.overCap !== undefined)
|
|
2929
3310
|
throw new ToolError(linkPlan.overCap, 400);
|
|
2930
|
-
if (accessChanges && a.confirm !== true) {
|
|
3311
|
+
if ((accessChanges || othersChange) && a.confirm !== true) {
|
|
3312
|
+
const why = [
|
|
3313
|
+
...(accessChanges ? [`your access at "${dest}" differs from "${src}"`] : []),
|
|
3314
|
+
...(othersChange ? ['it changes who else may open or edit it (`holderChanges`)'] : []),
|
|
3315
|
+
];
|
|
2931
3316
|
return {
|
|
2932
3317
|
...impact,
|
|
2933
3318
|
confirmationRequired: true,
|
|
2934
3319
|
moved: false,
|
|
2935
|
-
message: `Nothing was moved:
|
|
3320
|
+
message: `Nothing was moved: ${why.join(', and ')}, so this move requires confirm: true.`,
|
|
2936
3321
|
};
|
|
2937
3322
|
}
|
|
2938
3323
|
// The look above is what produces the sentence; the filesystem's own
|
|
@@ -2984,14 +3369,14 @@ patternMatcher) {
|
|
|
2984
3369
|
mount({
|
|
2985
3370
|
name: 'copy_file',
|
|
2986
3371
|
gated: true,
|
|
2987
|
-
description: '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. ' +
|
|
2988
|
-
'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 AS IT WILL BE once the copy has landed, with
|
|
2989
|
-
'One exception to that shape: when the destination is one you may not write, the answer is the refusal
|
|
3372
|
+
description: '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. `src` may be a `__tmp__/<path>` ref to a file you staged in the workspace\'s tmp folder: the copy is how such a file is kept, as a normal repository file. ' +
|
|
3373
|
+
'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 AS IT WILL BE once the copy has landed, with the copy\'s own rules counted at its new place. ' +
|
|
3374
|
+
'One exception to that shape: when the destination is one you may not write, the answer is the refusal (`allowed: false` with `reason`, and the access figures) with no `kind` and no `descendants`, because nothing about the source is read before that verdict.',
|
|
2990
3375
|
inputs: {
|
|
2991
3376
|
type: 'object',
|
|
2992
3377
|
properties: {
|
|
2993
3378
|
branch: BRANCH_INPUT,
|
|
2994
|
-
src: wsPath(kbDirName, 'Source path'),
|
|
3379
|
+
src: orTmpRef(wsPath(kbDirName, 'Source path')),
|
|
2995
3380
|
dest: wsPath(kbDirName, 'Destination path — must not exist yet'),
|
|
2996
3381
|
dryRun: { type: 'boolean', description: 'Answer with the impact and change nothing.' },
|
|
2997
3382
|
sessionId: SESSION_ID_INPUT,
|
|
@@ -3019,11 +3404,16 @@ patternMatcher) {
|
|
|
3019
3404
|
proposable: true,
|
|
3020
3405
|
handler: async (a, ctx) => {
|
|
3021
3406
|
// A copy CARRIES the source content into the destination, so BOTH ends
|
|
3022
|
-
// go to the write hook. Ask about both before touching disk.
|
|
3023
|
-
|
|
3407
|
+
// go to the write hook. Ask about both before touching disk. A tmp ref
|
|
3408
|
+
// is not a knowledge-base path, so only the destination is one to ask
|
|
3409
|
+
// about when the copy is out of the workspace's tmp folder.
|
|
3410
|
+
const fromTmp = isTmpRef(a.src);
|
|
3411
|
+
if (!fromTmp)
|
|
3412
|
+
writePolicy.assertPathWritable(ctx.sessionId, a.src);
|
|
3024
3413
|
writePolicy.assertPathWritable(ctx.sessionId, a.dest);
|
|
3025
3414
|
const branch = a.branch;
|
|
3026
|
-
|
|
3415
|
+
if (!fromTmp)
|
|
3416
|
+
await assertAgentWriteAllowed(agentAccessGate, ctx, branch, a.src);
|
|
3027
3417
|
await assertAgentWriteAllowed(agentAccessGate, ctx, branch, a.dest);
|
|
3028
3418
|
const src = a.src;
|
|
3029
3419
|
const dest = a.dest;
|
|
@@ -3073,6 +3463,10 @@ patternMatcher) {
|
|
|
3073
3463
|
// the server's own absolute path. Anything that is not absence travels
|
|
3074
3464
|
// on as it always did.
|
|
3075
3465
|
const fs = await ctx.getFilesystem(branch);
|
|
3466
|
+
if (fromTmp) {
|
|
3467
|
+
await copyFromTmp(fs, branch, ctx, src, dest);
|
|
3468
|
+
return { src, dest, copied: true };
|
|
3469
|
+
}
|
|
3076
3470
|
try {
|
|
3077
3471
|
await asEntryExists(() => fs.copyFile(src, dest));
|
|
3078
3472
|
}
|
|
@@ -3397,7 +3791,7 @@ patternMatcher) {
|
|
|
3397
3791
|
'`replaced` / `updated`, or `refused` with `error` (the code) and `message` (why). `count` is how many landed and ' +
|
|
3398
3792
|
'`total` how many paths there were; `files` is cut to the first 25 unless you pass `all: true`. ' +
|
|
3399
3793
|
'Every path is judged one by one — by your write access, the platform-file rules and what is already there — ' +
|
|
3400
|
-
'
|
|
3794
|
+
'as `write_file` judges it, except that platform files are refused and binary content is fine; a refused path does not stop the others. ' +
|
|
3401
3795
|
'The token is single-use: it is spent by the apply that lands it, and refused if you use it twice, let it ' +
|
|
3402
3796
|
'expire, or present one issued to somebody else. `mode` means what it means on `write_file`.',
|
|
3403
3797
|
inputs: {
|
|
@@ -3476,7 +3870,7 @@ patternMatcher) {
|
|
|
3476
3870
|
'each one inside a folder too, is included only if you may read AND download that file. Returns ' +
|
|
3477
3871
|
'`{ expiresAt, expiresInSeconds, files: [{ path, bytes, sha256, downloadUrl }], folders: [{ path, bytes, ' +
|
|
3478
3872
|
'downloadUrl, files }], refused: [{ path, reason }] }`: a link per file, with its own content type, and per ' +
|
|
3479
|
-
`folder a zip at full repository paths (\`apply_file_upload\` it at \`${kbDirName}/\` to put
|
|
3873
|
+
`folder a zip at full repository paths (\`apply_file_upload\` it at \`${kbDirName}/\` to put them back). ` +
|
|
3480
3874
|
'Fetch with any HTTP client (`curl -o <name> "<downloadUrl>"`, or the address without its last segment and an ' +
|
|
3481
3875
|
'`x-download-token` header). Each link works ONCE, for 15 minutes, and serves the files as they are now. ' +
|
|
3482
3876
|
'Refused: `not found` (missing or named-but-unreadable; folders omit unreadable files), ' +
|