@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
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
AccountChangeRefusedError,
|
|
10
10
|
AccountDeactivatedError,
|
|
11
11
|
AuthBackendError,
|
|
12
|
+
SessionEndedError,
|
|
12
13
|
admitEveryone,
|
|
13
14
|
type AccountProvisionReason,
|
|
14
15
|
type IAccountAdmission,
|
|
@@ -32,6 +33,8 @@ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
32
33
|
* request carrying a credential asks, so the answer is kept briefly rather
|
|
33
34
|
* than read each time; switching an account off or on in THIS process
|
|
34
35
|
* forgets it at once, and another replica sees the change within this long.
|
|
36
|
+
* A session token never waits on it: {@link AuthService.resolveSession}
|
|
37
|
+
* reads the row each time, so a password change ends it everywhere at once.
|
|
35
38
|
*/
|
|
36
39
|
const ACTIVE_CACHE_MS = 30_000;
|
|
37
40
|
|
|
@@ -69,6 +72,33 @@ export const OWNER_SWITCH_OFF_REFUSAL = "The owner can't be switched off.";
|
|
|
69
72
|
/** What an admin is told when deleting an owner's account would leave them no way to sign in. */
|
|
70
73
|
export const OWNER_DELETE_REFUSAL = "The owner's account can't be deleted: they would have no way to sign in.";
|
|
71
74
|
|
|
75
|
+
/**
|
|
76
|
+
* A password change refused because the password it proved is no longer the
|
|
77
|
+
* account's: another session changed it between this change's read and its
|
|
78
|
+
* write. Nothing was written; the routes answer it with a 409 and its words.
|
|
79
|
+
*/
|
|
80
|
+
export class PasswordChangedMeanwhileError extends Error {
|
|
81
|
+
constructor() {
|
|
82
|
+
super('Your password was changed from another session while this change was being made, so nothing was changed. Sign in with the new password.');
|
|
83
|
+
this.name = 'PasswordChangedMeanwhileError';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A password write the service refuses on purpose: the policy, a wrong
|
|
89
|
+
* current password, an account whose password is not kept here. The message
|
|
90
|
+
* is written for the caller, and a route answers it with a 400 — unlike any
|
|
91
|
+
* other error a password write raises, which is a fault and answered generic.
|
|
92
|
+
*/
|
|
93
|
+
export class PasswordRefusedError extends Error {
|
|
94
|
+
readonly status = 400;
|
|
95
|
+
|
|
96
|
+
constructor(message: string) {
|
|
97
|
+
super(message);
|
|
98
|
+
this.name = 'PasswordRefusedError';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
72
102
|
/**
|
|
73
103
|
* Decoy hash verified when the email is unknown or has no password set, so
|
|
74
104
|
* those paths cost the same scrypt work as a real wrong-password attempt —
|
|
@@ -271,7 +301,7 @@ export class AuthService implements IAccountSwitchGuard {
|
|
|
271
301
|
const defaultName = normalizedEmail.split('@')[0] || normalizedEmail;
|
|
272
302
|
await this.assertAdmitted(normalizedEmail, 'bootstrap');
|
|
273
303
|
const admin = await this.clearOwnerSwitchOff(await this.upsertUserByEmail(normalizedEmail, defaultName));
|
|
274
|
-
return { token: this.signToken(admin
|
|
304
|
+
return { token: this.signToken(admin), user: this.toClientUser(admin) };
|
|
275
305
|
}
|
|
276
306
|
|
|
277
307
|
// Always run one scrypt verification — against the stored hash when there
|
|
@@ -287,7 +317,7 @@ export class AuthService implements IAccountSwitchGuard {
|
|
|
287
317
|
// is switched off would tell them it has an account.
|
|
288
318
|
const signedIn = await this.clearOwnerSwitchOff(user);
|
|
289
319
|
if (signedIn.deactivatedAt) throw new AccountDeactivatedError();
|
|
290
|
-
return { token: this.signToken(signedIn
|
|
320
|
+
return { token: this.signToken(signedIn), user: this.toClientUser(signedIn) };
|
|
291
321
|
}
|
|
292
322
|
|
|
293
323
|
/**
|
|
@@ -319,7 +349,7 @@ export class AuthService implements IAccountSwitchGuard {
|
|
|
319
349
|
const displayName = (name ?? '').trim() || normalizedEmail.split('@')[0] || normalizedEmail;
|
|
320
350
|
const user = await this.clearOwnerSwitchOff(await this.upsertUserByEmail(normalizedEmail, displayName));
|
|
321
351
|
if (user.deactivatedAt) throw new AccountDeactivatedError();
|
|
322
|
-
return { token: this.signToken(user
|
|
352
|
+
return { token: this.signToken(user), user: this.toClientUser(user) };
|
|
323
353
|
}
|
|
324
354
|
|
|
325
355
|
/**
|
|
@@ -361,7 +391,7 @@ export class AuthService implements IAccountSwitchGuard {
|
|
|
361
391
|
// than sending the admin off to pick a longer password first.
|
|
362
392
|
this.assertNotSystemAccount(normalizedEmail);
|
|
363
393
|
if (this.isEnvAdminEmail(normalizedEmail)) {
|
|
364
|
-
throw new
|
|
394
|
+
throw new PasswordRefusedError(ENV_ADMIN_PASSWORD_REFUSAL);
|
|
365
395
|
}
|
|
366
396
|
this.assertPasswordPolicy(password);
|
|
367
397
|
await this.assertAdmitted(normalizedEmail, 'admin-create');
|
|
@@ -370,16 +400,22 @@ export class AuthService implements IAccountSwitchGuard {
|
|
|
370
400
|
// EXPLICITLY supplied name is persisted too; a blank name keeps the
|
|
371
401
|
// existing display name rather than clobbering it with the local-part
|
|
372
402
|
// fallback. `returning()` yields the authoritative row either way.
|
|
403
|
+
// A reset is a password change like any other: the session generation
|
|
404
|
+
// moves on with it, so every session opened with the old password ends,
|
|
405
|
+
// and a self-service change that read the row before it is refused
|
|
406
|
+
// rather than overwriting the reset (see {@link changePassword}).
|
|
407
|
+
const sessionVersion = sql`${users.sessionVersion} + 1`;
|
|
373
408
|
const [row] = await this.db
|
|
374
409
|
.insert(users)
|
|
375
410
|
.values({ email: normalizedEmail, emailBidx: normalizedEmail, name: displayName, passwordHash })
|
|
376
411
|
.onConflictDoUpdate({
|
|
377
412
|
target: users.emailBidx,
|
|
378
413
|
set: suppliedName
|
|
379
|
-
? { passwordHash, name: suppliedName, updatedAt: new Date() }
|
|
380
|
-
: { passwordHash, updatedAt: new Date() },
|
|
414
|
+
? { passwordHash, sessionVersion, name: suppliedName, updatedAt: new Date() }
|
|
415
|
+
: { passwordHash, sessionVersion, updatedAt: new Date() },
|
|
381
416
|
})
|
|
382
417
|
.returning();
|
|
418
|
+
this.activeCache.delete(row.id);
|
|
383
419
|
return this.toClientUser(row);
|
|
384
420
|
}
|
|
385
421
|
|
|
@@ -443,21 +479,26 @@ export class AuthService implements IAccountSwitchGuard {
|
|
|
443
479
|
if (existing) {
|
|
444
480
|
return { user: this.toClientUser(existing), created: false, passwordSet: false, deactivated: existing.deactivatedAt != null };
|
|
445
481
|
}
|
|
446
|
-
throw new
|
|
482
|
+
throw new PasswordRefusedError(ENV_ADMIN_PASSWORD_REFUSAL);
|
|
447
483
|
}
|
|
448
484
|
this.assertPasswordPolicy(password);
|
|
449
485
|
await this.assertAdmitted(normalizedEmail, 'admin-create');
|
|
450
486
|
const passwordHash = await hashPassword(password);
|
|
451
487
|
const displayName = normalizedEmail.split('@')[0] || normalizedEmail;
|
|
488
|
+
// A first password on an existing account is a password write like any
|
|
489
|
+
// other, so the session generation moves on with it: a self-service
|
|
490
|
+
// first password that read the row before this one is refused rather
|
|
491
|
+
// than overwriting it (see {@link changePassword}).
|
|
452
492
|
const [row] = await this.db
|
|
453
493
|
.insert(users)
|
|
454
494
|
.values({ email: normalizedEmail, emailBidx: normalizedEmail, name: displayName, passwordHash })
|
|
455
495
|
.onConflictDoUpdate({
|
|
456
496
|
target: users.emailBidx,
|
|
457
|
-
set: { passwordHash, updatedAt: new Date() },
|
|
497
|
+
set: { passwordHash, sessionVersion: sql`${users.sessionVersion} + 1`, updatedAt: new Date() },
|
|
458
498
|
setWhere: and(isNull(users.passwordHash), isNull(users.deactivatedAt)),
|
|
459
499
|
})
|
|
460
500
|
.returning({ ...getTableColumns(users), created: CREATED });
|
|
501
|
+
if (row) this.activeCache.delete(row.id);
|
|
461
502
|
if (row) return { user: this.toClientUser(row), created: row.created === true, passwordSet: true, deactivated: false };
|
|
462
503
|
// The conflicting account has a password or is switched off: untouched.
|
|
463
504
|
const [existing] = await this.db.select().from(users).where(eq(users.emailBidx, normalizedEmail)).limit(1);
|
|
@@ -586,31 +627,49 @@ export class AuthService implements IAccountSwitchGuard {
|
|
|
586
627
|
* environment password is the platform's rescue path into a deployment and
|
|
587
628
|
* is changed there, which is what the Account page says in place of the
|
|
588
629
|
* form.
|
|
630
|
+
*
|
|
631
|
+
* A change ends every session opened before it: the row's
|
|
632
|
+
* `sessionVersion` moves on in the same write, so every token issued
|
|
633
|
+
* before it is refused at its next request (see {@link resolveSession}).
|
|
634
|
+
* The caller's own session is the one exception: the token returned here
|
|
635
|
+
* carries the new generation and is what the caller keeps using.
|
|
636
|
+
*
|
|
637
|
+
* The write only lands on the row as it was read. If another session
|
|
638
|
+
* changed the password between the read and the write, the current
|
|
639
|
+
* password this one proved is no longer current, so it is refused with
|
|
640
|
+
* {@link PasswordChangedMeanwhileError} and nothing is written. Two clicks
|
|
641
|
+
* of one form are the same race.
|
|
589
642
|
*/
|
|
590
643
|
async changePassword(
|
|
591
644
|
userId: string,
|
|
592
645
|
currentPassword: string | undefined,
|
|
593
646
|
newPassword: string,
|
|
594
|
-
): Promise<
|
|
647
|
+
): Promise<{ token: string }> {
|
|
595
648
|
const [user] = await this.db.select().from(users).where(eq(users.id, userId)).limit(1);
|
|
596
|
-
if (!user) throw new
|
|
649
|
+
if (!user) throw new PasswordRefusedError('User not found');
|
|
597
650
|
// Before the policy check, so the deployment admin is told the real reason
|
|
598
651
|
// rather than being sent to fix a password that would be refused anyway.
|
|
599
652
|
this.assertNotSystemAccount(user.email);
|
|
600
653
|
if (this.isEnvAdminEmail(user.email)) {
|
|
601
|
-
throw new
|
|
654
|
+
throw new PasswordRefusedError(ENV_ADMIN_PASSWORD_REFUSAL);
|
|
602
655
|
}
|
|
603
656
|
this.assertPasswordPolicy(newPassword);
|
|
604
657
|
if (user.passwordHash) {
|
|
605
658
|
if (!currentPassword || !(await verifyPassword(currentPassword, user.passwordHash))) {
|
|
606
|
-
throw new
|
|
659
|
+
throw new PasswordRefusedError('Current password is incorrect');
|
|
607
660
|
}
|
|
608
661
|
}
|
|
609
662
|
const passwordHash = await hashPassword(newPassword);
|
|
610
|
-
await this.db
|
|
663
|
+
const [changed] = await this.db
|
|
611
664
|
.update(users)
|
|
612
|
-
.set({ passwordHash, updatedAt: new Date() })
|
|
613
|
-
.where(eq(users.id, userId))
|
|
665
|
+
.set({ passwordHash, sessionVersion: sql`${users.sessionVersion} + 1`, updatedAt: new Date() })
|
|
666
|
+
.where(and(eq(users.id, userId), eq(users.sessionVersion, user.sessionVersion)))
|
|
667
|
+
.returning();
|
|
668
|
+
if (!changed) throw new PasswordChangedMeanwhileError();
|
|
669
|
+
// Sessions read the row fresh (see resolveSession); the cached entry is
|
|
670
|
+
// dropped so nothing in this process keeps the old generation either.
|
|
671
|
+
this.activeCache.delete(userId);
|
|
672
|
+
return { token: this.signToken(changed) };
|
|
614
673
|
}
|
|
615
674
|
|
|
616
675
|
/**
|
|
@@ -664,18 +723,24 @@ export class AuthService implements IAccountSwitchGuard {
|
|
|
664
723
|
/** Every password write calls this first: the system accounts never get one. */
|
|
665
724
|
private assertNotSystemAccount(normalizedEmail: string): void {
|
|
666
725
|
if (SYSTEM_ACCOUNT_EMAILS.includes(normalizedEmail)) {
|
|
667
|
-
throw new
|
|
726
|
+
throw new PasswordRefusedError(SYSTEM_ACCOUNT_PASSWORD_REFUSAL);
|
|
668
727
|
}
|
|
669
728
|
}
|
|
670
729
|
|
|
671
730
|
private assertPasswordPolicy(password: string): void {
|
|
672
731
|
if ((password ?? '').length < MIN_PASSWORD_LENGTH) {
|
|
673
|
-
throw new
|
|
732
|
+
throw new PasswordRefusedError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
|
|
674
733
|
}
|
|
675
734
|
}
|
|
676
735
|
|
|
677
|
-
|
|
678
|
-
|
|
736
|
+
/**
|
|
737
|
+
* A session token for `user`, stamped with the session generation its row
|
|
738
|
+
* is at (`sv`): {@link resolveSession} refuses it once the row moves on.
|
|
739
|
+
*/
|
|
740
|
+
private signToken(user: { id: string; email: string; sessionVersion: number }): string {
|
|
741
|
+
return jwt.sign({ userId: user.id, email: user.email, sv: user.sessionVersion }, this.config.jwtSecret, {
|
|
742
|
+
expiresIn: '7d',
|
|
743
|
+
});
|
|
679
744
|
}
|
|
680
745
|
|
|
681
746
|
/**
|
|
@@ -710,39 +775,55 @@ export class AuthService implements IAccountSwitchGuard {
|
|
|
710
775
|
|
|
711
776
|
/**
|
|
712
777
|
* The signature and expiry of a session token, and nothing else — whether
|
|
713
|
-
* its account is still on is
|
|
778
|
+
* its account is still on, and whether the session is still honoured, are
|
|
779
|
+
* {@link resolveSession}'s questions. Every
|
|
714
780
|
* place a session is ACCEPTED goes through that one.
|
|
715
781
|
*/
|
|
716
782
|
verifyToken(token: string): { userId: string; email: string } {
|
|
783
|
+
const { userId, email } = this.decodeSession(token);
|
|
784
|
+
return { userId, email };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** {@link verifyToken} plus the session generation the token was issued at. */
|
|
788
|
+
private decodeSession(token: string): { userId: string; email: string; sessionVersion: number } {
|
|
717
789
|
const decoded = jwt.verify(token, this.config.jwtSecret) as {
|
|
718
790
|
userId: string;
|
|
719
791
|
email: string;
|
|
792
|
+
sv?: number;
|
|
720
793
|
};
|
|
721
|
-
|
|
794
|
+
// A token issued before sessions had generations carries none: it is of
|
|
795
|
+
// the first one, which is where every row started.
|
|
796
|
+
return { userId: decoded.userId, email: decoded.email, sessionVersion: decoded.sv ?? 0 };
|
|
722
797
|
}
|
|
723
798
|
|
|
724
799
|
/**
|
|
725
|
-
* A session token that may still be used: signed by us, not expired,
|
|
726
|
-
*
|
|
800
|
+
* A session token that may still be used: signed by us, not expired, its
|
|
801
|
+
* account still on, and issued since the account's password last changed.
|
|
802
|
+
* A token outlives a deactivation or a password change by up to seven
|
|
727
803
|
* days, so the signature alone no longer says the bearer may enter.
|
|
728
|
-
* Throws for an invalid token, {@link
|
|
729
|
-
*
|
|
804
|
+
* Throws for an invalid token, {@link SessionEndedError} for one a
|
|
805
|
+
* password change ended, {@link AccountDeactivatedError} for one whose
|
|
806
|
+
* account was switched off.
|
|
730
807
|
*/
|
|
731
808
|
async resolveSession(token: string): Promise<{ userId: string; email: string }> {
|
|
732
|
-
const claim = this.
|
|
733
|
-
let active: boolean;
|
|
809
|
+
const { sessionVersion, ...claim } = this.decodeSession(token);
|
|
810
|
+
let account: { active: boolean; sessionVersion: number };
|
|
734
811
|
try {
|
|
735
|
-
|
|
812
|
+
// Read fresh, never from the cache: a password changed on another
|
|
813
|
+
// replica must end this token at its very next request, and only the
|
|
814
|
+
// row can say it was.
|
|
815
|
+
account = await this.accountState(claim.userId, { fresh: true });
|
|
736
816
|
} catch (err) {
|
|
737
817
|
// The token is fine; the database is not. Said apart from a refusal, so
|
|
738
818
|
// an outage is a 500 and not a sign-out (see `AuthBackendError`).
|
|
739
819
|
throw new AuthBackendError(err);
|
|
740
820
|
}
|
|
741
|
-
if (!active) throw new AccountDeactivatedError();
|
|
821
|
+
if (!account.active) throw new AccountDeactivatedError();
|
|
822
|
+
if (sessionVersion !== account.sessionVersion) throw new SessionEndedError();
|
|
742
823
|
return claim;
|
|
743
824
|
}
|
|
744
825
|
|
|
745
|
-
private readonly activeCache = new Map<string, { active: boolean; at: number }>();
|
|
826
|
+
private readonly activeCache = new Map<string, { active: boolean; sessionVersion: number; at: number }>();
|
|
746
827
|
|
|
747
828
|
/**
|
|
748
829
|
* Whether the account behind a credential is on: it exists and no admin
|
|
@@ -762,18 +843,38 @@ export class AuthService implements IAccountSwitchGuard {
|
|
|
762
843
|
* ({@link clearOwnerSwitchOff}).
|
|
763
844
|
*/
|
|
764
845
|
async isActive(userId: string): Promise<boolean> {
|
|
846
|
+
return (await this.accountState(userId)).active;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* {@link isActive}'s answer and the session generation the account is at,
|
|
851
|
+
* read and cached together; a password change in this process forgets the
|
|
852
|
+
* entry at once, as switching an account off does. `fresh` reads the row
|
|
853
|
+
* whatever the cache holds (and refreshes it): {@link resolveSession}'s
|
|
854
|
+
* question, since a session's generation must hold on every replica.
|
|
855
|
+
*/
|
|
856
|
+
private async accountState(
|
|
857
|
+
userId: string,
|
|
858
|
+
{ fresh = false }: { fresh?: boolean } = {},
|
|
859
|
+
): Promise<{ active: boolean; sessionVersion: number }> {
|
|
765
860
|
const now = Date.now();
|
|
766
861
|
const hit = this.activeCache.get(userId);
|
|
767
|
-
if (hit && now - hit.at < ACTIVE_CACHE_MS) return hit
|
|
862
|
+
if (!fresh && hit && now - hit.at < ACTIVE_CACHE_MS) return hit;
|
|
768
863
|
const [found] = await this.db
|
|
769
|
-
.select({
|
|
864
|
+
.select({
|
|
865
|
+
id: users.id,
|
|
866
|
+
email: users.email,
|
|
867
|
+
deactivatedAt: users.deactivatedAt,
|
|
868
|
+
sessionVersion: users.sessionVersion,
|
|
869
|
+
})
|
|
770
870
|
.from(users)
|
|
771
871
|
.where(eq(users.id, userId))
|
|
772
872
|
.limit(1);
|
|
773
873
|
const row = found && (await this.clearOwnerSwitchOff(found));
|
|
774
874
|
const active = row !== undefined && (!row.deactivatedAt || this.isEnvAdminEmail(row.email ?? ''));
|
|
775
|
-
|
|
776
|
-
|
|
875
|
+
const entry = { active, sessionVersion: row?.sessionVersion ?? 0, at: now };
|
|
876
|
+
this.activeCache.set(userId, entry);
|
|
877
|
+
return entry;
|
|
777
878
|
}
|
|
778
879
|
|
|
779
880
|
/**
|
|
@@ -4,8 +4,7 @@ import { logger } from '../../shared/logging.js';
|
|
|
4
4
|
const log = logger('oidc');
|
|
5
5
|
import type express from 'express';
|
|
6
6
|
import type { AuthProviderPlugin } from './auth.routes.js';
|
|
7
|
-
import {
|
|
8
|
-
import { AUTH_COOKIE_NAME } from './auth.middleware.js';
|
|
7
|
+
import { setAuthCookie } from './auth.middleware.js';
|
|
9
8
|
import type { AuthService } from './auth.service.js';
|
|
10
9
|
import { AccountAdmissionRefusedError, AccountDeactivatedError } from './account-admission.js';
|
|
11
10
|
import { normalizeIssuerUrl } from '../settings/index.js';
|
|
@@ -311,13 +310,7 @@ export class OidcAuthProvider implements AuthProviderPlugin {
|
|
|
311
310
|
} catch (error) {
|
|
312
311
|
log.error('OIDC sign-in record failed:', { err: error });
|
|
313
312
|
}
|
|
314
|
-
res
|
|
315
|
-
httpOnly: true,
|
|
316
|
-
sameSite: 'lax',
|
|
317
|
-
secure: cookieSecure,
|
|
318
|
-
maxAge: AUTH_COOKIE_MAX_AGE_S * 1000,
|
|
319
|
-
path: '/',
|
|
320
|
-
});
|
|
313
|
+
setAuthCookie(res, token, cookieSecure);
|
|
321
314
|
res.redirect(`${publicFrontendUrl}/auth/oidc/callback#token=${encodeURIComponent(token)}`);
|
|
322
315
|
} catch (error) {
|
|
323
316
|
// A refused provisioning is not a failed sign-in: the identity was
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { afterAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { migrate } from 'drizzle-orm/node-postgres/migrator';
|
|
7
|
+
import { sql } from 'drizzle-orm';
|
|
8
|
+
import { closeDb, createDb, type Database } from '../connection.js';
|
|
9
|
+
import { runCoreMigrations, runPiiEncryptionBackfill } from '../migrate.js';
|
|
10
|
+
import { coreMigrationsDir } from '../../../assets.js';
|
|
11
|
+
import { IndexOnWrite, SealOnWrite } from '../../../shared/column-crypto.js';
|
|
12
|
+
import { PG_ADMIN_URL, PG_TEST_TIMEOUT, dropScratchDatabases, scratchDatabaseUrl } from '../../../__tests__/pg-scratch.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Migration 0022 on a real Postgres, from both states a deployment can be in
|
|
16
|
+
* when it starts on this release: without `users.session_version` (every
|
|
17
|
+
* account starts at 0, so nobody is signed out by the upgrade), and with the
|
|
18
|
+
* column already there (the guarded `ADD COLUMN` changes nothing, so the
|
|
19
|
+
* generations it holds, and the sessions they honour, survive the start).
|
|
20
|
+
*
|
|
21
|
+
* Runs where `TEST_DATABASE_URL` names a server this role may create
|
|
22
|
+
* databases on, each test in a scratch database of its own.
|
|
23
|
+
*/
|
|
24
|
+
const KEY = randomBytes(32).toString('base64');
|
|
25
|
+
const open: Database[] = [];
|
|
26
|
+
const folders: string[] = [];
|
|
27
|
+
|
|
28
|
+
/** The core history as it stood BEFORE 0022: the packaged folder minus 0022, in a folder of this test's own. */
|
|
29
|
+
function historyBefore0022(): string {
|
|
30
|
+
const folder = mkdtempSync(path.join(tmpdir(), 'hexis-core-before-0022-'));
|
|
31
|
+
folders.push(folder);
|
|
32
|
+
cpSync(coreMigrationsDir(), folder, { recursive: true });
|
|
33
|
+
rmSync(path.join(folder, '0022_session_version.sql'));
|
|
34
|
+
rmSync(path.join(folder, 'meta', '0022_snapshot.json'));
|
|
35
|
+
const journalPath = path.join(folder, 'meta', '_journal.json');
|
|
36
|
+
const journal = JSON.parse(readFileSync(journalPath, 'utf8')) as { entries: { idx: number }[] };
|
|
37
|
+
journal.entries = journal.entries.filter((e) => e.idx < 22);
|
|
38
|
+
writeFileSync(journalPath, JSON.stringify(journal, null, 2));
|
|
39
|
+
return folder;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A database an older version left: started on the history to 0021, holding
|
|
44
|
+
* the accounts that version wrote, through SQL that names only the columns
|
|
45
|
+
* it knew. `withColumn` adds `session_version` by hand first, as a database
|
|
46
|
+
* that already has it would, and gives each account a generation of its own.
|
|
47
|
+
*/
|
|
48
|
+
async function olderDeployment(withColumn: boolean): Promise<Database> {
|
|
49
|
+
const db = createDb(await scratchDatabaseUrl('hexis_session_version'), { piiKey: KEY });
|
|
50
|
+
open.push(db);
|
|
51
|
+
await migrate(db, { migrationsFolder: historyBefore0022(), migrationsTable: '__drizzle_migrations_core' });
|
|
52
|
+
await runPiiEncryptionBackfill(db);
|
|
53
|
+
if (withColumn) await db.execute(sql`alter table users add column session_version integer default 0 not null`);
|
|
54
|
+
const seal = (plain: string) => new SealOnWrite(plain) as unknown as string;
|
|
55
|
+
const index = (plain: string) => new IndexOnWrite(plain) as unknown as string;
|
|
56
|
+
const account = (email: string, name: string) => sql`(${seal(email)}, ${index(email)}, ${seal(name)})`;
|
|
57
|
+
await db.execute(sql`
|
|
58
|
+
insert into users (email, email_bidx, name) values
|
|
59
|
+
${account('ada@example.com', 'Ada')},
|
|
60
|
+
${account('bo@example.com', 'Bo')}
|
|
61
|
+
`);
|
|
62
|
+
if (withColumn) {
|
|
63
|
+
await db.execute(sql`update users set session_version = 3 where email_bidx = ${index('ada@example.com')}`);
|
|
64
|
+
await db.execute(sql`update users set session_version = 7 where email_bidx = ${index('bo@example.com')}`);
|
|
65
|
+
}
|
|
66
|
+
return db;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** How many migrations the packaged history holds — what a fresh start records as applied. */
|
|
70
|
+
function packagedHistoryLength(): number {
|
|
71
|
+
const journal = JSON.parse(readFileSync(path.join(coreMigrationsDir(), 'meta', '_journal.json'), 'utf8')) as {
|
|
72
|
+
entries: unknown[];
|
|
73
|
+
};
|
|
74
|
+
return journal.entries.length;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Each account's generation, in a fixed order. */
|
|
78
|
+
const generations = async (db: Database) =>
|
|
79
|
+
(await db.execute(sql`select session_version from users order by session_version`)).rows.map(
|
|
80
|
+
(r) => (r as { session_version: number }).session_version,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
/** What the column is: its type, default and nullability, as Postgres reports them. */
|
|
84
|
+
const columnShape = async (db: Database) =>
|
|
85
|
+
(
|
|
86
|
+
await db.execute(sql`
|
|
87
|
+
select data_type, column_default, is_nullable from information_schema.columns
|
|
88
|
+
where table_schema = 'public' and table_name = 'users' and column_name = 'session_version'
|
|
89
|
+
`)
|
|
90
|
+
).rows;
|
|
91
|
+
|
|
92
|
+
const EXPECTED_SHAPE = [{ data_type: 'integer', column_default: '0', is_nullable: 'NO' }];
|
|
93
|
+
|
|
94
|
+
describe.skipIf(!PG_ADMIN_URL)('migration 0022 (users.session_version), on a real Postgres', () => {
|
|
95
|
+
afterAll(async () => {
|
|
96
|
+
for (const db of open.splice(0)) await closeDb(db);
|
|
97
|
+
for (const folder of folders.splice(0)) rmSync(folder, { recursive: true, force: true });
|
|
98
|
+
await dropScratchDatabases();
|
|
99
|
+
}, PG_TEST_TIMEOUT);
|
|
100
|
+
|
|
101
|
+
it(
|
|
102
|
+
'adds the column at 0 for every existing account, so no session is ended by the upgrade',
|
|
103
|
+
async () => {
|
|
104
|
+
const db = await olderDeployment(false);
|
|
105
|
+
expect(await columnShape(db)).toEqual([]);
|
|
106
|
+
|
|
107
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
108
|
+
|
|
109
|
+
expect(await columnShape(db)).toEqual(EXPECTED_SHAPE);
|
|
110
|
+
expect(await generations(db)).toEqual([0, 0]);
|
|
111
|
+
},
|
|
112
|
+
PG_TEST_TIMEOUT,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
it(
|
|
116
|
+
'finds the column already there and changes nothing, keeping every generation it holds',
|
|
117
|
+
async () => {
|
|
118
|
+
const db = await olderDeployment(true);
|
|
119
|
+
expect(await columnShape(db)).toEqual(EXPECTED_SHAPE);
|
|
120
|
+
|
|
121
|
+
await runCoreMigrations(db, coreMigrationsDir());
|
|
122
|
+
|
|
123
|
+
expect(await columnShape(db)).toEqual(EXPECTED_SHAPE);
|
|
124
|
+
expect(await generations(db)).toEqual([3, 7]);
|
|
125
|
+
// And 0022 is recorded as applied, so the next start does not try it
|
|
126
|
+
// again — judged against the packaged history's own length, so a later
|
|
127
|
+
// migration does not make this count stale.
|
|
128
|
+
const applied = await db.execute(sql`select count(*)::int as n from drizzle.__drizzle_migrations_core`);
|
|
129
|
+
expect((applied.rows[0] as { n: number }).n).toBe(packagedHistoryLength());
|
|
130
|
+
},
|
|
131
|
+
PG_TEST_TIMEOUT,
|
|
132
|
+
);
|
|
133
|
+
});
|
|
@@ -41,6 +41,16 @@ export const users = pgTable('users', {
|
|
|
41
41
|
* until an admin (or the user, from their Account page) sets one.
|
|
42
42
|
*/
|
|
43
43
|
passwordHash: text('password_hash'),
|
|
44
|
+
/**
|
|
45
|
+
* Which generation of this account's sessions is honoured. Every session
|
|
46
|
+
* token carries the value it was issued at (`sv`; a token from before the
|
|
47
|
+
* column existed carries none and counts as 0), and one whose value is not
|
|
48
|
+
* the row's is refused. Changing the password moves it on, so every session
|
|
49
|
+
* opened with the old password ends at its next request — the one that
|
|
50
|
+
* made the change is handed a fresh token. Connection keys are not
|
|
51
|
+
* sessions and do not look at it.
|
|
52
|
+
*/
|
|
53
|
+
sessionVersion: integer('session_version').default(0).notNull(),
|
|
44
54
|
/**
|
|
45
55
|
* The one onboarding fact the server keeps: has this person concluded the
|
|
46
56
|
* connect-your-agent setup (the welcome page's Done, or the reminder
|
|
@@ -277,6 +287,15 @@ export const externalApiKeys = pgTable('api_tokens', {
|
|
|
277
287
|
* deleted in one step.
|
|
278
288
|
*/
|
|
279
289
|
deletedAt: timestamp('deleted_at'),
|
|
290
|
+
/**
|
|
291
|
+
* The agent configuration the key connects as — a repo-relative knowledge-base
|
|
292
|
+
* path, chosen when the key is minted. Null is Everything: the key sees
|
|
293
|
+
* every tool, skill and plugin its owner may read, exactly as before the
|
|
294
|
+
* column existed. The path is the whole record: the file is re-read on
|
|
295
|
+
* every surface build, so editing it changes what the key sees without a
|
|
296
|
+
* new key (see `modules/agent-scope`).
|
|
297
|
+
*/
|
|
298
|
+
agentPath: text('agent_path'),
|
|
280
299
|
}, (t) => ({
|
|
281
300
|
byUser: index('api_tokens_by_user').on(t.userId),
|
|
282
301
|
}));
|
|
@@ -486,6 +505,12 @@ export const oauthAuthCodes = pgTable('oauth_auth_codes', {
|
|
|
486
505
|
expiresAt: timestamp('expires_at').notNull(),
|
|
487
506
|
consumedAt: timestamp('consumed_at'),
|
|
488
507
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
508
|
+
/**
|
|
509
|
+
* The "Connect as" choice made on the consent step, carried from Finish to
|
|
510
|
+
* the token exchange that makes (or finds) the agent connection — the code
|
|
511
|
+
* is the only thing that travels between the two. Null is Everything.
|
|
512
|
+
*/
|
|
513
|
+
agentPath: text('agent_path'),
|
|
489
514
|
}, (t) => ({
|
|
490
515
|
// Sweep expired/spent rows without a full scan.
|
|
491
516
|
byExpiry: index('oauth_auth_codes_by_expiry').on(t.expiresAt),
|
|
@@ -525,6 +550,14 @@ export const agentConnections = pgTable('agent_connections', {
|
|
|
525
550
|
revokedAt: timestamp('revoked_at'),
|
|
526
551
|
/** `owner` | `admin` — who ended it; null while live. Same vocabulary as `api_tokens.revoked_by`. */
|
|
527
552
|
revokedBy: text('revoked_by'),
|
|
553
|
+
/**
|
|
554
|
+
* The agent configuration the person chose to connect as on the consent step — a
|
|
555
|
+
* repo-relative knowledge-base path — or null for Everything. Set by every
|
|
556
|
+
* consent that lands on the row, so re-authorising with another choice
|
|
557
|
+
* changes what the agent sees from its next request. Same meaning as
|
|
558
|
+
* `api_tokens.agent_path`.
|
|
559
|
+
*/
|
|
560
|
+
agentPath: text('agent_path'),
|
|
528
561
|
}, (t) => ({
|
|
529
562
|
byUser: index('agent_connections_by_user').on(t.userId),
|
|
530
563
|
liveUnq: uniqueIndex('agent_connections_live_unq')
|
|
@@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
4
4
|
import { createDeclaredVariableRoutes } from '../declared-variables.routes.js';
|
|
5
5
|
import type { IToolManualService, ToolManualSummary } from '../../tool-manuals/tool-manuals.contract.js';
|
|
6
6
|
import type { ISecretsVaultService } from '../../secrets-vault/secrets-vault.contract.js';
|
|
7
|
+
import { ConnectionScope, type IAgentScopeResolver } from '../../agent-scope/index.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The one core route that releases secret VALUES. What is worth pinning here is
|
|
@@ -46,6 +47,9 @@ async function mount(opts: {
|
|
|
46
47
|
source?: 'session' | 'external' | 'internal';
|
|
47
48
|
manuals?: ToolManualSummary[];
|
|
48
49
|
secrets?: Record<string, string>;
|
|
50
|
+
/** The `.agent` file the caller's connection was made as; absent is Everything. */
|
|
51
|
+
agentPath?: string;
|
|
52
|
+
agentScope?: Pick<IAgentScopeResolver, 'resolve'>;
|
|
49
53
|
}): Promise<Harness> {
|
|
50
54
|
const userId = opts.userId ?? 'user-1';
|
|
51
55
|
const source = opts.source ?? 'external';
|
|
@@ -64,10 +68,18 @@ async function mount(opts: {
|
|
|
64
68
|
toolManualService,
|
|
65
69
|
secretsVault,
|
|
66
70
|
(req, _res, next) => {
|
|
67
|
-
if (userId)
|
|
71
|
+
if (userId) {
|
|
72
|
+
req.toolAuth = {
|
|
73
|
+
userId,
|
|
74
|
+
source,
|
|
75
|
+
scope: 'write',
|
|
76
|
+
...(opts.agentPath ? { agentPath: opts.agentPath } : {}),
|
|
77
|
+
} as typeof req.toolAuth;
|
|
78
|
+
}
|
|
68
79
|
next();
|
|
69
80
|
},
|
|
70
81
|
async () => 'runner@x.eu',
|
|
82
|
+
opts.agentScope,
|
|
71
83
|
),
|
|
72
84
|
);
|
|
73
85
|
httpServer = await new Promise<HttpServer>((r) => {
|
|
@@ -133,6 +145,37 @@ describe('POST /agent/local-tools/:slug/variables', () => {
|
|
|
133
145
|
expect(resolve).not.toHaveBeenCalled();
|
|
134
146
|
});
|
|
135
147
|
|
|
148
|
+
it("releases only the variables of a connector the connection's .agent file loads", async () => {
|
|
149
|
+
const agent = { path: 'Agents/ops.agent', name: 'Ops' };
|
|
150
|
+
const loading = (connectors: string[]) => ({
|
|
151
|
+
resolve: async () => ConnectionScope.narrowed(agent, { skills: [], connectors, platformTools: null }, 'fp'),
|
|
152
|
+
});
|
|
153
|
+
// A folder entry over the tool's plugin loads it.
|
|
154
|
+
const inScope = await mount({
|
|
155
|
+
manuals: [GIT_TOOL],
|
|
156
|
+
secrets: { git_GITHUB_TOKEN: 'ghp_secret' },
|
|
157
|
+
agentPath: agent.path,
|
|
158
|
+
agentScope: loading(['Plugins/Engineering']),
|
|
159
|
+
});
|
|
160
|
+
expect((await fetch(`${inScope.base}/agent/local-tools/git/variables`, { method: 'POST' })).status).toBe(200);
|
|
161
|
+
await new Promise<void>((r) => httpServer!.close(() => r()));
|
|
162
|
+
|
|
163
|
+
// A readable tool the file does not load is as absent as an unreadable one.
|
|
164
|
+
const outOfScope = await mount({
|
|
165
|
+
manuals: [GIT_TOOL],
|
|
166
|
+
secrets: { git_GITHUB_TOKEN: 'ghp_secret' },
|
|
167
|
+
agentPath: agent.path,
|
|
168
|
+
agentScope: loading(['Plugins/billing.tool']),
|
|
169
|
+
});
|
|
170
|
+
expect((await fetch(`${outOfScope.base}/agent/local-tools/git/variables`, { method: 'POST' })).status).toBe(404);
|
|
171
|
+
expect(outOfScope.resolve).not.toHaveBeenCalled();
|
|
172
|
+
await new Promise<void>((r) => httpServer!.close(() => r()));
|
|
173
|
+
|
|
174
|
+
// A narrowed connection with no resolver wired loads nothing.
|
|
175
|
+
const unwired = await mount({ manuals: [GIT_TOOL], agentPath: agent.path });
|
|
176
|
+
expect((await fetch(`${unwired.base}/agent/local-tools/git/variables`, { method: 'POST' })).status).toBe(404);
|
|
177
|
+
});
|
|
178
|
+
|
|
136
179
|
it('403s an unauthenticated caller', async () => {
|
|
137
180
|
const { base } = await mount({ userId: '', manuals: [GIT_TOOL] });
|
|
138
181
|
expect((await fetch(`${base}/agent/local-tools/git/variables`, { method: 'POST' })).status).toBe(403);
|