@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
|
@@ -5,6 +5,7 @@ const log = logger('declared-variables');
|
|
|
5
5
|
import { utcpNamespacedKey } from '../../shared/utcp-namespace.js';
|
|
6
6
|
import type { IToolManualService } from '../tool-manuals/index.js';
|
|
7
7
|
import type { ISecretsVaultService } from '../secrets-vault/index.js';
|
|
8
|
+
import { resolveConnectionScope, type IAgentScopeResolver } from '../agent-scope/index.js';
|
|
8
9
|
|
|
9
10
|
type ResolveUserEmail = (userId: string) => Promise<string | undefined>;
|
|
10
11
|
|
|
@@ -48,6 +49,12 @@ export function createDeclaredVariableRoutes(
|
|
|
48
49
|
secretsVault: ISecretsVaultService,
|
|
49
50
|
manualAuth: RequestHandler,
|
|
50
51
|
resolveUserEmail: ResolveUserEmail,
|
|
52
|
+
/**
|
|
53
|
+
* Resolves the scope of a connection made as an agent configuration: such a
|
|
54
|
+
* connection is handed only the variables of a connector the file loads.
|
|
55
|
+
* Absent, a narrowed connection is handed none.
|
|
56
|
+
*/
|
|
57
|
+
agentScope?: Pick<IAgentScopeResolver, 'resolve'>,
|
|
51
58
|
): express.Router {
|
|
52
59
|
const router = express.Router();
|
|
53
60
|
|
|
@@ -125,7 +132,11 @@ export function createDeclaredVariableRoutes(
|
|
|
125
132
|
// `listAccessible` already applies the per-file read verdict, so an
|
|
126
133
|
// unreadable manual is simply absent — same fail-closed read model the
|
|
127
134
|
// catalog uses.
|
|
128
|
-
const
|
|
135
|
+
const found = (await toolManualService.listAccessible(who.email)).find((m) => m.slug === slug);
|
|
136
|
+
// A connection made as an agent configuration is handed only the variables of
|
|
137
|
+
// a connector it loads; another is as absent as an unreadable one.
|
|
138
|
+
const scope = found ? await resolveConnectionScope(agentScope, who.email, req.toolAuth?.agentPath) : null;
|
|
139
|
+
const manual = found && scope?.allowsConnector(found.path) ? found : undefined;
|
|
129
140
|
if (!manual) return void res.status(404).json({ error: 'Not found' });
|
|
130
141
|
if (manual.remote !== false) {
|
|
131
142
|
return void res.status(409).json({
|
|
@@ -281,6 +281,18 @@ describe('open_page: refusals', () => {
|
|
|
281
281
|
expect(embedService.mintForUser).not.toHaveBeenCalled();
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
+
it('refuses a tmp ref — no knowledge-base page — reading and minting nothing', async () => {
|
|
285
|
+
const read = vi.fn<ReadForTool>();
|
|
286
|
+
const { call, embedService } = await serve({ read });
|
|
287
|
+
const { status, body } = await call({ path: '__tmp__/chat-uploads/plan.xlsx' });
|
|
288
|
+
expect(status).toBe(400);
|
|
289
|
+
expect(body.kind).toBe('tmp-ref-read-only');
|
|
290
|
+
expect(body.error).toContain('open_page does not take one there');
|
|
291
|
+
expect(body.error).toContain('copy_file');
|
|
292
|
+
expect(read).not.toHaveBeenCalled();
|
|
293
|
+
expect(embedService.mintForUser).not.toHaveBeenCalled();
|
|
294
|
+
});
|
|
295
|
+
|
|
284
296
|
it('refuses a path outside the knowledge base, and a missing one', async () => {
|
|
285
297
|
const { call, embedService } = await serve();
|
|
286
298
|
expect((await call({ path: '' })).status).toBe(400);
|
|
@@ -6,6 +6,7 @@ import type { KbContext } from '../../shared/kb-context.js';
|
|
|
6
6
|
import type { ReadForTool } from '../workspace/index.js';
|
|
7
7
|
import type { IEmbedService } from './embed.interface.js';
|
|
8
8
|
import { requireExternalSource } from '../tool-auth/index.js';
|
|
9
|
+
import { assertTmpRefsTaken } from '../kb-fs/index.js';
|
|
9
10
|
import { SESSION_ID_INPUT, type ToolDescriptionNotes, sessionIdInputOf } from '../workspace/index.js';
|
|
10
11
|
|
|
11
12
|
/** The tool name, in one place: the app manifest keys its view by it. */
|
|
@@ -149,6 +150,10 @@ export function registerEmbedTools(
|
|
|
149
150
|
toolHandler(async (args, ctx: ToolContext) => {
|
|
150
151
|
const raw = typeof args.path === 'string' ? args.path.trim() : '';
|
|
151
152
|
if (!raw) throw new ToolError('`path` is required.', 400);
|
|
153
|
+
// A tmp ref (`__tmp__/…`) is no knowledge-base page: refused by the one
|
|
154
|
+
// admission gate the normalised tools pass, before `toRepoRelative`
|
|
155
|
+
// would read it as the repository path `<kbDirName>/__tmp__/…`.
|
|
156
|
+
assertTmpRefsTaken(OPEN_PAGE_TOOL, { path: raw });
|
|
152
157
|
const heading = typeof args.heading === 'string' && args.heading.trim() ? args.heading.trim() : undefined;
|
|
153
158
|
const repoRelative = toRepoRelative(raw, kbDirName);
|
|
154
159
|
if (repoRelative === null) throw new ToolError(OUTSIDE_REPO(kbDirName), 400);
|
|
@@ -1482,6 +1482,31 @@ describe('LockingFilesystem — a destination that is taken is refused, never re
|
|
|
1482
1482
|
expect(await fs.readFile(onDisk(TAKEN), 'utf-8')).toBe('the winner of the race');
|
|
1483
1483
|
});
|
|
1484
1484
|
|
|
1485
|
+
/**
|
|
1486
|
+
* The same race for an EXCLUSIVE write (`overwrite: false`), which is how
|
|
1487
|
+
* `copy_file` lands bytes read out of the workspace's tmp folder: the
|
|
1488
|
+
* refusal is the clash sentence, and the lock releases untouched rather
|
|
1489
|
+
* than discarding the winner's queued file.
|
|
1490
|
+
*/
|
|
1491
|
+
it('an exclusive write onto a taken name is the same refusal, and releases UNTOUCHED', async () => {
|
|
1492
|
+
const workflow = makeWorkflow();
|
|
1493
|
+
const layer = new LockingFilesystem(
|
|
1494
|
+
{ basePath: root, contained: true },
|
|
1495
|
+
{ workflow, workspaceId: 'ws-feat', branch: 'feat', user: USER, kbDirName: KB },
|
|
1496
|
+
);
|
|
1497
|
+
await fs.writeFile(onDisk(TAKEN), 'the winner of the race');
|
|
1498
|
+
|
|
1499
|
+
await expect(layer.writeFile(TAKEN, 'the loser', { overwrite: false })).rejects.toMatchObject({
|
|
1500
|
+
name: 'DestinationTakenError',
|
|
1501
|
+
message: 'A file named taken.md already exists in KnowledgeBase.',
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
expect(workflow.releaseLockUntouched).toHaveBeenCalledWith('ws-feat', 'feat', TAKEN, USER);
|
|
1505
|
+
expect(workflow.releaseLockNoCommit).not.toHaveBeenCalled();
|
|
1506
|
+
expect(workflow.releaseLock).not.toHaveBeenCalled();
|
|
1507
|
+
expect(await fs.readFile(onDisk(TAKEN), 'utf-8')).toBe('the winner of the race');
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1485
1510
|
it.skipIf(process.platform === 'win32')('answers the clash sentence when a DANGLING link holds the name', async () => {
|
|
1486
1511
|
// A link to nothing still owns the directory entry, so the exclusive
|
|
1487
1512
|
// create fails — and an existence probe that follows links would say the
|
|
@@ -231,8 +231,8 @@ describe('a line break in a path — one name to the gates, two object names to
|
|
|
231
231
|
});
|
|
232
232
|
|
|
233
233
|
it('refuses it through normalizePathArgs, which is where the MCP tools meet the rule', () => {
|
|
234
|
-
expect(() => normalizePathArgs({ path: INJECTED }, KB)).toThrow(/control character/);
|
|
235
|
-
expect(() => normalizePathArgs({ files: [{ path: INJECTED }] }, KB)).toThrow(/control character/);
|
|
234
|
+
expect(() => normalizePathArgs({ path: INJECTED }, KB, { tool: 'read_file' })).toThrow(/control character/);
|
|
235
|
+
expect(() => normalizePathArgs({ files: [{ path: INJECTED }] }, KB, { tool: 'read_file' })).toThrow(/control character/);
|
|
236
236
|
});
|
|
237
237
|
});
|
|
238
238
|
|
|
@@ -360,6 +360,8 @@ describe('assertRepoRootNameFree', () => {
|
|
|
360
360
|
});
|
|
361
361
|
|
|
362
362
|
describe('normalizePathArgs', () => {
|
|
363
|
+
const T = { tool: 'read_file' };
|
|
364
|
+
|
|
363
365
|
it('normalizes every path-shaped tool argument, batch entries included', () => {
|
|
364
366
|
expect(
|
|
365
367
|
normalizePathArgs(
|
|
@@ -372,6 +374,7 @@ describe('normalizePathArgs', () => {
|
|
|
372
374
|
files: [{ path: 'e.md', content: '/x' }],
|
|
373
375
|
},
|
|
374
376
|
KB,
|
|
377
|
+
T,
|
|
375
378
|
),
|
|
376
379
|
).toEqual({
|
|
377
380
|
branch: '/main',
|
|
@@ -385,22 +388,64 @@ describe('normalizePathArgs', () => {
|
|
|
385
388
|
|
|
386
389
|
it('leaves a spill ref alone — it belongs to no workspace', () => {
|
|
387
390
|
expect(
|
|
388
|
-
normalizePathArgs({ path: '__tool_chain_spill__/abc.json' }, KB, (v) => v.startsWith('__tool_chain_spill__/')),
|
|
391
|
+
normalizePathArgs({ path: '__tool_chain_spill__/abc.json' }, KB, { tool: 'read_file', skip: (v) => v.startsWith('__tool_chain_spill__/') }),
|
|
389
392
|
).toEqual({ path: '__tool_chain_spill__/abc.json' });
|
|
390
393
|
});
|
|
391
394
|
|
|
395
|
+
it('still takes the earlier call form, a skip predicate or nothing, and refuses a tmp ref in every input there', () => {
|
|
396
|
+
const spill = (v: string) => v.startsWith('__tool_chain_spill__/');
|
|
397
|
+
expect(normalizePathArgs({ path: '__tool_chain_spill__/abc.json' }, KB, spill)).toEqual({ path: '__tool_chain_spill__/abc.json' });
|
|
398
|
+
expect(normalizePathArgs({ path: 'a.md' }, KB)).toEqual({ path: 'knowledge-base/a.md' });
|
|
399
|
+
for (const legacy of [undefined, spill]) {
|
|
400
|
+
expect(() => normalizePathArgs({ path: '__tmp__/x.md' }, KB, legacy)).toThrow(/this tool does not take one there/);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
392
404
|
it('leaves an absent or empty path to the handler\'s own required-argument message', () => {
|
|
393
|
-
expect(normalizePathArgs({ path: undefined }, KB)).toEqual({ path: undefined });
|
|
394
|
-
expect(normalizePathArgs({ path: '' }, KB)).toEqual({ path: '' });
|
|
395
|
-
expect(normalizePathArgs({ branch: 'main' }, KB)).toEqual({ branch: 'main' });
|
|
405
|
+
expect(normalizePathArgs({ path: undefined }, KB, T)).toEqual({ path: undefined });
|
|
406
|
+
expect(normalizePathArgs({ path: '' }, KB, T)).toEqual({ path: '' });
|
|
407
|
+
expect(normalizePathArgs({ branch: 'main' }, KB, T)).toEqual({ branch: 'main' });
|
|
396
408
|
});
|
|
397
409
|
|
|
398
410
|
it('collapses the spellings so two callers take one lock row', () => {
|
|
399
|
-
expect(normalizePathArgs({ path: './knowledge-base//a.md' }, KB)).toEqual({ path: 'knowledge-base/a.md' });
|
|
411
|
+
expect(normalizePathArgs({ path: './knowledge-base//a.md' }, KB, T)).toEqual({ path: 'knowledge-base/a.md' });
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('admits a tmp ref, as written, in exactly the inputs the tool names', () => {
|
|
415
|
+
expect(normalizePathArgs({ src: '__tmp__/a.pdf', dest: 'a.pdf' }, KB, { tool: 'copy_file', tmpRefInputs: ['src'] })).toEqual({
|
|
416
|
+
src: '__tmp__/a.pdf',
|
|
417
|
+
dest: 'knowledge-base/a.pdf',
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('refuses a tmp ref in every other input, lists included, naming the tool and copy_file', () => {
|
|
422
|
+
const calls: Array<Record<string, unknown>> = [
|
|
423
|
+
{ path: '__tmp__/a.md' },
|
|
424
|
+
{ src: '__tmp__/a.md' },
|
|
425
|
+
{ dest: '__tmp__/a.md' },
|
|
426
|
+
{ destination: '__tmp__' },
|
|
427
|
+
{ files: [{ path: 'ok.md' }, { path: '__tmp__/a.md' }] },
|
|
428
|
+
{ paths: ['ok.md', '__tmp__/a.md'] },
|
|
429
|
+
{ resolutions: [{ path: 'ok.md', choose: 'ours' }, { path: '__tmp__/a.md', content: 'x' }] },
|
|
430
|
+
];
|
|
431
|
+
for (const args of calls) {
|
|
432
|
+
expect(() => normalizePathArgs(args, KB, { tool: 'save_file' })).toThrow(
|
|
433
|
+
/is a tmp ref, and save_file does not take one there: the workspace's tmp folder is read-only through the ref\..*copy_file/,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
// A tool that takes a ref in `src` still refuses one in `dest`.
|
|
437
|
+
expect(() => normalizePathArgs({ dest: '__tmp__/a.md' }, KB, { tool: 'copy_file', tmpRefInputs: ['src'] })).toThrow(
|
|
438
|
+
/copy_file does not take one there/,
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('reads a path that only contains the prefix as an ordinary repository path', () => {
|
|
443
|
+
expect(normalizePathArgs({ path: 'knowledge-base/__tmp__/a.md' }, KB, T)).toEqual({ path: 'knowledge-base/__tmp__/a.md' });
|
|
444
|
+
expect(normalizePathArgs({ path: '/knowledge-base/__tmp__/a.md' }, KB, T)).toEqual({ path: 'knowledge-base/__tmp__/a.md' });
|
|
400
445
|
});
|
|
401
446
|
|
|
402
447
|
it('refuses a traversing tool argument', () => {
|
|
403
|
-
expect(() => normalizePathArgs({ path: '../escape.md' }, KB)).toThrow(/outside the knowledge base repository/);
|
|
448
|
+
expect(() => normalizePathArgs({ path: '../escape.md' }, KB, T)).toThrow(/outside the knowledge base repository/);
|
|
404
449
|
});
|
|
405
450
|
});
|
|
406
451
|
|
|
@@ -8,6 +8,7 @@ export { WorkspaceMutex, WorkspaceIdMutex } from './mutex.js';
|
|
|
8
8
|
export { readRefSha, readRevSha, type RefFileAnswer } from './ref-files.js';
|
|
9
9
|
export { gitDirOf, readGitfile } from './git-dir.js';
|
|
10
10
|
export { normalizeWorkspacePath } from './repo-path.js';
|
|
11
|
+
export { TMP_REF_PREFIX, assertTmpRefsTaken, isTmpRef, tmpRefReadOnlyRefusal } from './tmp-ref.js';
|
|
11
12
|
// Reached by other modules; exported here so they stop importing the files.
|
|
12
13
|
export { BevelIgnoreStack } from './bevel-ignore.js';
|
|
13
14
|
export { assertValidBranchName, assertValidRelativePath, isBranchAuthoredBy, isOwnSuggestionsBranch } from './branch-name.js';
|
|
@@ -18,3 +19,4 @@ export { LockingFilesystem, MoveLockedError, MoveRacedError } from './locking-fi
|
|
|
18
19
|
export { ReadOnlyFilesystem } from './read-only-filesystem.js';
|
|
19
20
|
export { normalizeRepositoryAddress, sameRepository } from './remote-url.js';
|
|
20
21
|
export { assertInsideRepo, assertKbDirNameFree, assertRepoRootNameFree, assertRepoRootNameFreeArgs, hasControlCharacter, isInsideRepo, normalizePathArgs } from './repo-path.js';
|
|
22
|
+
export type { NormalizePathArgsOptions } from './repo-path.js';
|
|
@@ -226,9 +226,15 @@ export class LockingFilesystem extends GitGuardedFilesystem {
|
|
|
226
226
|
const plan = await this.planCreate(inputPath, 'file');
|
|
227
227
|
if (plan?.kind === 'seed-access-md') await this.seedAccessMd(plan);
|
|
228
228
|
const toWrite = content;
|
|
229
|
+
// An EXCLUSIVE write (`overwrite: false`, the `wx` flag) that finds the
|
|
230
|
+
// name taken is refused as a copy is (`asTakenRefusal`): the refusal is a
|
|
231
|
+
// `DestinationTakenError`, so the lock is released untouched. Left as the
|
|
232
|
+
// raw failure it would be a failed op, and the discard that follows resets
|
|
233
|
+
// the PATH — throwing away the winner's file, whose commit is still queued.
|
|
234
|
+
const land = () => super.writeFile(inputPath, toWrite, options);
|
|
229
235
|
return this.withLock(
|
|
230
236
|
inputPath,
|
|
231
|
-
() =>
|
|
237
|
+
() => (options?.overwrite === false ? this.asTakenRefusal(inputPath, land) : land()),
|
|
232
238
|
(check || validate) &&
|
|
233
239
|
(async () => {
|
|
234
240
|
// The caller's verdict first: it decides whether this write happens
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { WorkflowValidationError } from '../../shared/domain-errors.js';
|
|
3
3
|
import { sanitizedPath } from '../../shared/printable.js';
|
|
4
|
+
import { assertTmpRefsTaken, isTmpRef } from './tmp-ref.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Workspace-relative paths and the repository folder.
|
|
@@ -312,6 +313,16 @@ export function assertKbDirNameFree(
|
|
|
312
313
|
);
|
|
313
314
|
}
|
|
314
315
|
|
|
316
|
+
/** How {@link normalizePathArgs} treats one tool's call. */
|
|
317
|
+
export interface NormalizePathArgsOptions {
|
|
318
|
+
/** The tool the call is to, named in a tmp-ref refusal. */
|
|
319
|
+
tool: string;
|
|
320
|
+
/** The inputs of `tool` that take a tmp ref (`__tmp__/…`); every other refuses one. */
|
|
321
|
+
tmpRefInputs?: readonly string[];
|
|
322
|
+
/** A value that is no workspace path at all, passed to the handler as written. */
|
|
323
|
+
skip?: (value: string) => boolean;
|
|
324
|
+
}
|
|
325
|
+
|
|
315
326
|
/** The tool inputs that carry a workspace path. */
|
|
316
327
|
const PATH_ARG_KEYS = ['path', 'src', 'dest', 'destination'] as const;
|
|
317
328
|
|
|
@@ -321,19 +332,34 @@ const PATH_ARG_KEYS = ['path', 'src', 'dest', 'destination'] as const;
|
|
|
321
332
|
* copy. This is where the MCP surface meets the normaliser — one call, before
|
|
322
333
|
* any handler runs, so no tool keeps a resolver of its own.
|
|
323
334
|
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
335
|
+
* It is also where a tmp ref (`__tmp__/…`) is admitted or refused, first:
|
|
336
|
+
* {@link assertTmpRefsTaken} refuses one in every input of `tool` that
|
|
337
|
+
* `tmpRefInputs` does not name, and a ref in an input that does name it
|
|
338
|
+
* reaches the handler as written — so no tool whose paths pass through here
|
|
339
|
+
* can read the ref as the repository path `<kbDirName>/__tmp__/…`.
|
|
340
|
+
*
|
|
341
|
+
* `skip` is for the other input that is not a workspace path at all:
|
|
342
|
+
* `read_file` also takes a `__tool_chain_spill__/…` ref, which belongs to no
|
|
343
|
+
* workspace and must reach the handler as written. An absent or non-string
|
|
344
|
+
* value is left alone too — the handler's own required-argument message is
|
|
345
|
+
* better than a path refusal about `undefined`.
|
|
346
|
+
*
|
|
347
|
+
* The earlier call form — a `skip` predicate (or nothing) as the third
|
|
348
|
+
* argument — is still taken, for a distribution's own tool written against
|
|
349
|
+
* it: such a call names no tool and takes a tmp ref in no input, so the ref is
|
|
350
|
+
* refused there as read-only, under the name "this tool".
|
|
329
351
|
*/
|
|
330
352
|
export function normalizePathArgs(
|
|
331
353
|
args: Record<string, unknown>,
|
|
332
354
|
kbDirName: string,
|
|
333
|
-
|
|
355
|
+
options?: NormalizePathArgsOptions | ((value: string) => boolean),
|
|
334
356
|
): Record<string, unknown> {
|
|
357
|
+
const { tool, tmpRefInputs, skip } =
|
|
358
|
+
typeof options === 'object' ? options : { tool: 'this tool', tmpRefInputs: undefined, skip: options };
|
|
359
|
+
assertTmpRefsTaken(tool, args, tmpRefInputs);
|
|
360
|
+
// Every tmp ref still here is in an input that takes one.
|
|
335
361
|
const one = (value: unknown): unknown =>
|
|
336
|
-
typeof value !== 'string' || value.length === 0 || skip?.(value)
|
|
362
|
+
typeof value !== 'string' || value.length === 0 || isTmpRef(value) || skip?.(value)
|
|
337
363
|
? value
|
|
338
364
|
: normalizeWorkspacePath(value, kbDirName);
|
|
339
365
|
const out: Record<string, unknown> = { ...args };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { WorkflowValidationError } from '../../shared/domain-errors.js';
|
|
2
|
+
import { sanitizedPath } from '../../shared/printable.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The tmp ref's admission: which tool inputs may carry one, and the refusal
|
|
6
|
+
* every other input answers. What the ref REACHES is `workspace/workspace-tmp.ts`'s;
|
|
7
|
+
* whether a tool may be HANDED one is decided here, once, by
|
|
8
|
+
* {@link assertTmpRefsTaken} — which {@link normalizePathArgs} runs before it
|
|
9
|
+
* places any path under the checkout, so no tool surface that takes a path
|
|
10
|
+
* can turn `__tmp__/x` into the repository path `<kbDirName>/__tmp__/x` and
|
|
11
|
+
* write, read or show it as if the ref had been honoured. A surface that
|
|
12
|
+
* resolves its paths without the normaliser (`open_page`) calls the gate
|
|
13
|
+
* itself.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The ref's prefix. A ref is `__tmp__/<path inside the workspace's tmp folder>`. */
|
|
17
|
+
export const TMP_REF_PREFIX = '__tmp__/';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether a tool argument is a tmp ref rather than a repository path: the
|
|
21
|
+
* prefix as written, or the prefix's own folder name alone (which names the
|
|
22
|
+
* folder, and is refused as such by the resolver). A path that only CONTAINS
|
|
23
|
+
* the name — `/__tmp__/x`, `knowledge-base/__tmp__/x` — is an ordinary
|
|
24
|
+
* repository path.
|
|
25
|
+
*/
|
|
26
|
+
export function isTmpRef(value: unknown): boolean {
|
|
27
|
+
return typeof value === 'string' && (value === TMP_REF_PREFIX.slice(0, -1) || value.startsWith(TMP_REF_PREFIX));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Why a tool refuses a tmp ref in an input that is not one of the four that
|
|
32
|
+
* take it: the folder is read-only through the ref, and a file is kept by
|
|
33
|
+
* copying it into the repository. A 400 with a `kind` to branch on, before
|
|
34
|
+
* any gate, lock or write.
|
|
35
|
+
*/
|
|
36
|
+
export function tmpRefReadOnlyRefusal(ref: string, tool: string): WorkflowValidationError {
|
|
37
|
+
const shown = sanitizedPath(ref);
|
|
38
|
+
return new WorkflowValidationError(
|
|
39
|
+
`"${shown}" is a tmp ref, and ${tool} does not take one there: the workspace's tmp folder is read-only through ` +
|
|
40
|
+
'the ref. read_file, file_stat and grep read the one file it names; to keep the file, copy_file it to a ' +
|
|
41
|
+
'repository path (the ref as `src`, the repository path as `dest`).',
|
|
42
|
+
{ kind: 'tmp-ref-read-only', path: shown, tool },
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The tool inputs that carry a workspace path as a string. */
|
|
47
|
+
const PATH_INPUTS = ['path', 'src', 'dest', 'destination'] as const;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Every path a tool call carries, with the input it came in: the string
|
|
51
|
+
* inputs, and for EVERY list input, each entry that is a path itself
|
|
52
|
+
* (`paths` of `request_file_download`) or an object carrying one in `path`
|
|
53
|
+
* (`files[].path` of a batch write, `resolutions[].path` of
|
|
54
|
+
* `update_branch`). Read by shape rather than by a list of tool inputs, so a
|
|
55
|
+
* list a tool adds later is gated without anyone remembering to name it here.
|
|
56
|
+
*/
|
|
57
|
+
function pathEntries(args: Record<string, unknown>): Array<[string, unknown]> {
|
|
58
|
+
const entries: Array<[string, unknown]> = PATH_INPUTS.map((key) => [key, args[key]]);
|
|
59
|
+
for (const [key, value] of Object.entries(args)) {
|
|
60
|
+
if (!Array.isArray(value)) continue;
|
|
61
|
+
for (const item of value as unknown[]) {
|
|
62
|
+
if (typeof item === 'string') {
|
|
63
|
+
if (key === 'paths') entries.push([key, item]);
|
|
64
|
+
} else if (item && typeof item === 'object') {
|
|
65
|
+
entries.push([key, (item as Record<string, unknown>).path]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return entries;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Refuse a tmp ref in any input of `tool` that is not in `takes` — with
|
|
74
|
+
* {@link tmpRefReadOnlyRefusal}, before anything else runs. `takes` is empty
|
|
75
|
+
* for every tool but the four reads of the one file a ref names.
|
|
76
|
+
*/
|
|
77
|
+
export function assertTmpRefsTaken(tool: string, args: Record<string, unknown>, takes: readonly string[] = []): void {
|
|
78
|
+
for (const [key, value] of pathEntries(args)) {
|
|
79
|
+
if (isTmpRef(value) && !takes.includes(key)) throw tmpRefReadOnlyRefusal(value as string, tool);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
import { migrate } from 'drizzle-orm/node-postgres/migrator';
|
|
5
|
+
import type { OAuthClientInformationFull } from '@modelcontextprotocol/sdk/shared/auth.js';
|
|
6
|
+
import { BevelOAuthProvider } from '../oauth/bevel-oauth-provider.js';
|
|
7
|
+
import { ExternalApiKeyService } from '../../tool-auth/external-api-key.service.js';
|
|
8
|
+
import { closeDb, createDb, type Database } from '../../database/connection.js';
|
|
9
|
+
import { agentConnections, externalApiKeys, oauthClients, users } from '../../database/core-schema.js';
|
|
10
|
+
import { coreMigrationsDir } from '../../../assets.js';
|
|
11
|
+
import { PG_ADMIN_URL, PG_TEST_TIMEOUT, dropScratchDatabases, scratchDatabaseUrl } from '../../../__tests__/pg-scratch.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The "Connect as" choice, through the real intermediary it is stored in: a
|
|
15
|
+
* migrated Postgres. The consent's choice goes onto the one-time code at
|
|
16
|
+
* Finish, crosses the token exchange onto the agent connection, and comes back
|
|
17
|
+
* out of every access-token verification; a key's choice goes onto its row at
|
|
18
|
+
* mint and comes back out of every key verification. Driven through the
|
|
19
|
+
* provider's and the key service's public surfaces only. Skipped without
|
|
20
|
+
* `TEST_DATABASE_URL`, as every `*.pg.test.ts` is.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const PREFIX = 'bevel-mcp_';
|
|
24
|
+
const REDIRECT = 'https://agent.example.com/callback';
|
|
25
|
+
|
|
26
|
+
describe.skipIf(!PG_ADMIN_URL)('connecting as an agent, on a real Postgres', () => {
|
|
27
|
+
let db: Database;
|
|
28
|
+
let provider: BevelOAuthProvider;
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
db = createDb(await scratchDatabaseUrl('hexis_connect_as'), { piiKey: randomBytes(32).toString('base64') });
|
|
32
|
+
await migrate(db, { migrationsFolder: coreMigrationsDir() });
|
|
33
|
+
provider = new BevelOAuthProvider({
|
|
34
|
+
db,
|
|
35
|
+
crypto: null,
|
|
36
|
+
stateSecret: 'test-state-secret',
|
|
37
|
+
publicFrontendUrl: 'https://app.example.com',
|
|
38
|
+
tokenPrefix: PREFIX,
|
|
39
|
+
});
|
|
40
|
+
vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
41
|
+
}, PG_TEST_TIMEOUT);
|
|
42
|
+
|
|
43
|
+
afterAll(async () => {
|
|
44
|
+
vi.restoreAllMocks();
|
|
45
|
+
await closeDb(db);
|
|
46
|
+
await dropScratchDatabases();
|
|
47
|
+
}, PG_TEST_TIMEOUT);
|
|
48
|
+
|
|
49
|
+
async function person(tag: string): Promise<string> {
|
|
50
|
+
const [user] = await db
|
|
51
|
+
.insert(users)
|
|
52
|
+
.values({ email: `${tag}@example.com`, emailBidx: `${tag}@example.com`, name: tag })
|
|
53
|
+
.returning({ id: users.id });
|
|
54
|
+
return user!.id;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function registeredClient(tag: string): Promise<OAuthClientInformationFull> {
|
|
58
|
+
const clientId = `client-${tag}-${randomBytes(4).toString('hex')}`;
|
|
59
|
+
await db.insert(oauthClients).values({ clientId, clientName: 'Claude', redirectUris: [REDIRECT] });
|
|
60
|
+
return { client_id: clientId, redirect_uris: [REDIRECT], token_endpoint_auth_method: 'none' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Finish on the consent step with `agentPath` (undefined: the route's Everything), then the client's exchange. */
|
|
64
|
+
async function consent(userId: string, client: OAuthClientInformationFull, agentPath?: string | null) {
|
|
65
|
+
const st = { c: client.client_id, r: REDIRECT, cc: 'challenge', iat: Date.now() };
|
|
66
|
+
const { redirectTo } =
|
|
67
|
+
agentPath === undefined ? await provider.issueAuthCode(userId, st) : await provider.issueAuthCode(userId, st, agentPath);
|
|
68
|
+
const code = new URL(redirectTo).searchParams.get('code')!;
|
|
69
|
+
return provider.exchangeAuthorizationCode(client, code);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const connectionsOf = (userId: string) =>
|
|
73
|
+
db
|
|
74
|
+
.select({ id: agentConnections.id, agentPath: agentConnections.agentPath, revokedAt: agentConnections.revokedAt })
|
|
75
|
+
.from(agentConnections)
|
|
76
|
+
.where(eq(agentConnections.userId, userId));
|
|
77
|
+
|
|
78
|
+
it('Everything: a consent with no choice records null and verifies with none — exactly as before', async () => {
|
|
79
|
+
const userId = await person('everything');
|
|
80
|
+
const tokens = await consent(userId, await registeredClient('everything'));
|
|
81
|
+
expect(await connectionsOf(userId)).toEqual([{ id: expect.any(String), agentPath: null, revokedAt: null }]);
|
|
82
|
+
expect((await provider.verifyAccessToken(tokens.access_token)).extra?.agentPath).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("the consent's choice round-trips to the connection row, and out of every verification", async () => {
|
|
86
|
+
const userId = await person('ada');
|
|
87
|
+
const client = await registeredClient('ada');
|
|
88
|
+
const tokens = await consent(userId, client, 'Agents/delivery-mechanic.agent');
|
|
89
|
+
|
|
90
|
+
const [row] = await connectionsOf(userId);
|
|
91
|
+
expect(row!.agentPath).toBe('Agents/delivery-mechanic.agent');
|
|
92
|
+
const info = await provider.verifyAccessToken(tokens.access_token);
|
|
93
|
+
expect(info.extra).toMatchObject({ connectionId: row!.id, agentPath: 'Agents/delivery-mechanic.agent' });
|
|
94
|
+
|
|
95
|
+
// A refresh keeps the connection's choice.
|
|
96
|
+
const refreshed = await provider.exchangeRefreshToken(client, tokens.refresh_token!);
|
|
97
|
+
expect((await provider.verifyAccessToken(refreshed.access_token)).extra?.agentPath).toBe('Agents/delivery-mechanic.agent');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('re-consenting with another choice changes the live connection, and its tokens follow it', async () => {
|
|
101
|
+
const userId = await person('bea');
|
|
102
|
+
const first = await consent(userId, await registeredClient('bea'), 'Agents/a.agent');
|
|
103
|
+
// Claude registers afresh on every re-authorisation; the agent is one connection.
|
|
104
|
+
await consent(userId, await registeredClient('bea'), null);
|
|
105
|
+
|
|
106
|
+
const rows = await connectionsOf(userId);
|
|
107
|
+
expect(rows).toHaveLength(1);
|
|
108
|
+
expect(rows[0]!.agentPath).toBeNull();
|
|
109
|
+
// The earlier pair still verifies — and now as the connection's choice.
|
|
110
|
+
expect((await provider.verifyAccessToken(first.access_token)).extra?.agentPath).toBeNull();
|
|
111
|
+
|
|
112
|
+
await consent(userId, await registeredClient('bea'), 'Agents/b.agent');
|
|
113
|
+
expect((await connectionsOf(userId))[0]!.agentPath).toBe('Agents/b.agent');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("a key's choice is stored at mint and comes back out of every verification", async () => {
|
|
117
|
+
const userId = await person('cy');
|
|
118
|
+
const keys = new ExternalApiKeyService(db, 'bevel_');
|
|
119
|
+
const asAgent = await keys.mint(userId, 'pipeline', { agentPath: 'Agents/ops.agent' });
|
|
120
|
+
const asEverything = await keys.mint(userId, 'laptop');
|
|
121
|
+
|
|
122
|
+
expect(asAgent.summary.agentPath).toBe('Agents/ops.agent');
|
|
123
|
+
expect(asEverything.summary.agentPath).toBeNull();
|
|
124
|
+
expect((await keys.verifyAndLoadToken(asAgent.plaintext))?.agentPath).toBe('Agents/ops.agent');
|
|
125
|
+
expect((await keys.verifyAndLoadToken(asEverything.plaintext))?.agentPath).toBeNull();
|
|
126
|
+
|
|
127
|
+
const stored = await db
|
|
128
|
+
.select({ label: externalApiKeys.label, agentPath: externalApiKeys.agentPath })
|
|
129
|
+
.from(externalApiKeys)
|
|
130
|
+
.where(eq(externalApiKeys.userId, userId));
|
|
131
|
+
expect(stored.sort((a, b) => a.label.localeCompare(b.label))).toEqual([
|
|
132
|
+
{ label: 'laptop', agentPath: null },
|
|
133
|
+
{ label: 'pipeline', agentPath: 'Agents/ops.agent' },
|
|
134
|
+
]);
|
|
135
|
+
expect((await keys.listForUser(userId)).map((k) => k.agentPath).sort()).toEqual(['Agents/ops.agent', null]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -53,8 +53,8 @@ function makeExternalApiKeyService(
|
|
|
53
53
|
|
|
54
54
|
function makeOAuthProvider(
|
|
55
55
|
verify?: (t: string) => Promise<{ extra?: Record<string, unknown> }>,
|
|
56
|
-
/**
|
|
57
|
-
live?: (connectionId: string) => Promise<
|
|
56
|
+
/** A live agent connection's current choice, or null when revoked — live as Everything unless a test says otherwise. */
|
|
57
|
+
live?: (connectionId: string) => Promise<{ agentPath: string | null } | null>,
|
|
58
58
|
) {
|
|
59
59
|
return {
|
|
60
60
|
looksLikeAccessToken: (t: string) => typeof t === 'string' && t.startsWith('bevel-mcp_'),
|
|
@@ -64,7 +64,7 @@ function makeOAuthProvider(
|
|
|
64
64
|
throw new InvalidTokenError('unknown token');
|
|
65
65
|
}),
|
|
66
66
|
),
|
|
67
|
-
|
|
67
|
+
liveConnectionChoice: vi.fn(live ?? (async () => ({ agentPath: null }))),
|
|
68
68
|
noteConnectionUse: vi.fn(),
|
|
69
69
|
} as unknown as BevelOAuthProvider;
|
|
70
70
|
}
|
|
@@ -318,10 +318,34 @@ describe('createMcpAuthMiddleware', () => {
|
|
|
318
318
|
expect(req.agentConnectionId).toBe('conn-7');
|
|
319
319
|
// Every request through the exchanged grant asks whether the
|
|
320
320
|
// connection still admits one, and counts as a use of it.
|
|
321
|
-
expect((oauth as any).
|
|
321
|
+
expect((oauth as any).liveConnectionChoice).toHaveBeenCalledWith('conn-7');
|
|
322
322
|
expect((oauth as any).noteConnectionUse).toHaveBeenCalledWith('conn-7');
|
|
323
323
|
});
|
|
324
324
|
|
|
325
|
+
it("serves an exchanged grant as its connection's CURRENT choice, not the one it was minted with", async () => {
|
|
326
|
+
const auth = makeAuthService();
|
|
327
|
+
(auth.getUserById as ReturnType<typeof vi.fn>) = vi.fn(async () => ({
|
|
328
|
+
id: 'user-7',
|
|
329
|
+
email: 'seven@example.com',
|
|
330
|
+
}));
|
|
331
|
+
// Minted as Everything; the person has since re-consented as an agent.
|
|
332
|
+
const everything = internal.mint({ userId: 'user-7', externalProxy: true, connectionId: 'conn-7' }, 60_000);
|
|
333
|
+
const narrowed = makeOAuthProvider(undefined, async () => ({ agentPath: 'Agents/narrow.agent' }));
|
|
334
|
+
const first = makeReqRes(`Bearer ${everything}`);
|
|
335
|
+
await makeMw({ internal, auth, oauth: narrowed })(first.req, first.res, first.next);
|
|
336
|
+
expect(first.next).toHaveBeenCalled();
|
|
337
|
+
expect(first.req.mcpAgentPath).toBe('Agents/narrow.agent');
|
|
338
|
+
// Minted as an agent; the person has since re-consented as Everything.
|
|
339
|
+
const asAgent = internal.mint(
|
|
340
|
+
{ userId: 'user-7', externalProxy: true, connectionId: 'conn-7', agentPath: 'Agents/narrow.agent' },
|
|
341
|
+
60_000,
|
|
342
|
+
);
|
|
343
|
+
const second = makeReqRes(`Bearer ${asAgent}`);
|
|
344
|
+
await makeMw({ internal, auth, oauth: makeOAuthProvider() })(second.req, second.res, second.next);
|
|
345
|
+
expect(second.next).toHaveBeenCalled();
|
|
346
|
+
expect(second.req.mcpAgentPath).toBeUndefined();
|
|
347
|
+
});
|
|
348
|
+
|
|
325
349
|
it('401s an exchanged grant whose agent connection was revoked — the token is stateless, the connection is not', async () => {
|
|
326
350
|
const token = internal.mint({ userId: 'user-7', externalProxy: true, connectionId: 'conn-7' }, 60_000);
|
|
327
351
|
const auth = makeAuthService();
|
|
@@ -329,7 +353,7 @@ describe('createMcpAuthMiddleware', () => {
|
|
|
329
353
|
id: 'user-7',
|
|
330
354
|
email: 'seven@example.com',
|
|
331
355
|
}));
|
|
332
|
-
const oauth = makeOAuthProvider(undefined, async () =>
|
|
356
|
+
const oauth = makeOAuthProvider(undefined, async () => null);
|
|
333
357
|
const mw = makeMw({ internal, auth, oauth });
|
|
334
358
|
const { req, res, next, status, setHeader } = makeReqRes(`Bearer ${token}`);
|
|
335
359
|
await mw(req, res, next);
|
|
@@ -29,6 +29,12 @@ import type { McpCallerKind } from '../mcp-auth.middleware.js';
|
|
|
29
29
|
*/
|
|
30
30
|
export const fakeAuth: RequestHandler = (req, _res, next) => {
|
|
31
31
|
req.userId = (req.headers['x-test-user'] as string | undefined) ?? 'user-A';
|
|
32
|
+
// The signed-in person's email, for a suite whose route reads it.
|
|
33
|
+
const email = req.headers['x-test-email'] as string | undefined;
|
|
34
|
+
if (email) req.userEmail = email;
|
|
35
|
+
// The agent the credential connects as, as the real middleware sets it.
|
|
36
|
+
const agentPath = req.headers['x-test-agent-path'] as string | undefined;
|
|
37
|
+
if (agentPath) req.mcpAgentPath = agentPath;
|
|
32
38
|
// The credential kind the real middleware would have recognised, when a
|
|
33
39
|
// suite needs one (the page header is a session's alone).
|
|
34
40
|
const kind = req.headers['x-test-kind'] as McpCallerKind | undefined;
|
|
@@ -48,6 +54,7 @@ export interface McpRoutesDeps {
|
|
|
48
54
|
internalTokens?: unknown;
|
|
49
55
|
oauthProvider?: unknown;
|
|
50
56
|
resourceMetadataUrl?: string;
|
|
57
|
+
agents?: unknown;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
/** Servers opened by `mountMcpRoutes`, closed together by `closeMountedRoutes`. */
|
|
@@ -73,6 +80,7 @@ export async function mountMcpRoutes(deps: McpRoutesDeps = {}): Promise<string>
|
|
|
73
80
|
(deps.internalTokens ?? stub) as never,
|
|
74
81
|
(deps.oauthProvider ?? stub) as never,
|
|
75
82
|
deps.resourceMetadataUrl ?? '',
|
|
83
|
+
deps.agents as never,
|
|
76
84
|
),
|
|
77
85
|
);
|
|
78
86
|
const server = await new Promise<HttpServer>((resolve) => {
|