@bevel-software/platform-core-backend 0.33.0 → 0.34.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 +19 -8
- package/agent-guide/html-views.md +231 -5
- package/agent-guide/tool-manuals.md +1 -1
- 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 +14 -7
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +16 -5
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +37 -12
- 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 +17 -9
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +12 -2
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +164 -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 +35 -29
- 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/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.tools.d.ts.map +1 -1
- package/dist/modules/agent-guide/agent-guide.tools.js +13 -3
- package/dist/modules/agent-guide/agent-guide.tools.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 +3 -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 +16 -1
- 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/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 +68 -13
- 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/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 +15 -3
- package/dist/modules/secrets-vault/secrets-vault.routes.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 +24 -12
- 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-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/tool-manuals.contract.d.ts +19 -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 +13 -6
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +23 -15
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +25 -10
- 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 +14 -9
- 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/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.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +460 -116
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- 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/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 +3 -3
- package/src/__tests__/pg-scratch.test.ts +69 -0
- package/src/__tests__/pg-scratch.ts +31 -0
- package/src/__tests__/slow-ripgrep.ts +65 -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/catalog-revision.ts +24 -2
- package/src/core/core-ports.ts +42 -0
- package/src/core/create-core-server.ts +13 -0
- package/src/core/create-core-services.ts +49 -12
- 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 +2 -0
- 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__/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 +17 -10
- package/src/modules/access/access-control.service.ts +158 -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 +43 -33
- 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/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 +31 -0
- package/src/modules/agent-guide/agent-guide.tools.ts +13 -2
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +1 -0
- package/src/modules/agent-instructions/compose.ts +3 -1
- package/src/modules/agent-instructions/shared-file-rules.ts +17 -1
- 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/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 +107 -17
- 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/secrets-vault.routes.ts +23 -3
- 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 +44 -13
- 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__/can-write.tools.test.ts +3 -1
- package/src/modules/tool-helpers/tool-context.ts +10 -0
- 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.service.test.ts +42 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
- package/src/modules/tool-manuals/index.ts +6 -2
- package/src/modules/tool-manuals/tool-manuals.contract.ts +18 -6
- package/src/modules/tool-manuals/tool-manuals.routes.ts +40 -3
- package/src/modules/tool-manuals/tool-manuals.service.ts +31 -15
- package/src/modules/tool-manuals/tool-manuals.tools.ts +28 -11
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +2 -2
- 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 +14 -9
- 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 +2 -2
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +22 -10
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +393 -113
- 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 +500 -155
- package/src/shared/path-order.ts +37 -8
- 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/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,13 +8,14 @@ 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
13
|
import { ToolRegistry } from '../../tool-registry/tool-registry.js';
|
|
13
14
|
import { createToolHandlerFactory } from '../../tool-helpers/tool-handler.js';
|
|
14
15
|
import { ToolError, type ToolContext } from '../../tool-helpers/tool.contract.js';
|
|
15
16
|
import type { ToolAuth } from '../../tool-auth/tool-auth.middleware.js';
|
|
16
17
|
import { registerWorkspaceTools } from '../workspace.tools.js';
|
|
17
|
-
import {
|
|
18
|
+
import { RipgrepPatternMatcher, type RipgrepPatternMatcherOptions } from '../index.js';
|
|
18
19
|
import { sharedFileRules, sharedFileRulesSection } from '../../agent-instructions/shared-file-rules.js';
|
|
19
20
|
import { composeAgentGuide } from '../../agent-guide/agent-guide.js';
|
|
20
21
|
import { GUIDE_FIRST_SENTENCE } from '../../tool-registry/guide-first.js';
|
|
@@ -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) });
|
|
1316
|
+
|
|
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
|
+
});
|
|
1288
1402
|
|
|
1289
|
-
it('
|
|
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
|
|
|
@@ -2539,7 +2798,7 @@ describe('branch is a required parameter in the tool contract', () => {
|
|
|
2539
2798
|
undefined,
|
|
2540
2799
|
undefined,
|
|
2541
2800
|
// So `grep` is mounted; it spawns no worker, as nothing here searches.
|
|
2542
|
-
new
|
|
2801
|
+
new RipgrepPatternMatcher(),
|
|
2543
2802
|
);
|
|
2544
2803
|
return registry;
|
|
2545
2804
|
}
|
|
@@ -4568,17 +4827,38 @@ describe('agent read/write hooks', () => {
|
|
|
4568
4827
|
expect(writes).toEqual([]);
|
|
4569
4828
|
});
|
|
4570
4829
|
|
|
4571
|
-
it('grep tells the read hook about every file
|
|
4830
|
+
it('grep tells the read hook about every file it opens — a file with no matching line included — not only its root', async () => {
|
|
4572
4831
|
const base = await start();
|
|
4573
4832
|
record();
|
|
4574
4833
|
await fs.mkdir(`${KB_DIR}/KnowledgeBase/Product/Knowledge`, { recursive: true });
|
|
4575
4834
|
await fs.mkdir(`${KB_DIR}/KnowledgeBase/Legal/Knowledge`, { recursive: true });
|
|
4576
4835
|
await fs.writeFile(`${KB_DIR}/KnowledgeBase/Product/Knowledge/P.md`, 'needle');
|
|
4577
4836
|
await fs.writeFile(`${KB_DIR}/KnowledgeBase/Legal/Knowledge/L.md`, 'needle');
|
|
4837
|
+
// Searched and read by ripgrep like the others; it answers no line.
|
|
4838
|
+
await fs.writeFile(`${KB_DIR}/KnowledgeBase/Legal/Knowledge/Quiet.md`, 'nothing of the sort');
|
|
4578
4839
|
await post(`${base}/api/agent/tools/grep`, { pattern: 'needle', path: KB_DIR, sessionId: 's1' });
|
|
4579
4840
|
const paths = reads.map((r) => r.wsPath);
|
|
4580
4841
|
expect(paths).toContain(`${KB_DIR}/KnowledgeBase/Product/Knowledge/P.md`);
|
|
4581
4842
|
expect(paths).toContain(`${KB_DIR}/KnowledgeBase/Legal/Knowledge/L.md`);
|
|
4843
|
+
expect(paths).toContain(`${KB_DIR}/KnowledgeBase/Legal/Knowledge/Quiet.md`);
|
|
4844
|
+
expect(paths.filter((p) => p === `${KB_DIR}/KnowledgeBase/Legal/Knowledge/Quiet.md`)).toHaveLength(1);
|
|
4845
|
+
});
|
|
4846
|
+
|
|
4847
|
+
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 () => {
|
|
4848
|
+
const base = await start();
|
|
4849
|
+
record();
|
|
4850
|
+
hooks.onAgentRead(async (op) => {
|
|
4851
|
+
if (op.wsPath === `${KB_DIR}/KnowledgeBase/Legal/Knowledge/Quiet.md`) throw new ToolError('Quiet.md is off limits to this run', 403);
|
|
4852
|
+
});
|
|
4853
|
+
await fs.mkdir(`${KB_DIR}/KnowledgeBase/Product/Knowledge`, { recursive: true });
|
|
4854
|
+
await fs.mkdir(`${KB_DIR}/KnowledgeBase/Legal/Knowledge`, { recursive: true });
|
|
4855
|
+
await fs.writeFile(`${KB_DIR}/KnowledgeBase/Product/Knowledge/P.md`, 'needle');
|
|
4856
|
+
await fs.writeFile(`${KB_DIR}/KnowledgeBase/Legal/Knowledge/Quiet.md`, 'nothing of the sort');
|
|
4857
|
+
const res = await post(`${base}/api/agent/tools/grep`, { pattern: 'needle', path: KB_DIR, sessionId: 's1' });
|
|
4858
|
+
expect(res.status).toBe(403);
|
|
4859
|
+
expect(await res.json()).toMatchObject({ error: 'Quiet.md is off limits to this run' });
|
|
4860
|
+
// The refusal came at admission: the hook heard of the file, and the search answered nothing from any file.
|
|
4861
|
+
expect(reads.map((r) => r.wsPath)).toContain(`${KB_DIR}/KnowledgeBase/Legal/Knowledge/Quiet.md`);
|
|
4582
4862
|
});
|
|
4583
4863
|
|
|
4584
4864
|
it('the write hook covers write_file, edit_file and mkdir, once per path', async () => {
|
|
@@ -152,6 +152,16 @@ describe('DocExtractService — how long a failure is remembered', () => {
|
|
|
152
152
|
expect(told).toHaveLength(1);
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
+
it('two reads of the same bytes at different paths share one extraction, and each marker names its own path', async () => {
|
|
156
|
+
const extractor = scriptedExtractor(() => ({ ok: true, summary: '1 page', text: '[page 1]\nshared' }));
|
|
157
|
+
const service = new DocExtractService(cacheRoot, extractor, { now, onUnreadable });
|
|
158
|
+
const bytes = Buffer.from('same bytes, two names');
|
|
159
|
+
const [a, b] = await Promise.all([service.extract('a/deck.pdf', bytes, ORIGIN), service.extract('b/copy.pdf', bytes, ORIGIN)]);
|
|
160
|
+
expect(extractor.calls).toBe(1);
|
|
161
|
+
expect(a).toEqual({ ok: true, marker: '[extracted text of a/deck.pdf — 1 page]', text: '[page 1]\nshared' });
|
|
162
|
+
expect(b).toEqual({ ok: true, marker: '[extracted text of b/copy.pdf — 1 page]', text: '[page 1]\nshared' });
|
|
163
|
+
});
|
|
164
|
+
|
|
155
165
|
it('still answers the read when whoever is told throws', async () => {
|
|
156
166
|
const extractor = scriptedExtractor(() => TIMEOUT);
|
|
157
167
|
const service = new DocExtractService(cacheRoot, extractor, {
|