@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
|
@@ -8,41 +8,40 @@ import AdmZip from 'adm-zip';
|
|
|
8
8
|
import express from 'express';
|
|
9
9
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
10
|
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
11
|
+
import { slowRipgrep } from '../../../__tests__/slow-ripgrep.js';
|
|
11
12
|
import { LocalFilesystem } from '@mastra/core/workspace';
|
|
12
|
-
import { ToolRegistry } from '../../tool-registry/
|
|
13
|
-
import { createToolHandlerFactory } from '
|
|
14
|
-
import { ToolError, type ToolContext } from '../../tool-helpers/
|
|
15
|
-
import type { ToolAuth } from '../../tool-auth/
|
|
13
|
+
import { GUIDE_FIRST_SENTENCE, ToolRegistry } from '../../tool-registry/index.js';
|
|
14
|
+
import { AccessControlService, WorkflowHooks, createToolHandlerFactory, type AgentOperationContext } from '../../../__tests__/tool-wiring.js';
|
|
15
|
+
import { ToolError, type ToolContext } from '../../tool-helpers/index.js';
|
|
16
|
+
import type { ToolAuth } from '../../tool-auth/index.js';
|
|
16
17
|
import { registerWorkspaceTools } from '../workspace.tools.js';
|
|
17
|
-
import {
|
|
18
|
-
import { sharedFileRules, sharedFileRulesSection } from '../../agent-instructions/
|
|
19
|
-
import { composeAgentGuide } from '../../agent-guide/
|
|
20
|
-
import { GUIDE_FIRST_SENTENCE } from '../../tool-registry/guide-first.js';
|
|
18
|
+
import { RipgrepPatternMatcher, type RipgrepPatternMatcherOptions } from '../index.js';
|
|
19
|
+
import { sharedFileRules, sharedFileRulesSection } from '../../agent-instructions/index.js';
|
|
20
|
+
import { composeAgentGuide } from '../../agent-guide/index.js';
|
|
21
21
|
import { RoutineWritePolicyService } from '../routine-write-policy.js';
|
|
22
22
|
import { UuidSessionSink, type ISessionSink } from '../session-sink.js';
|
|
23
23
|
import { FIRST_RUN_SECTION_ID, STARTER_GUIDE_FILE, firstRunNote, type FirstRunStarterSource } from '../first-run.js';
|
|
24
|
-
import { WorkflowHooks, type AgentOperationContext } from '../../workflow/workflow-hooks.js';
|
|
25
24
|
import { SESSION_ID_DESCRIPTION, ToolDescriptionNotes } from '../agent-access.gate.js';
|
|
26
25
|
import { SpillStore } from '../spill-store.js';
|
|
27
26
|
import { DocExtractService } from '../file-readers/doc-extract.service.js';
|
|
28
27
|
import { WorkerDocumentExtractor } from '../file-readers/extract-in-worker.js';
|
|
29
28
|
import { OCTET_STREAM_FALLBACK_NOTE } from '../file-readers/content-mode.js';
|
|
30
|
-
import type { IAccessControl } from '../../access/
|
|
31
|
-
import {
|
|
32
|
-
import { NodeFs } from '../../kb-fs/node-fs.js';
|
|
29
|
+
import type { IAccessControl } from '../../access/index.js';
|
|
30
|
+
import { realDisk } from '../../../__tests__/real-disk.js';
|
|
33
31
|
import type { WorkspaceService } from '../workspace.service.js';
|
|
34
32
|
import { DEFAULT_KB_LAYOUT, isBranchAuthoredBy, isOwnSuggestionsBranch } from '@bevel-software/platform-shared';
|
|
35
|
-
import { normalizeWorkspacePath } from '../../kb-fs/
|
|
33
|
+
import { normalizeWorkspacePath } from '../../kb-fs/index.js';
|
|
36
34
|
import { GIT_INTERNALS_MESSAGE, PathNotFoundError } from '../../../shared/domain-errors.js';
|
|
37
|
-
import { AccessDeniedError } from '../../access-model/
|
|
35
|
+
import { AccessDeniedError } from '../../access-model/index.js';
|
|
38
36
|
import { proposalTitleFor } from '../write-denial.js';
|
|
39
37
|
import { NOT_FOUND_NEXT_STEP } from '../not-found.js';
|
|
40
38
|
import { compileCheck, exampleArguments } from '@bevel-software/platform-mcp-core';
|
|
41
|
-
import { routeToolSchemas } from '../../tool-helpers/route-tool-schemas.js';
|
|
42
|
-
import { TOOL_DESCRIPTION_CAP, clientVisibleLength } from '../../tool-registry/description-length.js';
|
|
43
39
|
|
|
44
40
|
const KB_DIR = 'knowledge-base';
|
|
45
41
|
|
|
42
|
+
/** A move that changes nobody's access: what a stand-in without rules answers. */
|
|
43
|
+
const NO_CHANGES = { lose: [], gain: [] };
|
|
44
|
+
|
|
46
45
|
/** Allow-all access control — the default for tests not exercising read gating. */
|
|
47
46
|
const allowAll = {
|
|
48
47
|
canRead: async () => true,
|
|
@@ -66,6 +65,8 @@ const allowAll = {
|
|
|
66
65
|
// a folder's `access.md` files actually does to it is exercised against
|
|
67
66
|
// REAL rules in "a move preview judges the destination as it will be".
|
|
68
67
|
previewAccessAfterRelocation: async () => ({ read: true, write: true, download: true, owner: true }),
|
|
68
|
+
// Who else loses or gains access in a move: nobody, here.
|
|
69
|
+
prospectiveHolderChanges: async () => NO_CHANGES,
|
|
69
70
|
} as unknown as IAccessControl;
|
|
70
71
|
|
|
71
72
|
/** Access control that denies `canRead` for an explicit set of repo-relative paths. */
|
|
@@ -144,13 +145,13 @@ let raceHook: (() => Promise<void>) | null = null;
|
|
|
144
145
|
* The grep matcher `start()` wires, built from `matcherOptions` (the
|
|
145
146
|
* defaults, unless a test narrows the pool) and closed after each test.
|
|
146
147
|
*/
|
|
147
|
-
let patternMatcher:
|
|
148
|
+
let patternMatcher: RipgrepPatternMatcher | undefined;
|
|
148
149
|
/**
|
|
149
150
|
* The document extractor `start()` wires, closed after each test: its workers
|
|
150
151
|
* are made lazily, and `unref()` lets the process exit but keeps them alive.
|
|
151
152
|
*/
|
|
152
153
|
let docExtractor: WorkerDocumentExtractor | undefined;
|
|
153
|
-
let matcherOptions:
|
|
154
|
+
let matcherOptions: RipgrepPatternMatcherOptions = {};
|
|
154
155
|
|
|
155
156
|
async function start(
|
|
156
157
|
scope: 'read' | 'write' = 'write',
|
|
@@ -160,7 +161,7 @@ async function start(
|
|
|
160
161
|
): Promise<string> {
|
|
161
162
|
raceHook = null;
|
|
162
163
|
await patternMatcher?.close();
|
|
163
|
-
patternMatcher = new
|
|
164
|
+
patternMatcher = new RipgrepPatternMatcher(matcherOptions);
|
|
164
165
|
await docExtractor?.close();
|
|
165
166
|
const extractor = new WorkerDocumentExtractor();
|
|
166
167
|
docExtractor = extractor;
|
|
@@ -1015,6 +1016,10 @@ describe("the agent guide at the guide's name", () => {
|
|
|
1015
1016
|
expect((await grep(base, 'needle', 'AGENTS.md')).matches).toEqual([{ path: GUIDE, line: 3, text: 'Name every needle you plant.' }]);
|
|
1016
1017
|
// A search under a folder does not reach a file that is not under it.
|
|
1017
1018
|
expect((await grep(base, 'needle', `${KB_DIR}/Handbook`)).matches.map((m) => m.path)).toEqual([`${KB_DIR}/Handbook/a.md`]);
|
|
1019
|
+
// A folder named with a trailing slash answers the same paths, spelled
|
|
1020
|
+
// once, and the guide once.
|
|
1021
|
+
expect((await grep(base, 'needle', `${KB_DIR}/Handbook/`)).matches.map((m) => m.path)).toEqual([`${KB_DIR}/Handbook/a.md`]);
|
|
1022
|
+
expect((await grep(base, 'needle', `${KB_DIR}/`)).matches).toEqual(fromRoot.matches);
|
|
1018
1023
|
|
|
1019
1024
|
// With the knowledge base's own AGENTS.md, the search covers the composed
|
|
1020
1025
|
// text — the own file first, then the guide — and once: the walk's own
|
|
@@ -1029,17 +1034,18 @@ describe("the agent guide at the guide's name", () => {
|
|
|
1029
1034
|
]);
|
|
1030
1035
|
expect((await grep(base, 'needle', GUIDE)).matches).toEqual(composed);
|
|
1031
1036
|
|
|
1032
|
-
// The own file does not use up `max_results` twice: the
|
|
1033
|
-
// the composed search, so
|
|
1034
|
-
//
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1037
|
+
// The own file does not use up `max_results` twice: the tree search
|
|
1038
|
+
// leaves it to the composed search, so its lines are counted once and the
|
|
1039
|
+
// file after it in path order is still reached. Counted twice, its five
|
|
1040
|
+
// lines and their duplicates would fill a cap of seven, and Handbook/
|
|
1041
|
+
// would be cut.
|
|
1037
1042
|
await fs.writeFile(`${KB_DIR}/AGENTS.md`, `# Acme\n\n${'needle\n'.repeat(5)}`);
|
|
1038
|
-
const capped = (await (await post(`${base}/api/agent/tools/grep`, { pattern: 'needle', max_results:
|
|
1039
|
-
matches: { path: string }[];
|
|
1043
|
+
const capped = (await (await post(`${base}/api/agent/tools/grep`, { pattern: 'needle', max_results: 7 })).json()) as {
|
|
1044
|
+
matches: { path: string; line: number }[];
|
|
1040
1045
|
};
|
|
1041
|
-
|
|
1042
|
-
expect(capped.matches).
|
|
1046
|
+
// Five lines of the own file and the guide's one, then the file after it.
|
|
1047
|
+
expect(capped.matches.map((m) => m.path)).toEqual([...Array<string>(6).fill(GUIDE), `${KB_DIR}/Handbook/a.md`]);
|
|
1048
|
+
expect(new Set(capped.matches.map((m) => `${m.path}:${m.line}`)).size).toBe(7);
|
|
1043
1049
|
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Acme\n\nOur needle rule.\n');
|
|
1044
1050
|
|
|
1045
1051
|
// A caller who may not read the own file searches the guide alone, from
|
|
@@ -1192,6 +1198,22 @@ describe('grep with a path that names a file', () => {
|
|
|
1192
1198
|
}
|
|
1193
1199
|
});
|
|
1194
1200
|
|
|
1201
|
+
it('a named text file answers the lines a folder search answers from it, valid UTF-8 lines of a file holding others included', async () => {
|
|
1202
|
+
const base = await start();
|
|
1203
|
+
await fs.writeFile(`${KB_DIR}/mixed/latin.txt`, Buffer.concat([Buffer.from('needle \xff\n', 'latin1'), Buffer.from('needle ok\n')]));
|
|
1204
|
+
const named = await grep(base, { pattern: 'needle', path: `${KB_DIR}/mixed/latin.txt` });
|
|
1205
|
+
const folder = await grep(base, { pattern: 'needle', path: `${KB_DIR}/mixed` });
|
|
1206
|
+
expect(named.matches).toEqual([{ path: `${KB_DIR}/mixed/latin.txt`, line: 2, text: 'needle ok' }]);
|
|
1207
|
+
expect(named.matches).toEqual(folder.matches);
|
|
1208
|
+
expect(named.note).toBeUndefined();
|
|
1209
|
+
// A line that is not UTF-8 counts toward ripgrep's cap, yet the line after it is still answered.
|
|
1210
|
+
for (const path of [`${KB_DIR}/mixed/latin.txt`, `${KB_DIR}/mixed`]) {
|
|
1211
|
+
const one = await grep(base, { pattern: 'needle', path, max_results: 1 });
|
|
1212
|
+
expect(one.matches).toEqual([{ path: `${KB_DIR}/mixed/latin.txt`, line: 2, text: 'needle ok' }]);
|
|
1213
|
+
expect(one.truncated).toBe(true);
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1195
1217
|
it('a path with nothing at it fails with an error naming the path — never an empty success', async () => {
|
|
1196
1218
|
const base = await start();
|
|
1197
1219
|
const res = await post(`${base}/api/agent/tools/grep`, { pattern: 'needle', path: `${KB_DIR}/notes/ghost.md` });
|
|
@@ -1278,119 +1300,356 @@ describe('grep with a path that names a file', () => {
|
|
|
1278
1300
|
});
|
|
1279
1301
|
|
|
1280
1302
|
/**
|
|
1281
|
-
*
|
|
1282
|
-
*
|
|
1283
|
-
* request keeps being served
|
|
1284
|
-
*
|
|
1303
|
+
* grep runs ripgrep (RipgrepPatternMatcher), as a child process: the pattern
|
|
1304
|
+
* is ripgrep's default engine's to judge, a search is stopped at its budget
|
|
1305
|
+
* while every other request keeps being served, and the walk covers every
|
|
1306
|
+
* text file the caller may read — hidden, git-ignored and untracked alike —
|
|
1307
|
+
* without following a link or entering `.git`.
|
|
1285
1308
|
*/
|
|
1286
|
-
describe('
|
|
1287
|
-
|
|
1309
|
+
describe('grep runs ripgrep', () => {
|
|
1310
|
+
type GrepBody = { matches: { path: string; line: number; text: string }[]; truncated: boolean; note?: string; error?: string; code?: string };
|
|
1311
|
+
const grep = async (base: string, body: Record<string, unknown>): Promise<{ status: number; body: GrepBody }> => {
|
|
1312
|
+
const res = await post(`${base}/api/agent/tools/grep`, body);
|
|
1313
|
+
return { status: res.status, body: (await res.json()) as GrepBody };
|
|
1314
|
+
};
|
|
1315
|
+
const git = (...args: string[]) => promisify(execFile)('git', args, { cwd: join(tempDir, KB_DIR) });
|
|
1288
1316
|
|
|
1289
|
-
it('
|
|
1317
|
+
it('stops a search at its 5 s budget with the 400 "too expensive", leaves no ripgrep running, and serves other calls meanwhile', async () => {
|
|
1318
|
+
const slow = await slowRipgrep();
|
|
1319
|
+
try {
|
|
1320
|
+
matcherOptions = {};
|
|
1321
|
+
const base = await start();
|
|
1322
|
+
await fs.writeFile(`${KB_DIR}/hit.md`, 'needle\n');
|
|
1323
|
+
const started = Date.now();
|
|
1324
|
+
const searching = grep(base, { pattern: 'needle' }).then((r) => ({ ...r, took: Date.now() - started }));
|
|
1325
|
+
// Probe while the search runs: each read must answer well within a second.
|
|
1326
|
+
const probes: number[] = [];
|
|
1327
|
+
let done = false;
|
|
1328
|
+
void searching.finally(() => (done = true));
|
|
1329
|
+
while (!done) {
|
|
1330
|
+
const t = Date.now();
|
|
1331
|
+
const res = await post(`${base}/api/agent/tools/read_file`, { path: `${KB_DIR}/a.md` });
|
|
1332
|
+
expect(res.status).toBe(200);
|
|
1333
|
+
probes.push(Date.now() - t);
|
|
1334
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1335
|
+
}
|
|
1336
|
+
const { status, body, took } = await searching;
|
|
1337
|
+
expect(status).toBe(400);
|
|
1338
|
+
expect(body.code).toBe('pattern_too_expensive');
|
|
1339
|
+
expect(body.error).toMatch(/^Pattern too expensive: /);
|
|
1340
|
+
expect(body.error).toContain("this search's 5 s budget");
|
|
1341
|
+
expect(took).toBeGreaterThanOrEqual(5_000);
|
|
1342
|
+
expect(took).toBeLessThan(5_000 + 2_000);
|
|
1343
|
+
expect(probes.length).toBeGreaterThan(10);
|
|
1344
|
+
expect(Math.max(...probes)).toBeLessThan(1_000);
|
|
1345
|
+
expect(await slow.alive()).toEqual([]);
|
|
1346
|
+
} finally {
|
|
1347
|
+
await slow.remove();
|
|
1348
|
+
}
|
|
1349
|
+
}, 15_000);
|
|
1350
|
+
|
|
1351
|
+
it('answers 503 when every search slot is taken and the queue is full, rather than an incomplete result', async () => {
|
|
1352
|
+
const slow = await slowRipgrep();
|
|
1353
|
+
try {
|
|
1354
|
+
matcherOptions = { budgetMs: 1_000, maxRunning: 1, maxWaiting: 1 };
|
|
1355
|
+
const base = await start();
|
|
1356
|
+
await fs.writeFile(`${KB_DIR}/hit.md`, 'needle\n');
|
|
1357
|
+
const holding = grep(base, { pattern: 'needle' }).then((r) => r.status);
|
|
1358
|
+
while ((await slow.alive()).length === 0) await new Promise((r) => setTimeout(r, 20));
|
|
1359
|
+
const queued = grep(base, { pattern: 'needle' }).then((r) => r.status);
|
|
1360
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1361
|
+
const refused = await grep(base, { pattern: 'needle' });
|
|
1362
|
+
expect(refused.status).toBe(503);
|
|
1363
|
+
expect(refused.body.code).toBe('search_busy');
|
|
1364
|
+
expect(await Promise.all([holding, queued])).toEqual([400, 400]);
|
|
1365
|
+
expect(await slow.alive()).toEqual([]);
|
|
1366
|
+
// A slot is free again: with the real ripgrep back, a search answers.
|
|
1367
|
+
await slow.remove();
|
|
1368
|
+
const after = await grep(base, { pattern: 'needle', path: `${KB_DIR}/hit.md` });
|
|
1369
|
+
expect(after.status).toBe(200);
|
|
1370
|
+
expect(after.body.matches).toEqual([{ path: `${KB_DIR}/hit.md`, line: 1, text: 'needle' }]);
|
|
1371
|
+
} finally {
|
|
1372
|
+
await slow.remove();
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
it.each([
|
|
1377
|
+
['look-ahead', 'foo(?=bar)', /look-around/],
|
|
1378
|
+
['look-behind', '(?<=foo)bar', /look-around/],
|
|
1379
|
+
['a backreference', '(a)\\1', /backreferences are not supported/],
|
|
1380
|
+
['invalid syntax', 'a(', /unclosed group/],
|
|
1381
|
+
])('refuses %s with a 400 quoting ripgrep and naming the dialect, whatever path it names', async (_what, pattern, reason) => {
|
|
1382
|
+
const base = await start();
|
|
1383
|
+
await fs.writeFile(`${KB_DIR}/img.png`, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0, 0, 0]));
|
|
1384
|
+
for (const path of [undefined, `${KB_DIR}/a.md`, `${KB_DIR}/img.png`]) {
|
|
1385
|
+
const res = await grep(base, path === undefined ? { pattern } : { pattern, path });
|
|
1386
|
+
expect(res.status).toBe(400);
|
|
1387
|
+
expect(res.body.code).toBe('invalid_pattern');
|
|
1388
|
+
expect(res.body.error).toMatch(/^Invalid regex: /);
|
|
1389
|
+
expect(res.body.error).toMatch(reason);
|
|
1390
|
+
expect(res.body.error).toContain('RE2-style patterns: no look-around and no backreferences');
|
|
1391
|
+
// ripgrep's advice to pass a flag the caller cannot pass is not repeated.
|
|
1392
|
+
expect(res.body.error).not.toContain('--pcre2');
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
it('the description and the pattern input say which patterns the engine takes', async () => {
|
|
1397
|
+
await start();
|
|
1398
|
+
const def = (await toolRegistry.listInternal()).find((t) => t.name === 'grep');
|
|
1399
|
+
expect(def?.description).toContain("grep runs ripgrep's default regex engine, which takes RE2-style patterns: no look-around and no backreferences.");
|
|
1400
|
+
expect(JSON.stringify(def?.inputSchema ?? def)).toContain('RE2-style (ripgrep): no look-around, no backreferences.');
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
it('a pattern that backtracks for ever in JavaScript is answered at once: the engine does not backtrack', async () => {
|
|
1290
1404
|
const base = await start();
|
|
1291
|
-
|
|
1405
|
+
const longLine = `${'a'.repeat(5_000)}!`;
|
|
1406
|
+
await fs.writeFile(`${KB_DIR}/redos.md`, `intro\n${longLine}\n`);
|
|
1292
1407
|
const started = Date.now();
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
);
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1408
|
+
const none = await grep(base, { pattern: '^(a+)+$' });
|
|
1409
|
+
expect(none.status).toBe(200);
|
|
1410
|
+
expect(none.body.matches).toEqual([]);
|
|
1411
|
+
const some = await grep(base, { pattern: '^a+!$', path: `${KB_DIR}/redos.md` });
|
|
1412
|
+
expect(some.body.matches).toEqual([{ path: `${KB_DIR}/redos.md`, line: 2, text: longLine.slice(0, 300) }]);
|
|
1413
|
+
expect(Date.now() - started).toBeLessThan(2_000);
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
it('searches hidden, git-ignored and untracked files, never .git, and never through a link', async () => {
|
|
1417
|
+
const base = await start();
|
|
1418
|
+
const outside = await mkdtemp(join(tmpdir(), 'grep-outside-'));
|
|
1419
|
+
try {
|
|
1420
|
+
await writeFile(join(outside, 'secret.md'), 'needle from outside\n');
|
|
1421
|
+
await git('init', '-q');
|
|
1422
|
+
await fs.writeFile(`${KB_DIR}/.gitignore`, 'build/\nignored.md\n');
|
|
1423
|
+
await fs.writeFile(`${KB_DIR}/tracked.md`, 'needle tracked\n');
|
|
1424
|
+
await git('add', '.gitignore', 'tracked.md');
|
|
1425
|
+
await git('-c', 'user.email=t@x', '-c', 'user.name=t', 'commit', '-qm', 'init');
|
|
1426
|
+
await fs.writeFile(`${KB_DIR}/.notes.md`, 'needle hidden\n');
|
|
1427
|
+
await fs.writeFile(`${KB_DIR}/.hidden/deep.md`, 'needle in a hidden folder\n');
|
|
1428
|
+
await fs.writeFile(`${KB_DIR}/build/out.txt`, 'needle ignored folder\n');
|
|
1429
|
+
await fs.writeFile(`${KB_DIR}/ignored.md`, 'needle ignored file\n');
|
|
1430
|
+
await fs.writeFile(`${KB_DIR}/untracked.md`, 'needle untracked\n');
|
|
1431
|
+
await writeFile(join(tempDir, KB_DIR, '.git', 'needle-in-git'), 'needle git internals\n');
|
|
1432
|
+
await mkdir(join(tempDir, KB_DIR, 'sub', '.GIT'), { recursive: true });
|
|
1433
|
+
await writeFile(join(tempDir, KB_DIR, 'sub', '.GIT', 'x'), 'needle git internals\n');
|
|
1434
|
+
await symlink(join(outside, 'secret.md'), join(tempDir, KB_DIR, 'link.md'));
|
|
1435
|
+
await symlink(outside, join(tempDir, KB_DIR, 'linked-dir'));
|
|
1436
|
+
await fs.writeFile(`${KB_DIR}/binary.dat`, Buffer.concat([Buffer.from('needle in binary\n'), Buffer.from([0, 1, 2])]));
|
|
1437
|
+
const res = await grep(base, { pattern: 'needle' });
|
|
1303
1438
|
expect(res.status).toBe(200);
|
|
1304
|
-
|
|
1305
|
-
|
|
1439
|
+
expect(res.body.matches.map((m) => m.path)).toEqual([
|
|
1440
|
+
`${KB_DIR}/.hidden/deep.md`,
|
|
1441
|
+
`${KB_DIR}/.notes.md`,
|
|
1442
|
+
`${KB_DIR}/build/out.txt`,
|
|
1443
|
+
`${KB_DIR}/ignored.md`,
|
|
1444
|
+
`${KB_DIR}/tracked.md`,
|
|
1445
|
+
`${KB_DIR}/untracked.md`,
|
|
1446
|
+
]);
|
|
1447
|
+
expect(JSON.stringify(res.body)).not.toContain('outside');
|
|
1448
|
+
expect(JSON.stringify(res.body)).not.toContain('git internals');
|
|
1449
|
+
} finally {
|
|
1450
|
+
await rm(outside, { recursive: true, force: true });
|
|
1306
1451
|
}
|
|
1307
|
-
const { status, body, took } = await searching;
|
|
1308
|
-
expect(status).toBe(400);
|
|
1309
|
-
expect(body.error).toMatch(/^Pattern too expensive: /);
|
|
1310
|
-
expect(body.code).toBe('pattern_too_expensive');
|
|
1311
|
-
expect(body.error).toContain("this search's 5 s budget");
|
|
1312
|
-
expect(took).toBeLessThan(5_000 + 2_000);
|
|
1313
|
-
expect(probes.length).toBeGreaterThan(0);
|
|
1314
|
-
expect(Math.max(...probes)).toBeLessThan(1_000);
|
|
1315
1452
|
});
|
|
1316
1453
|
|
|
1317
|
-
it('
|
|
1318
|
-
const base = await start();
|
|
1319
|
-
await fs.writeFile(`${KB_DIR}/
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1454
|
+
it('never answers a line from a file the caller may not read, nor counts it against max_results', async () => {
|
|
1455
|
+
const base = await start('read', denyReads(new Set(['a-secret.md', 'Locked/b.md'])));
|
|
1456
|
+
await fs.writeFile(`${KB_DIR}/a-secret.md`, 'needle 1\nneedle 2\nneedle 3\n');
|
|
1457
|
+
await fs.writeFile(`${KB_DIR}/Locked/b.md`, 'needle locked\n');
|
|
1458
|
+
await fs.writeFile(`${KB_DIR}/open.md`, 'needle open\n');
|
|
1459
|
+
const res = await grep(base, { pattern: 'needle', max_results: 1 });
|
|
1460
|
+
expect(res.body.matches).toEqual([{ path: `${KB_DIR}/open.md`, line: 1, text: 'needle open' }]);
|
|
1461
|
+
expect(res.body.truncated).toBe(true);
|
|
1323
1462
|
});
|
|
1324
1463
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1464
|
+
// Four million matching lines: a file ripgrep searched would spend far more
|
|
1465
|
+
// than the 1.5 s budget answering them.
|
|
1466
|
+
const huge = 'needle\n'.repeat(4_000_000);
|
|
1467
|
+
|
|
1468
|
+
it('never searches a file the caller may not read, so what it holds cannot turn the answer into a 400', async () => {
|
|
1469
|
+
matcherOptions = { budgetMs: 1_500 };
|
|
1470
|
+
const base = await start('read', denyReads(new Set(['Locked/huge.md'])));
|
|
1471
|
+
await fs.writeFile(`${KB_DIR}/Locked/huge.md`, huge);
|
|
1472
|
+
await fs.writeFile(`${KB_DIR}/open.md`, 'needle open\n');
|
|
1473
|
+
const res = await grep(base, { pattern: 'needle' });
|
|
1474
|
+
expect(res.status).toBe(200);
|
|
1475
|
+
expect(res.body.matches).toEqual([{ path: `${KB_DIR}/open.md`, line: 1, text: 'needle open' }]);
|
|
1476
|
+
}, 20_000);
|
|
1477
|
+
|
|
1478
|
+
it('spends the budget on judging who may read what too: a verdict that outlasts it answers the 400 at the budget', async () => {
|
|
1479
|
+
matcherOptions = { budgetMs: 1_000 };
|
|
1480
|
+
// An access check that takes ten seconds, as a cold one over a large tree could.
|
|
1481
|
+
const slowAccess = {
|
|
1482
|
+
...allowAll,
|
|
1483
|
+
canReadBatch: (_w: string, _u: string, paths: string[]) =>
|
|
1484
|
+
new Promise<Map<string, boolean>>((resolve) => setTimeout(() => resolve(new Map(paths.map((p) => [p, true]))), 10_000)),
|
|
1485
|
+
} as unknown as IAccessControl;
|
|
1486
|
+
const base = await start('read', slowAccess);
|
|
1487
|
+
await fs.writeFile(`${KB_DIR}/open.md`, 'needle open\n');
|
|
1488
|
+
const started = Date.now();
|
|
1489
|
+
const res = await grep(base, { pattern: 'needle' });
|
|
1490
|
+
expect(res.status).toBe(400);
|
|
1491
|
+
expect(res.body.code).toBe('pattern_too_expensive');
|
|
1492
|
+
expect(Date.now() - started).toBeLessThan(3_000);
|
|
1493
|
+
}, 20_000);
|
|
1494
|
+
|
|
1495
|
+
it('stops the search at the file that fills max_results, so what comes after it costs nothing', async () => {
|
|
1496
|
+
matcherOptions = { budgetMs: 1_500 };
|
|
1328
1497
|
const base = await start();
|
|
1329
|
-
await fs.writeFile(`${KB_DIR}/
|
|
1330
|
-
await fs.writeFile(`${KB_DIR}/
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1498
|
+
await fs.writeFile(`${KB_DIR}/a.md`, 'needle first\n');
|
|
1499
|
+
await fs.writeFile(`${KB_DIR}/z/huge.md`, huge);
|
|
1500
|
+
const res = await grep(base, { pattern: 'needle', max_results: 1 });
|
|
1501
|
+
expect(res.status).toBe(200);
|
|
1502
|
+
expect(res.body.matches).toEqual([{ path: `${KB_DIR}/a.md`, line: 1, text: 'needle first' }]);
|
|
1503
|
+
expect(res.body.truncated).toBe(true);
|
|
1504
|
+
}, 20_000);
|
|
1505
|
+
|
|
1506
|
+
it('reads a file of millions of matching lines only as far as max_results, in a tree and named alone', async () => {
|
|
1507
|
+
matcherOptions = { budgetMs: 1_500 };
|
|
1508
|
+
const base = await start();
|
|
1509
|
+
await fs.writeFile(`${KB_DIR}/a/huge.md`, huge);
|
|
1510
|
+
await fs.writeFile(`${KB_DIR}/b.md`, 'needle b\n');
|
|
1511
|
+
const expected = Array.from({ length: 200 }, (_, i) => ({ path: `${KB_DIR}/a/huge.md`, line: i + 1, text: 'needle' }));
|
|
1512
|
+
for (const path of [KB_DIR, `${KB_DIR}/a/huge.md`]) {
|
|
1513
|
+
const res = await grep(base, { pattern: 'needle', path });
|
|
1514
|
+
expect(res.status).toBe(200);
|
|
1515
|
+
expect(res.body.matches).toEqual(expected);
|
|
1516
|
+
expect(res.body.truncated).toBe(true);
|
|
1517
|
+
}
|
|
1518
|
+
}, 20_000);
|
|
1519
|
+
|
|
1520
|
+
it('a named file whose only NUL lies past the first 64 KB has no searchable text; the same length of text has no matches and no note', async () => {
|
|
1521
|
+
matcherOptions = {};
|
|
1522
|
+
const base = await start();
|
|
1523
|
+
// ripgrep stops reading a file at the lines the answer can take, so a NUL
|
|
1524
|
+
// far past the pattern's last chance is found by the read that follows,
|
|
1525
|
+
// chunk by chunk — here in the fourth chunk.
|
|
1526
|
+
const filler = Buffer.alloc(200 * 1024, 'x');
|
|
1527
|
+
await fs.writeFile(`${KB_DIR}/late.md`, Buffer.concat([filler, Buffer.from([0])]));
|
|
1528
|
+
await fs.writeFile(`${KB_DIR}/plain.md`, filler);
|
|
1529
|
+
const binary = await grep(base, { pattern: 'needle', path: `${KB_DIR}/late.md` });
|
|
1530
|
+
expect(binary.status).toBe(200);
|
|
1531
|
+
expect(binary.body.matches).toEqual([]);
|
|
1532
|
+
expect(binary.body.note).toContain('no searchable text');
|
|
1533
|
+
const text = await grep(base, { pattern: 'needle', path: `${KB_DIR}/plain.md` });
|
|
1534
|
+
expect(text.status).toBe(200);
|
|
1535
|
+
expect(text.body.matches).toEqual([]);
|
|
1536
|
+
expect(text.body.note).toBeUndefined();
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
it('stops the search when its caller goes away, leaving no ripgrep running', async () => {
|
|
1540
|
+
const slow = await slowRipgrep();
|
|
1541
|
+
try {
|
|
1542
|
+
matcherOptions = {};
|
|
1543
|
+
const base = await start();
|
|
1544
|
+
const gone = new AbortController();
|
|
1545
|
+
const call = fetch(`${base}/api/agent/tools/grep`, {
|
|
1546
|
+
method: 'POST',
|
|
1547
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer x' },
|
|
1548
|
+
body: JSON.stringify({ branch: 'main', pattern: 'needle' }),
|
|
1549
|
+
signal: gone.signal,
|
|
1550
|
+
}).catch(() => undefined);
|
|
1551
|
+
while ((await slow.alive()).length === 0) await new Promise((r) => setTimeout(r, 20));
|
|
1552
|
+
const left = Date.now();
|
|
1553
|
+
gone.abort();
|
|
1554
|
+
await call;
|
|
1555
|
+
// Well inside the 5 s budget the search would otherwise have run to.
|
|
1556
|
+
while ((await slow.alive()).length > 0 && Date.now() - left < 4_000) await new Promise((r) => setTimeout(r, 20));
|
|
1557
|
+
expect(await slow.alive()).toEqual([]);
|
|
1558
|
+
expect(Date.now() - left).toBeLessThan(1_000);
|
|
1559
|
+
} finally {
|
|
1560
|
+
await slow.remove();
|
|
1369
1561
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
const files = Math.ceil((2 * budgetMs) / again);
|
|
1374
|
-
matcherOptions = { budgetMs };
|
|
1562
|
+
}, 15_000);
|
|
1563
|
+
|
|
1564
|
+
it('searches node_modules and folders of any depth, and a link inside the workspace only where it points', async () => {
|
|
1375
1565
|
const base = await start();
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
expect(
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1566
|
+
const deep = Array.from({ length: 15 }, (_, i) => `d${i}`).join('/');
|
|
1567
|
+
await fs.writeFile(`${KB_DIR}/node_modules/pkg/readme.md`, 'needle in node_modules\n');
|
|
1568
|
+
await fs.writeFile(`${KB_DIR}/${deep}/deep.md`, 'needle fifteen levels down\n');
|
|
1569
|
+
await fs.writeFile(`${KB_DIR}/real/target.md`, 'needle at the target\n');
|
|
1570
|
+
await symlink(join(tempDir, KB_DIR, 'real', 'target.md'), join(tempDir, KB_DIR, 'file-link.md'));
|
|
1571
|
+
await symlink(join(tempDir, KB_DIR, 'real'), join(tempDir, KB_DIR, 'folder-link'));
|
|
1572
|
+
const res = await grep(base, { pattern: 'needle' });
|
|
1573
|
+
expect(res.status).toBe(200);
|
|
1574
|
+
expect(res.body.matches.map((m) => m.path)).toEqual([
|
|
1575
|
+
`${KB_DIR}/${deep}/deep.md`,
|
|
1576
|
+
`${KB_DIR}/node_modules/pkg/readme.md`,
|
|
1577
|
+
`${KB_DIR}/real/target.md`,
|
|
1578
|
+
]);
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
it('leaves out a file whose name holds a line break, which no tool can read, and answers the rest', async () => {
|
|
1389
1582
|
const base = await start();
|
|
1390
|
-
await
|
|
1391
|
-
|
|
1583
|
+
await writeFile(join(tempDir, KB_DIR, 'two\nlines.md'), 'needle in an odd name\n');
|
|
1584
|
+
await fs.writeFile(`${KB_DIR}/plain.md`, 'needle plain\n');
|
|
1585
|
+
expect((await post(`${base}/api/agent/tools/read_file`, { path: `${KB_DIR}/two\nlines.md` })).status).toBe(400);
|
|
1586
|
+
const res = await grep(base, { pattern: 'needle' });
|
|
1392
1587
|
expect(res.status).toBe(200);
|
|
1393
|
-
expect(
|
|
1588
|
+
expect(res.body.matches).toEqual([{ path: `${KB_DIR}/plain.md`, line: 1, text: 'needle plain' }]);
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
it('answers the first max_results by path then line, whatever reads them, and marks the answer truncated', async () => {
|
|
1592
|
+
const docx = (text: string): Buffer => {
|
|
1593
|
+
const zip = new AdmZip();
|
|
1594
|
+
zip.addFile('[Content_Types].xml', Buffer.from('<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"/>'));
|
|
1595
|
+
zip.addFile(
|
|
1596
|
+
'word/document.xml',
|
|
1597
|
+
Buffer.from(`<?xml version="1.0"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>${text}</w:t></w:r></w:p></w:body></w:document>`),
|
|
1598
|
+
);
|
|
1599
|
+
return zip.toBuffer();
|
|
1600
|
+
};
|
|
1601
|
+
const base = await start();
|
|
1602
|
+
await fs.writeFile(`${KB_DIR}/b.md`, 'x\nneedle b2\nneedle b3\n');
|
|
1603
|
+
await fs.writeFile(`${KB_DIR}/a/z.md`, 'needle a/z\n');
|
|
1604
|
+
await fs.writeFile(`${KB_DIR}/a.md`, 'needle a.md\n');
|
|
1605
|
+
await fs.writeFile(`${KB_DIR}/a/m.docx`, docx('needle in a document'));
|
|
1606
|
+
await fs.writeFile(`${KB_DIR}/c.md`, `needle ${'c'.repeat(400)}\n`);
|
|
1607
|
+
const all = await grep(base, { pattern: 'needle' });
|
|
1608
|
+
expect(all.body.matches.map((m) => [m.path, m.line])).toEqual([
|
|
1609
|
+
[`${KB_DIR}/a/m.docx`, 2],
|
|
1610
|
+
[`${KB_DIR}/a/z.md`, 1],
|
|
1611
|
+
[`${KB_DIR}/a.md`, 1],
|
|
1612
|
+
[`${KB_DIR}/b.md`, 2],
|
|
1613
|
+
[`${KB_DIR}/b.md`, 3],
|
|
1614
|
+
[`${KB_DIR}/c.md`, 1],
|
|
1615
|
+
]);
|
|
1616
|
+
expect(all.body.truncated).toBe(false);
|
|
1617
|
+
expect(all.body.matches[5]!.text).toHaveLength(300);
|
|
1618
|
+
const capped = await grep(base, { pattern: 'needle', max_results: 4 });
|
|
1619
|
+
expect(capped.body.matches).toEqual(all.body.matches.slice(0, 4));
|
|
1620
|
+
expect(capped.body.truncated).toBe(true);
|
|
1621
|
+
// The default cap is 200.
|
|
1622
|
+
await fs.writeFile(`${KB_DIR}/many.md`, 'needle\n'.repeat(250));
|
|
1623
|
+
const many = await grep(base, { pattern: 'needle', path: `${KB_DIR}/many.md` });
|
|
1624
|
+
expect(many.body.matches).toHaveLength(200);
|
|
1625
|
+
expect(many.body.truncated).toBe(true);
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
it('honours ignore_case', async () => {
|
|
1629
|
+
const base = await start();
|
|
1630
|
+
await fs.writeFile(`${KB_DIR}/case.md`, 'Needle\nneedle\nNEEDLE\n');
|
|
1631
|
+
expect((await grep(base, { pattern: 'NEEDLE', path: `${KB_DIR}/case.md` })).body.matches.map((m) => m.line)).toEqual([3]);
|
|
1632
|
+
expect((await grep(base, { pattern: 'NEEDLE', path: `${KB_DIR}/case.md`, ignore_case: true })).body.matches.map((m) => m.line)).toEqual([1, 2, 3]);
|
|
1633
|
+
expect((await grep(base, { pattern: 'NEEDLE', ignore_case: true })).body.matches.map((m) => m.line)).toEqual([1, 2, 3]);
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
it('searches a pattern and a path beginning with `-` as text, never as an option', async () => {
|
|
1637
|
+
const base = await start();
|
|
1638
|
+
await fs.writeFile(`${KB_DIR}/-dir/-n.md`, 'run it with -n\nnothing here\n');
|
|
1639
|
+
for (const path of [undefined, `${KB_DIR}/-dir`, `${KB_DIR}/-dir/-n.md`, '-dir']) {
|
|
1640
|
+
const res = await grep(base, path === undefined ? { pattern: '-n' } : { pattern: '-n', path });
|
|
1641
|
+
expect(res.status).toBe(200);
|
|
1642
|
+
expect(res.body.matches).toEqual([{ path: path === '-dir' ? `${KB_DIR}/-dir/-n.md` : `${KB_DIR}/-dir/-n.md`, line: 1, text: 'run it with -n' }]);
|
|
1643
|
+
}
|
|
1644
|
+
// `--files` as a pattern is a pattern too: it matches nothing here rather than listing files.
|
|
1645
|
+
expect((await grep(base, { pattern: '--files' })).body.matches).toEqual([]);
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
it('a pattern carrying a NUL is refused with the escape to use instead', async () => {
|
|
1649
|
+
const base = await start();
|
|
1650
|
+
const res = await grep(base, { pattern: 'a\u0000b' });
|
|
1651
|
+
expect(res.status).toBe(400);
|
|
1652
|
+
expect(res.body.error).toContain('\\x00');
|
|
1394
1653
|
});
|
|
1395
1654
|
});
|
|
1396
1655
|
|
|
@@ -1865,30 +2124,12 @@ describe('office documents and PDFs', () => {
|
|
|
1865
2124
|
// Narrowed by hand: the assertion above already failed the test if not.
|
|
1866
2125
|
if (!compiled.checkable) throw new Error(compiled.reason);
|
|
1867
2126
|
expect(compiled.check(exampleArguments(def.inputs)), def.name).toEqual([]);
|
|
1868
|
-
// And the FLAT schema, which is what the tool's own route checks the
|
|
1869
|
-
// call against: the two must agree, or a call the example produced would
|
|
1870
|
-
// be refused one layer in.
|
|
1871
|
-
const flat = routeToolSchemas(def.name)?.flat;
|
|
1872
|
-
expect(flat, def.name).toBeDefined();
|
|
1873
|
-
const flatCheck = compileCheck(flat);
|
|
1874
|
-
expect(flatCheck.checkable, `${def.name} (flat): ${flatCheck.checkable ? '' : flatCheck.reason}`).toBe(true);
|
|
1875
|
-
if (!flatCheck.checkable) throw new Error(flatCheck.reason);
|
|
1876
|
-
expect(flatCheck.check(exampleArguments(flat)), `${def.name} (flat)`).toEqual([]);
|
|
1877
|
-
}
|
|
1878
|
-
});
|
|
1879
|
-
|
|
1880
|
-
it('every description, call example and purpose prefix included, stays inside the cap a client shows', async () => {
|
|
1881
|
-
await start();
|
|
1882
|
-
const tools = await toolRegistry.listInternal();
|
|
1883
|
-
for (const def of tools) {
|
|
1884
|
-
// Measured as a CLIENT receives it: the call line, the purpose prefix at
|
|
1885
|
-
// its own cap (the four knowledge-base tools carry one), and the
|
|
1886
|
-
// description — the three things that ride one tool's entry.
|
|
1887
|
-
const received = clientVisibleLength(def);
|
|
1888
|
-
expect(received, `${def.name} is ${received} characters (cap ${TOOL_DESCRIPTION_CAP})`).toBeLessThanOrEqual(
|
|
1889
|
-
TOOL_DESCRIPTION_CAP,
|
|
1890
|
-
);
|
|
1891
2127
|
}
|
|
2128
|
+
// That the FLAT schema each route checks a call against agrees with the
|
|
2129
|
+
// example is the route layer's own property: tool-helpers'
|
|
2130
|
+
// `route-tool-schemas.catalog.test.ts` holds it over the whole catalog.
|
|
2131
|
+
// And that every description stays inside the cap a client shows is the
|
|
2132
|
+
// registry's: `tool-description-length.test.ts`, over the same catalog.
|
|
1892
2133
|
});
|
|
1893
2134
|
|
|
1894
2135
|
describe('binary capability contract: a text file, a document, an image and a zip', () => {
|
|
@@ -2216,7 +2457,7 @@ describe('start_session', () => {
|
|
|
2216
2457
|
undefined,
|
|
2217
2458
|
undefined /* downloads */,
|
|
2218
2459
|
starterPacks,
|
|
2219
|
-
|
|
2460
|
+
realDisk(),
|
|
2220
2461
|
);
|
|
2221
2462
|
app.use('/api', router);
|
|
2222
2463
|
server = await new Promise<HttpServer>((r) => {
|
|
@@ -2539,7 +2780,7 @@ describe('branch is a required parameter in the tool contract', () => {
|
|
|
2539
2780
|
undefined,
|
|
2540
2781
|
undefined,
|
|
2541
2782
|
// So `grep` is mounted; it spawns no worker, as nothing here searches.
|
|
2542
|
-
new
|
|
2783
|
+
new RipgrepPatternMatcher(),
|
|
2543
2784
|
);
|
|
2544
2785
|
return registry;
|
|
2545
2786
|
}
|
|
@@ -2997,7 +3238,8 @@ describe('preflight for moves and deletes', () => {
|
|
|
2997
3238
|
if (rel === 'sealed.md' || rel.endsWith('/sealed.md')) return { read: true, write: false, download: false, owner: false };
|
|
2998
3239
|
if (rel.startsWith('HR/')) return { read: true, write: true, download: false, owner: false };
|
|
2999
3240
|
if (rel.startsWith('Locked/')) return { read: true, write: false, download: false, owner: false };
|
|
3000
|
-
|
|
3241
|
+
// The folder itself answers as its contents do, as the resolver's scopes do.
|
|
3242
|
+
if (rel === 'Secret' || rel.startsWith('Secret/')) return { read: false, write: false, download: false, owner: false };
|
|
3001
3243
|
return { read: true, write: true, download: true, owner: true };
|
|
3002
3244
|
};
|
|
3003
3245
|
const rules = {
|
|
@@ -3014,6 +3256,7 @@ describe('preflight for moves and deletes', () => {
|
|
|
3014
3256
|
// preview learned to relocate them, which keeps every ordering and
|
|
3015
3257
|
// oracle test below about what it is about.
|
|
3016
3258
|
previewAccessAfterRelocation: async (_w: string, _u: string, _from: string, to: string) => verbsFor(to),
|
|
3259
|
+
prospectiveHolderChanges: async () => NO_CHANGES,
|
|
3017
3260
|
} as unknown as IAccessControl;
|
|
3018
3261
|
|
|
3019
3262
|
const call = async (base: string, tool: string, body: Record<string, unknown>) => {
|
|
@@ -3142,10 +3385,12 @@ describe('preflight for moves and deletes', () => {
|
|
|
3142
3385
|
writes.push(op.wsPath ?? '');
|
|
3143
3386
|
});
|
|
3144
3387
|
expect((await call(base, 'move_file', args)).body).toMatchObject({ moved: true });
|
|
3145
|
-
//
|
|
3146
|
-
//
|
|
3147
|
-
//
|
|
3148
|
-
|
|
3388
|
+
// The move's two ends first, as read_file tells it (what moves, and
|
|
3389
|
+
// the folder it lands in); then `Locked/refs.md` is named (left for
|
|
3390
|
+
// want of write access, its links listed), so the read hook hears of
|
|
3391
|
+
// it; the unreadable page and the transcript are never named, so it
|
|
3392
|
+
// never hears of them.
|
|
3393
|
+
expect(reads.sort()).toEqual([KB('Locked/refs.md'), KB('Sales/2026'), KB('Sales/deal.md'), KB('Sales/deal.md'), KB('Sales/index.md')]);
|
|
3149
3394
|
expect([...new Set(writes)].sort()).toEqual([KB('Sales/2026/deal.md'), KB('Sales/deal.md'), KB('Sales/index.md')]);
|
|
3150
3395
|
});
|
|
3151
3396
|
|
|
@@ -3191,6 +3436,32 @@ describe('preflight for moves and deletes', () => {
|
|
|
3191
3436
|
expect(await exists(args.src)).toBe(false);
|
|
3192
3437
|
});
|
|
3193
3438
|
|
|
3439
|
+
it('a caller who cannot read what moves is refused as read_file refuses them, on a draft too, and nothing is looked at', async () => {
|
|
3440
|
+
const resolved: string[] = [];
|
|
3441
|
+
const base = await start('write', {
|
|
3442
|
+
...rules,
|
|
3443
|
+
prospectiveHolderChanges: async (_w: string, from: string) => {
|
|
3444
|
+
resolved.push(from);
|
|
3445
|
+
return { lose: [{ kind: 'group', name: 'Board', verb: 'read' }], gain: [] };
|
|
3446
|
+
},
|
|
3447
|
+
} as unknown as IAccessControl);
|
|
3448
|
+
await fs.writeFile(KB('Secret/plan.md'), 'plan');
|
|
3449
|
+
// A draft is not write-gated, so only the read gate stands between a
|
|
3450
|
+
// caller and a move of a file they may not open — and it does: the dry
|
|
3451
|
+
// run, the unconfirmed move and the confirmed one are all read_file's
|
|
3452
|
+
// own refusal, with nobody resolved and nothing moved.
|
|
3453
|
+
const args = { branch: 'someone/draft', src: KB('Secret/plan.md'), dest: KB('Secret/old/plan.md') };
|
|
3454
|
+
const read = await call(base, 'read_file', { branch: args.branch, path: args.src });
|
|
3455
|
+
expect(read.status).toBe(403);
|
|
3456
|
+
for (const body of [{ ...args, dryRun: true }, args, { ...args, confirm: true }]) {
|
|
3457
|
+
const res = await call(base, 'move_file', body);
|
|
3458
|
+
expect(res.status).toBe(403);
|
|
3459
|
+
expect(res.body).toEqual(read.body);
|
|
3460
|
+
}
|
|
3461
|
+
expect(resolved).toEqual([]);
|
|
3462
|
+
expect(await exists(args.src)).toBe(true);
|
|
3463
|
+
});
|
|
3464
|
+
|
|
3194
3465
|
it('an access-changing move shows before/after and runs only with confirm: true', async () => {
|
|
3195
3466
|
const base = await seeded();
|
|
3196
3467
|
const args = { src: KB('Sales/deal.md'), dest: KB('HR/deal.md') };
|
|
@@ -3389,13 +3660,18 @@ describe('preflight for moves and deletes', () => {
|
|
|
3389
3660
|
expect(await exists(KB('Sales/moved'))).toBe(false);
|
|
3390
3661
|
});
|
|
3391
3662
|
|
|
3392
|
-
it('a
|
|
3663
|
+
it('a move into a folder the caller cannot read is refused as read_file refuses the folder, before the write verdict', async () => {
|
|
3393
3664
|
const base = await seeded();
|
|
3665
|
+
// The folder's rules are what a move preview would read back (the
|
|
3666
|
+
// caller's verdicts there, who else is affected), so the read refusal
|
|
3667
|
+
// comes first — not the write denial, which would say whether the path
|
|
3668
|
+
// can be proposed and name the folder's contents on a collision.
|
|
3394
3669
|
const run = await call(base, 'move_file', { src: KB('Sales/deal.md'), dest: KB('Secret/deal.md'), confirm: true });
|
|
3395
3670
|
expect(run.status).toBe(403);
|
|
3396
|
-
expect(run.body).
|
|
3671
|
+
expect(run.body.error).toContain(`permission to read "${KB('Secret')}"`);
|
|
3672
|
+
expect(run.body.kind).toBeUndefined();
|
|
3397
3673
|
expect(run.body.proposal).toBeUndefined();
|
|
3398
|
-
expect(
|
|
3674
|
+
expect(await exists(KB('Sales/deal.md'))).toBe(true);
|
|
3399
3675
|
});
|
|
3400
3676
|
|
|
3401
3677
|
it('a draft branch is not write-gated, so the same move is allowed there', async () => {
|
|
@@ -3966,7 +4242,8 @@ describe('preflight for moves and deletes', () => {
|
|
|
3966
4242
|
// The protocol itself, and what a platform folder does to it, are shared.
|
|
3967
4243
|
const rules = sharedFileRulesSection(testKbContext().layout);
|
|
3968
4244
|
expect(rules).toContain('`dryRun: true`');
|
|
3969
|
-
|
|
4245
|
+
// Anyone's: a move asks when it changes who else may open or edit the file too (`holderChanges`).
|
|
4246
|
+
expect(rules).toContain("A non-empty folder is deleted, and a move that changes anyone's access runs, only with `confirm: true`");
|
|
3970
4247
|
expect(rules).toContain('platform folder');
|
|
3971
4248
|
const dry = await call(base, 'delete_folder', { path: KB('Sales/archive'), dryRun: true });
|
|
3972
4249
|
const unconfirmed = await call(base, 'delete_folder', { path: KB('Sales/archive') });
|
|
@@ -4032,6 +4309,7 @@ describe('a write refused for permissions says whether and how to propose it', (
|
|
|
4032
4309
|
canOwner: async () => true,
|
|
4033
4310
|
canWriteBatchAtRef: async () => null,
|
|
4034
4311
|
previewAccessAfterRelocation: async () => ({ read: true, write: true, download: true, owner: true }),
|
|
4312
|
+
prospectiveHolderChanges: async () => NO_CHANGES,
|
|
4035
4313
|
eligibleWritersAtRef: async () => ({ roles: ['Sales Lead'], users: [{ name: 'Owner', email: 'owner@x' }] }),
|
|
4036
4314
|
} as unknown as IAccessControl;
|
|
4037
4315
|
return { ac, calls };
|
|
@@ -4568,17 +4846,38 @@ describe('agent read/write hooks', () => {
|
|
|
4568
4846
|
expect(writes).toEqual([]);
|
|
4569
4847
|
});
|
|
4570
4848
|
|
|
4571
|
-
it('grep tells the read hook about every file
|
|
4849
|
+
it('grep tells the read hook about every file it opens — a file with no matching line included — not only its root', async () => {
|
|
4572
4850
|
const base = await start();
|
|
4573
4851
|
record();
|
|
4574
4852
|
await fs.mkdir(`${KB_DIR}/KnowledgeBase/Product/Knowledge`, { recursive: true });
|
|
4575
4853
|
await fs.mkdir(`${KB_DIR}/KnowledgeBase/Legal/Knowledge`, { recursive: true });
|
|
4576
4854
|
await fs.writeFile(`${KB_DIR}/KnowledgeBase/Product/Knowledge/P.md`, 'needle');
|
|
4577
4855
|
await fs.writeFile(`${KB_DIR}/KnowledgeBase/Legal/Knowledge/L.md`, 'needle');
|
|
4856
|
+
// Searched and read by ripgrep like the others; it answers no line.
|
|
4857
|
+
await fs.writeFile(`${KB_DIR}/KnowledgeBase/Legal/Knowledge/Quiet.md`, 'nothing of the sort');
|
|
4578
4858
|
await post(`${base}/api/agent/tools/grep`, { pattern: 'needle', path: KB_DIR, sessionId: 's1' });
|
|
4579
4859
|
const paths = reads.map((r) => r.wsPath);
|
|
4580
4860
|
expect(paths).toContain(`${KB_DIR}/KnowledgeBase/Product/Knowledge/P.md`);
|
|
4581
4861
|
expect(paths).toContain(`${KB_DIR}/KnowledgeBase/Legal/Knowledge/L.md`);
|
|
4862
|
+
expect(paths).toContain(`${KB_DIR}/KnowledgeBase/Legal/Knowledge/Quiet.md`);
|
|
4863
|
+
expect(paths.filter((p) => p === `${KB_DIR}/KnowledgeBase/Legal/Knowledge/Quiet.md`)).toHaveLength(1);
|
|
4864
|
+
});
|
|
4865
|
+
|
|
4866
|
+
it('a read hook that refuses a file the search would open ends the grep as it ends a read_file, even one that matches nothing', async () => {
|
|
4867
|
+
const base = await start();
|
|
4868
|
+
record();
|
|
4869
|
+
hooks.onAgentRead(async (op) => {
|
|
4870
|
+
if (op.wsPath === `${KB_DIR}/KnowledgeBase/Legal/Knowledge/Quiet.md`) throw new ToolError('Quiet.md is off limits to this run', 403);
|
|
4871
|
+
});
|
|
4872
|
+
await fs.mkdir(`${KB_DIR}/KnowledgeBase/Product/Knowledge`, { recursive: true });
|
|
4873
|
+
await fs.mkdir(`${KB_DIR}/KnowledgeBase/Legal/Knowledge`, { recursive: true });
|
|
4874
|
+
await fs.writeFile(`${KB_DIR}/KnowledgeBase/Product/Knowledge/P.md`, 'needle');
|
|
4875
|
+
await fs.writeFile(`${KB_DIR}/KnowledgeBase/Legal/Knowledge/Quiet.md`, 'nothing of the sort');
|
|
4876
|
+
const res = await post(`${base}/api/agent/tools/grep`, { pattern: 'needle', path: KB_DIR, sessionId: 's1' });
|
|
4877
|
+
expect(res.status).toBe(403);
|
|
4878
|
+
expect(await res.json()).toMatchObject({ error: 'Quiet.md is off limits to this run' });
|
|
4879
|
+
// The refusal came at admission: the hook heard of the file, and the search answered nothing from any file.
|
|
4880
|
+
expect(reads.map((r) => r.wsPath)).toContain(`${KB_DIR}/KnowledgeBase/Legal/Knowledge/Quiet.md`);
|
|
4582
4881
|
});
|
|
4583
4882
|
|
|
4584
4883
|
it('the write hook covers write_file, edit_file and mkdir, once per path', async () => {
|
|
@@ -4854,7 +5153,7 @@ describe('a move preview judges the destination as it will be', () => {
|
|
|
4854
5153
|
ensureRemotesFetched: async () => undefined,
|
|
4855
5154
|
} as unknown as WorkspaceService,
|
|
4856
5155
|
KB_DIR,
|
|
4857
|
-
|
|
5156
|
+
realDisk(),
|
|
4858
5157
|
);
|
|
4859
5158
|
return {
|
|
4860
5159
|
canRead: (w: string, u: string, r: string) => fresh().canRead(w, u, r),
|
|
@@ -4866,6 +5165,7 @@ describe('a move preview judges the destination as it will be', () => {
|
|
|
4866
5165
|
previewAccessAfterRelocation: (
|
|
4867
5166
|
w: string, u: string, from: string, to: string, opts?: { sourceRemains?: boolean },
|
|
4868
5167
|
) => fresh().previewAccessAfterRelocation(w, u, from, to, opts),
|
|
5168
|
+
prospectiveHolderChanges: (w: string, from: string, to: string) => fresh().prospectiveHolderChanges(w, from, to),
|
|
4869
5169
|
} as unknown as IAccessControl;
|
|
4870
5170
|
};
|
|
4871
5171
|
|
|
@@ -4909,6 +5209,8 @@ describe('a move preview judges the destination as it will be', () => {
|
|
|
4909
5209
|
expect(dry.body).toMatchObject({ kind: 'folder', accessChanges: false, allowed: true, moved: false });
|
|
4910
5210
|
expect(dry.body.access.after).toEqual(dry.body.access.before);
|
|
4911
5211
|
expect(dry.body.access.after.owner).toBe(true);
|
|
5212
|
+
// A rename carries its own rules along: nobody else gains or loses either.
|
|
5213
|
+
expect(dry.body.holderChanges).toBeUndefined();
|
|
4912
5214
|
// Nothing moved and nothing was written to answer the question.
|
|
4913
5215
|
expect(await fs.exists(args.dest)).toBe(false);
|
|
4914
5216
|
expect(await fs.exists(KB('Sales/access.md'))).toBe(true);
|
|
@@ -4933,6 +5235,91 @@ describe('a move preview judges the destination as it will be', () => {
|
|
|
4933
5235
|
expect(await verbsAt(base, args.dest)).toEqual(dry.body.access.after);
|
|
4934
5236
|
});
|
|
4935
5237
|
|
|
5238
|
+
it('a move that changes only who ELSE may edit it names them, and asks before it moves', async () => {
|
|
5239
|
+
const base = await seeded();
|
|
5240
|
+
// `Ops/` gives the caller what `Sales/` gives, and Ops Lead write as well.
|
|
5241
|
+
await fs.writeFile(
|
|
5242
|
+
KB('Ops/access.md'),
|
|
5243
|
+
rules(`write:\n - Mover <${MOVER}>\n - Ops Lead <ops@x.io>\nowner:\n - Mover <${MOVER}>\n`),
|
|
5244
|
+
);
|
|
5245
|
+
const args = { src: KB('Sales/deal.md'), dest: KB('Ops/deal.md') };
|
|
5246
|
+
|
|
5247
|
+
const dry = await call(base, 'move_file', { ...args, dryRun: true });
|
|
5248
|
+
expect(dry.body).toMatchObject({ accessChanges: false, allowed: true });
|
|
5249
|
+
// Everyone may read on both sides, so the only change is who may edit.
|
|
5250
|
+
expect(dry.body.holderChanges).toEqual({
|
|
5251
|
+
lose: [],
|
|
5252
|
+
gain: [{ kind: 'person', name: 'ops@x.io', email: 'ops@x.io', verb: 'write' }],
|
|
5253
|
+
});
|
|
5254
|
+
|
|
5255
|
+
const unconfirmed = await call(base, 'move_file', args);
|
|
5256
|
+
expect(unconfirmed.body).toMatchObject({ confirmationRequired: true, moved: false });
|
|
5257
|
+
expect(unconfirmed.body.message).toContain('`holderChanges`');
|
|
5258
|
+
expect(await fs.exists(args.src)).toBe(true);
|
|
5259
|
+
expect((await call(base, 'move_file', { ...args, confirm: true })).body).toMatchObject({ moved: true });
|
|
5260
|
+
});
|
|
5261
|
+
|
|
5262
|
+
it('a source the read hook refuses is refused as read_file refuses it: no preview, not even that the move would ask', async () => {
|
|
5263
|
+
const base = await seeded();
|
|
5264
|
+
await fs.writeFile(
|
|
5265
|
+
KB('Ops/access.md'),
|
|
5266
|
+
rules(`write:\n - Mover <${MOVER}>\n - Ops Lead <ops@x.io>\nowner:\n - Mover <${MOVER}>\n`),
|
|
5267
|
+
);
|
|
5268
|
+
const args = { src: KB('Sales/deal.md'), dest: KB('Ops/deal.md'), rewriteLinks: false };
|
|
5269
|
+
// The rules let the caller read it; a deployment's read hook does not. The
|
|
5270
|
+
// hook's refusal is the whole answer: a "this move asks for confirm" would
|
|
5271
|
+
// already say what the comparison found behind the hook.
|
|
5272
|
+
hooks.onAgentRead(async (op) => {
|
|
5273
|
+
if (op.wsPath === args.src) throw new Error('not for agents');
|
|
5274
|
+
});
|
|
5275
|
+
const read = await call(base, 'read_file', { path: args.src });
|
|
5276
|
+
expect(read.status).toBeGreaterThanOrEqual(400);
|
|
5277
|
+
for (const body of [{ ...args, dryRun: true }, args, { ...args, confirm: true }]) {
|
|
5278
|
+
const res = await call(base, 'move_file', body);
|
|
5279
|
+
expect(res.status).toBe(read.status);
|
|
5280
|
+
expect(res.body).toEqual(read.body);
|
|
5281
|
+
expect(JSON.stringify(res.body)).not.toContain('ops@x.io');
|
|
5282
|
+
}
|
|
5283
|
+
expect(await fs.exists(args.src)).toBe(true);
|
|
5284
|
+
});
|
|
5285
|
+
|
|
5286
|
+
it('a caller who reads what moves but not the folder it lands in is refused, on a draft too, and told nothing of the folder', async () => {
|
|
5287
|
+
const base = await seeded();
|
|
5288
|
+
// `Closed/` is somebody else's: its rules are what a move preview would
|
|
5289
|
+
// read back (who it lets in, whom it denies). On a draft no write gate
|
|
5290
|
+
// stands, so the read gate is what refuses — before anything is looked at.
|
|
5291
|
+
await fs.writeFile(
|
|
5292
|
+
KB('Closed/access.md'),
|
|
5293
|
+
rules('read:\n - Other <other@x.io>\n - deny everyone\nwrite:\n - Other <other@x.io>\n'),
|
|
5294
|
+
);
|
|
5295
|
+
await fs.writeFile(KB('Closed/deal.md'), 'taken');
|
|
5296
|
+
const args = { branch: 'someone/draft', src: KB('Sales/deal.md'), dest: KB('Closed/deal.md'), rewriteLinks: false };
|
|
5297
|
+
|
|
5298
|
+
for (const body of [{ ...args, dryRun: true }, args, { ...args, confirm: true }]) {
|
|
5299
|
+
const res = await call(base, 'move_file', body);
|
|
5300
|
+
expect(res.status).toBe(403);
|
|
5301
|
+
expect(res.body.error).toContain(`permission to read "${KB('Closed')}"`);
|
|
5302
|
+
// Neither the folder's rules nor what sits in it: the name clash at
|
|
5303
|
+
// `Closed/deal.md` is never reached.
|
|
5304
|
+
const text = JSON.stringify(res.body);
|
|
5305
|
+
expect(text).not.toContain('other@x.io');
|
|
5306
|
+
expect(text).not.toContain('already exists');
|
|
5307
|
+
}
|
|
5308
|
+
expect(await fs.exists(args.src)).toBe(true);
|
|
5309
|
+
expect(await fs.readFile(KB('Closed/deal.md'), { encoding: 'utf8' })).toBe('taken');
|
|
5310
|
+
});
|
|
5311
|
+
|
|
5312
|
+
it('a move out of a folder names everyone it leaves behind', async () => {
|
|
5313
|
+
const base = await seeded();
|
|
5314
|
+
const args = { src: KB('Work/Notes'), dest: KB('Legal/Notes') };
|
|
5315
|
+
const dry = await call(base, 'move_file', { ...args, dryRun: true });
|
|
5316
|
+
// Everyone may still read it in Legal/: the caller loses editing, not opening.
|
|
5317
|
+
expect(dry.body.holderChanges).toEqual({
|
|
5318
|
+
lose: [{ kind: 'person', name: MOVER, email: MOVER, verb: 'write' }],
|
|
5319
|
+
gain: [],
|
|
5320
|
+
});
|
|
5321
|
+
});
|
|
5322
|
+
|
|
4936
5323
|
it('what the folder inherited is left behind; what its own access.md gives comes along', async () => {
|
|
4937
5324
|
const base = await seeded();
|
|
4938
5325
|
const args = { src: KB('Work/Team'), dest: KB('Legal/Team') };
|