@bevel-software/platform-core-backend 0.21.0 → 0.23.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/dist/core/core-ports.d.ts +11 -1
- 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 +3 -3
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +62 -13
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +22 -3
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +158 -23
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +34 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +89 -13
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +0 -12
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +11 -13
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +42 -12
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +67 -10
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +214 -35
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access-requests.contract.d.ts +75 -0
- package/dist/modules/access/access-requests.contract.d.ts.map +1 -0
- package/dist/modules/access/access-requests.contract.js +20 -0
- package/dist/modules/access/access-requests.contract.js.map +1 -0
- package/dist/modules/access/access-requests.routes.d.ts +35 -0
- package/dist/modules/access/access-requests.routes.d.ts.map +1 -0
- package/dist/modules/access/access-requests.routes.js +237 -0
- package/dist/modules/access/access-requests.routes.js.map +1 -0
- package/dist/modules/access/access-requests.service.d.ts +123 -0
- package/dist/modules/access/access-requests.service.d.ts.map +1 -0
- package/dist/modules/access/access-requests.service.js +337 -0
- package/dist/modules/access/access-requests.service.js.map +1 -0
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +13 -15
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +12 -0
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +24 -0
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/auth/account-admission.d.ts +58 -7
- package/dist/modules/auth/account-admission.d.ts.map +1 -1
- package/dist/modules/auth/account-admission.js +44 -1
- package/dist/modules/auth/account-admission.js.map +1 -1
- package/dist/modules/auth/account.routes.d.ts +1 -1
- package/dist/modules/auth/account.routes.d.ts.map +1 -1
- package/dist/modules/auth/account.routes.js +61 -5
- package/dist/modules/auth/account.routes.js.map +1 -1
- package/dist/modules/auth/auth.middleware.d.ts +1 -1
- package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
- package/dist/modules/auth/auth.middleware.js +18 -7
- package/dist/modules/auth/auth.middleware.js.map +1 -1
- package/dist/modules/auth/auth.routes.d.ts.map +1 -1
- package/dist/modules/auth/auth.routes.js +8 -0
- package/dist/modules/auth/auth.routes.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts +96 -3
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +187 -9
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
- package/dist/modules/auth/oidc-auth-provider.js +7 -2
- package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +41 -24
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +35 -27
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/github-app/github-app.client.d.ts +101 -0
- package/dist/modules/github-app/github-app.client.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.client.js +222 -0
- package/dist/modules/github-app/github-app.client.js.map +1 -0
- package/dist/modules/github-app/github-app.connection.d.ts +102 -0
- package/dist/modules/github-app/github-app.connection.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.connection.js +185 -0
- package/dist/modules/github-app/github-app.connection.js.map +1 -0
- package/dist/modules/github-app/github-app.routes.d.ts +59 -0
- package/dist/modules/github-app/github-app.routes.d.ts.map +1 -0
- package/dist/modules/github-app/github-app.routes.js +323 -0
- package/dist/modules/github-app/github-app.routes.js.map +1 -0
- package/dist/modules/github-app/index.d.ts +4 -0
- package/dist/modules/github-app/index.d.ts.map +1 -0
- package/dist/modules/github-app/index.js +4 -0
- package/dist/modules/github-app/index.js.map +1 -0
- package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
- package/dist/modules/kb-fs/branch-name.js +12 -2
- package/dist/modules/kb-fs/branch-name.js.map +1 -1
- package/dist/modules/kb-fs/remote-url.d.ts +22 -0
- package/dist/modules/kb-fs/remote-url.d.ts.map +1 -0
- package/dist/modules/kb-fs/remote-url.js +35 -0
- package/dist/modules/kb-fs/remote-url.js.map +1 -0
- package/dist/modules/kb-fs/repo-path.d.ts +11 -16
- package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
- package/dist/modules/kb-fs/repo-path.js +79 -0
- package/dist/modules/kb-fs/repo-path.js.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
- package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
- package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
- package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
- package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
- package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
- package/dist/modules/kb-sync/sync-auth.js +1 -1
- package/dist/modules/kb-sync/sync-auth.js.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
- package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
- package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
- package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
- package/dist/modules/plugins/join-proposals.d.ts +17 -5
- package/dist/modules/plugins/join-proposals.d.ts.map +1 -1
- package/dist/modules/plugins/join-proposals.js +76 -22
- package/dist/modules/plugins/join-proposals.js.map +1 -1
- package/dist/modules/plugins/join-requests.service.d.ts +111 -18
- package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
- package/dist/modules/plugins/join-requests.service.js +173 -29
- package/dist/modules/plugins/join-requests.service.js.map +1 -1
- package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.routes.js +3 -2
- package/dist/modules/plugins/plugins.routes.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +22 -1
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +101 -7
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/managed-repository.d.ts +43 -0
- package/dist/modules/settings/managed-repository.d.ts.map +1 -0
- package/dist/modules/settings/managed-repository.js +60 -0
- package/dist/modules/settings/managed-repository.js.map +1 -0
- package/dist/modules/settings/repository-source.d.ts +128 -0
- package/dist/modules/settings/repository-source.d.ts.map +1 -0
- package/dist/modules/settings/repository-source.js +150 -0
- package/dist/modules/settings/repository-source.js.map +1 -0
- package/dist/modules/settings/setup.routes.d.ts +78 -4
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +379 -17
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.d.ts +6 -8
- package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
- package/dist/modules/skills/skill-access-requests.routes.js +24 -63
- package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts +90 -18
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.contract.js +4 -1
- package/dist/modules/skills/skills.contract.js.map +1 -1
- package/dist/modules/skills/skills.service.d.ts +98 -9
- package/dist/modules/skills/skills.service.d.ts.map +1 -1
- package/dist/modules/skills/skills.service.js +239 -37
- package/dist/modules/skills/skills.service.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts +7 -0
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +71 -7
- package/dist/modules/skills/skills.tools.js.map +1 -1
- 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 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-context.js +15 -0
- package/dist/modules/tool-helpers/tool-context.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.js +25 -4
- package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
- package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
- 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.js +1 -1
- package/dist/modules/tool-helpers/validate-token.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +113 -37
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +24 -0
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +30 -0
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +72 -1
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +170 -6
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
- package/dist/modules/workflow/git/node-git-runner.js +5 -0
- package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +45 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +99 -0
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +55 -0
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
- package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
- package/dist/modules/workflow/workflow-hooks.js +16 -1
- package/dist/modules/workflow/workflow-hooks.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +3 -1
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +94 -13
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +258 -51
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
- package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
- package/dist/modules/workspace/agent-access.gate.js +123 -0
- package/dist/modules/workspace/agent-access.gate.js.map +1 -0
- package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
- package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
- package/dist/modules/workspace/routine-write-policy.js +5 -6
- package/dist/modules/workspace/routine-write-policy.js.map +1 -1
- package/dist/modules/workspace/session-sink.d.ts +5 -5
- package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
- package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
- package/dist/modules/workspace/set-aside-clone.js +92 -0
- package/dist/modules/workspace/set-aside-clone.js.map +1 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +117 -1
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +190 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +92 -3
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +115 -5
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +272 -26
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +2 -2
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +337 -117
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/write-access/write-access.d.ts +60 -0
- package/dist/modules/write-access/write-access.d.ts.map +1 -0
- package/dist/modules/write-access/write-access.js +129 -0
- package/dist/modules/write-access/write-access.js.map +1 -0
- package/dist/shared/domain-errors.d.ts +64 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +83 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +30 -0
- package/dist/shared/git.contract.d.ts.map +1 -1
- package/dist/shared/git.contract.js +26 -0
- package/dist/shared/git.contract.js.map +1 -1
- package/dist/tenancy/static-tenant-source.d.ts +0 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +0 -2
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/migrations/0014_change_request_closed_reason.sql +1 -0
- package/migrations/0015_account_deactivation.sql +3 -0
- package/migrations/meta/0014_snapshot.json +2265 -0
- package/migrations/meta/0015_snapshot.json +2271 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +3 -3
- package/src/__tests__/kb-layout-config.test.ts +0 -2
- package/src/__tests__/retired-settings.test.ts +96 -0
- package/src/core/__tests__/gated-boot.test.ts +132 -0
- package/src/core/__tests__/lifecycle.test.ts +170 -1
- package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
- package/src/core/core-ports.ts +11 -1
- package/src/core/create-core-server.ts +69 -15
- package/src/core/create-core-services.ts +190 -27
- package/src/core/lifecycle.ts +98 -13
- package/src/core-config.ts +13 -14
- package/src/index.ts +12 -1
- package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
- package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
- package/src/modules/access/__tests__/access-requests.recut.test.ts +134 -0
- package/src/modules/access/__tests__/access-requests.routes.test.ts +610 -0
- package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
- package/src/modules/access/access-control.interface.ts +43 -12
- package/src/modules/access/access-control.service.ts +234 -36
- package/src/modules/access/access-requests.contract.ts +93 -0
- package/src/modules/access/access-requests.routes.ts +286 -0
- package/src/modules/access/access-requests.service.ts +420 -0
- package/src/modules/access/access.routes.ts +13 -15
- package/src/modules/access-model/access-grammar.ts +22 -0
- package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
- package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
- package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
- package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
- package/src/modules/auth/__tests__/auth.service.test.ts +39 -1
- package/src/modules/auth/account-admission.ts +78 -9
- package/src/modules/auth/account.routes.ts +63 -7
- package/src/modules/auth/auth.middleware.ts +19 -8
- package/src/modules/auth/auth.routes.ts +8 -0
- package/src/modules/auth/auth.service.ts +207 -7
- package/src/modules/auth/oidc-auth-provider.ts +7 -2
- package/src/modules/database/core-schema.ts +35 -27
- package/src/modules/github-app/__tests__/github-app.test.ts +848 -0
- package/src/modules/github-app/github-app.client.ts +268 -0
- package/src/modules/github-app/github-app.connection.ts +204 -0
- package/src/modules/github-app/github-app.routes.ts +359 -0
- package/src/modules/github-app/index.ts +19 -0
- package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
- package/src/modules/kb-fs/__tests__/remote-url.test.ts +38 -0
- package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
- package/src/modules/kb-fs/branch-name.ts +14 -1
- package/src/modules/kb-fs/remote-url.ts +35 -0
- package/src/modules/kb-fs/repo-path.ts +85 -0
- package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
- package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
- package/src/modules/kb-sync/sync-auth.ts +2 -2
- package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
- package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
- package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
- package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
- package/src/modules/plugins/__tests__/join-proposals.test.ts +100 -19
- package/src/modules/plugins/__tests__/join-requests.service.test.ts +27 -11
- package/src/modules/plugins/__tests__/join-requests.settlement.test.ts +371 -0
- package/src/modules/plugins/__tests__/plugins.routes.test.ts +1 -1
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
- package/src/modules/plugins/join-proposals.ts +87 -19
- package/src/modules/plugins/join-requests.service.ts +199 -39
- package/src/modules/plugins/plugins.routes.ts +3 -2
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
- package/src/modules/settings/__tests__/repository-source.test.ts +191 -0
- package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +386 -0
- package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +299 -0
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +236 -0
- package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
- package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
- package/src/modules/settings/deployment-settings.service.ts +117 -6
- package/src/modules/settings/managed-repository.ts +68 -0
- package/src/modules/settings/repository-source.ts +197 -0
- package/src/modules/settings/setup.routes.ts +463 -15
- package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
- package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
- package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +5 -1
- package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
- package/src/modules/skills/skill-access-requests.routes.ts +25 -75
- package/src/modules/skills/skills.contract.ts +91 -18
- package/src/modules/skills/skills.service.ts +278 -41
- package/src/modules/skills/skills.tools.ts +80 -8
- package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
- package/src/modules/tool-auth/external-api-key.service.ts +4 -2
- package/src/modules/tool-auth/internal-token.service.ts +3 -3
- package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
- package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
- package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
- package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
- package/src/modules/tool-helpers/tool-context.ts +15 -0
- package/src/modules/tool-helpers/tool-handler.ts +24 -4
- package/src/modules/tool-helpers/tool.contract.ts +3 -2
- package/src/modules/tool-helpers/validate-token.ts +1 -1
- package/src/modules/workflow/__tests__/apply-failure.test.ts +9 -1
- package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
- package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
- package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +159 -8
- package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
- package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +146 -40
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +405 -7
- package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
- package/src/modules/workflow/agent-tools/workflow.tools.ts +135 -38
- package/src/modules/workflow/file-lock.service.ts +31 -0
- package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
- package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +121 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +145 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-delete.test.ts +166 -0
- package/src/modules/workflow/git/git.service.ts +193 -4
- package/src/modules/workflow/git/node-git-runner.ts +6 -0
- package/src/modules/workflow/git/pull-request.service.ts +119 -0
- package/src/modules/workflow/pending-commits.service.ts +59 -1
- package/src/modules/workflow/workflow-hooks.ts +64 -26
- package/src/modules/workflow/workflow.routes.ts +3 -1
- package/src/modules/workflow/workflow.service.ts +290 -55
- package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
- package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
- package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
- package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
- package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
- package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
- package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +43 -0
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +235 -10
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +720 -17
- package/src/modules/workspace/agent-access.gate.ts +164 -0
- package/src/modules/workspace/routine-write-policy.ts +5 -6
- package/src/modules/workspace/session-sink.ts +5 -5
- package/src/modules/workspace/set-aside-clone.ts +96 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +297 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +242 -2
- package/src/modules/workspace/workspace.routes.ts +95 -3
- package/src/modules/workspace/workspace.service.ts +273 -25
- package/src/modules/workspace/workspace.tools.ts +373 -119
- package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
- package/src/modules/write-access/write-access.ts +153 -0
- package/src/shared/domain-errors.ts +89 -0
- package/src/shared/git.contract.ts +37 -0
- package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
- package/src/tenancy/static-tenant-source.ts +0 -3
- package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
- package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.policy.js +0 -62
- package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
- package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
- package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
- package/dist/modules/workflow/session-ontology.service.js +0 -147
- package/dist/modules/workflow/session-ontology.service.js.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
- package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
- package/dist/modules/workspace/session-ontology.gate.js +0 -161
- package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
- package/dist/shared/kb-layout.d.ts +0 -39
- package/dist/shared/kb-layout.d.ts.map +0 -1
- package/dist/shared/kb-layout.js +0 -103
- package/dist/shared/kb-layout.js.map +0 -1
- package/dist/shared/kb-layout.test.d.ts +0 -2
- package/dist/shared/kb-layout.test.d.ts.map +0 -1
- package/dist/shared/kb-layout.test.js +0 -75
- package/dist/shared/kb-layout.test.js.map +0 -1
- package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
- package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
- package/src/modules/workflow/session-ontology.policy.ts +0 -70
- package/src/modules/workflow/session-ontology.service.ts +0 -183
- package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
- package/src/modules/workspace/session-ontology.gate.ts +0 -191
- package/src/shared/kb-layout.test.ts +0 -98
- package/src/shared/kb-layout.ts +0 -102
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import type { Router, RequestHandler } from 'express';
|
|
2
2
|
import type { IToolRegistry, UtcpTool } from '../tool-registry/tool.contract.js';
|
|
3
|
-
import type
|
|
3
|
+
import { ToolError, type ToolContext } from '../tool-helpers/tool.contract.js';
|
|
4
4
|
import { toolDef } from '../tool-helpers/tool-def.js';
|
|
5
5
|
import type { ToolHandlerFactory } from '../tool-helpers/tool-handler.js';
|
|
6
6
|
import type { ISkillService } from './skills.contract.js';
|
|
7
7
|
import type { IAllowedToolsChecker } from './allowed-tools-check.js';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The optional `branch` both skill tools declare. Optional on purpose, unlike
|
|
11
|
+
* the required `branch` of the KB file tools: a skill read is answered by the
|
|
12
|
+
* released catalog by default, and naming a branch is the caller asking for
|
|
13
|
+
* something nobody has approved yet.
|
|
14
|
+
*/
|
|
15
|
+
const BRANCH_SKILLS_INPUT = {
|
|
16
|
+
type: 'string' as const,
|
|
17
|
+
description:
|
|
18
|
+
'Optional: a draft branch to read the skills from instead of the released (default) branch — ' +
|
|
19
|
+
'the branch you are working on. You must be able to read the branch and the skill on it; ' +
|
|
20
|
+
'a branch that does not exist answers 404 naming it. Omit it for the approved skills.',
|
|
21
|
+
};
|
|
22
|
+
|
|
9
23
|
/**
|
|
10
24
|
* Registers the two skill tools (both surfaces) and hosts their endpoints.
|
|
11
25
|
*
|
|
@@ -14,6 +28,13 @@ import type { IAllowedToolsChecker } from './allowed-tools-check.js';
|
|
|
14
28
|
* what exists right in the tool catalog, no `list_skills` round-trip needed, and
|
|
15
29
|
* it auto-updates as the default-branch catalog changes. The hosted routes are
|
|
16
30
|
* static; only the description text is dynamic.
|
|
31
|
+
*
|
|
32
|
+
* Both tools take an optional `branch`. Without it they answer from the
|
|
33
|
+
* released (default-branch) catalog — what the descriptions name, what the
|
|
34
|
+
* browser menu lists and what the MCP prompt surface serves. With it they read
|
|
35
|
+
* the skills as they are on that draft and say which of them nobody has
|
|
36
|
+
* approved, so an agent can try the skill it just wrote without waiting for a
|
|
37
|
+
* merge.
|
|
17
38
|
*/
|
|
18
39
|
export function registerSkillsTools(
|
|
19
40
|
registry: IToolRegistry,
|
|
@@ -32,8 +53,8 @@ export function registerSkillsTools(
|
|
|
32
53
|
router.post(
|
|
33
54
|
'/agent/tools/list_skills',
|
|
34
55
|
toolAuth,
|
|
35
|
-
toolHandler(async (
|
|
36
|
-
skills: await skillService.listSkills(ctx.user.email),
|
|
56
|
+
toolHandler(async (args, ctx: ToolContext) => ({
|
|
57
|
+
skills: await skillService.listSkills(ctx.user.email, { branch: branchArg(args) }),
|
|
37
58
|
})),
|
|
38
59
|
);
|
|
39
60
|
|
|
@@ -44,14 +65,38 @@ export function registerSkillsTools(
|
|
|
44
65
|
const name = typeof args.name === 'string' ? args.name : '';
|
|
45
66
|
const file = typeof args.file === 'string' ? args.file : undefined;
|
|
46
67
|
const version = typeof args.version === 'string' ? args.version : undefined;
|
|
68
|
+
const branch = branchArg(args);
|
|
47
69
|
if (!name) return { error: 'missing_name' };
|
|
48
|
-
|
|
70
|
+
// Refused rather than ranked: a `version` is a point in the released
|
|
71
|
+
// skill's history, a `branch` is a draft nobody has released — asked for
|
|
72
|
+
// together, neither answer is the one the caller meant, and guessing
|
|
73
|
+
// would serve instructions under a label that does not describe them.
|
|
74
|
+
if (branch !== undefined && version !== undefined && version.trim().length > 0) {
|
|
75
|
+
throw new ToolError(
|
|
76
|
+
'get_skill takes `branch` or `version`, not both: `branch` loads the skill as that draft has it ' +
|
|
77
|
+
'now, `version` loads a version the released skill declared. Pass one of them.',
|
|
78
|
+
400,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const result = await skillService.getSkill(ctx.user.email, name, file, { version, branch });
|
|
49
82
|
if (!allowedTools || !result.ok || result.kind !== 'skill') return result;
|
|
50
83
|
return { ...result, warnings: await allowedTools.check(ctx.user.email, result.skill.allowedTools) };
|
|
51
84
|
}),
|
|
52
85
|
);
|
|
53
86
|
}
|
|
54
87
|
|
|
88
|
+
/**
|
|
89
|
+
* The `branch` a tool call named, or undefined when it named none (the
|
|
90
|
+
* released catalog). A non-string or blank value is NOT a branch: it is taken
|
|
91
|
+
* as absent, which answers from the default branch — the approved one. The
|
|
92
|
+
* resolution of a real name, and the 404 for one the platform never heard of,
|
|
93
|
+
* belong to the workspace layer, exactly as on the file tools.
|
|
94
|
+
*/
|
|
95
|
+
function branchArg(args: Record<string, unknown>): string | undefined {
|
|
96
|
+
const branch = args.branch;
|
|
97
|
+
return typeof branch === 'string' && branch.trim().length > 0 ? branch : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
/** "Currently available skills: `a`, `b`." (or a no-skills note), filtered to what the caller may read. */
|
|
56
101
|
async function availableSkillsLine(skillService: ISkillService, userEmail?: string): Promise<string> {
|
|
57
102
|
const skills = await skillService.listSkills(userEmail);
|
|
@@ -67,9 +112,16 @@ async function buildListSkillsDef(skillService: ISkillService, userEmail?: strin
|
|
|
67
112
|
'for a skill that declares one, its current `version` (its SKILL.md `metadata.version`, else a ' +
|
|
68
113
|
'top-level `version`, else `lifecycle.version`). ' +
|
|
69
114
|
'Discover what skills exist before specialist work, then `get_skill` to load one. ' +
|
|
115
|
+
'Pass `branch` to list the skills as they are on a draft branch instead of the released set — ' +
|
|
116
|
+
'what you need to try a skill you just wrote there; each skill that differs from the released ' +
|
|
117
|
+
'one comes back with `unmerged: true` and that branch, meaning nobody has approved it. ' +
|
|
70
118
|
(await availableSkillsLine(skillService, userEmail)),
|
|
71
119
|
path: '/api/agent/tools/list_skills',
|
|
72
|
-
inputs: {
|
|
120
|
+
inputs: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: { branch: BRANCH_SKILLS_INPUT },
|
|
123
|
+
additionalProperties: false,
|
|
124
|
+
},
|
|
73
125
|
outputs: {
|
|
74
126
|
type: 'object',
|
|
75
127
|
properties: {
|
|
@@ -88,6 +140,13 @@ async function buildListSkillsDef(skillService: ISkillService, userEmail?: strin
|
|
|
88
140
|
'else `lifecycle.version` in its SKILL.md); absent when it declares none.',
|
|
89
141
|
},
|
|
90
142
|
path: { type: 'string' },
|
|
143
|
+
unmerged: {
|
|
144
|
+
type: 'boolean',
|
|
145
|
+
description:
|
|
146
|
+
'Only when `branch` was passed, and only on a skill that differs from the released one ' +
|
|
147
|
+
'(it exists only on that branch, or was changed there): nobody has approved what it says.',
|
|
148
|
+
},
|
|
149
|
+
branch: { type: 'string', description: 'With `unmerged`: the branch the skill was read from.' },
|
|
91
150
|
},
|
|
92
151
|
},
|
|
93
152
|
},
|
|
@@ -104,7 +163,7 @@ async function buildGetSkillDef(skillService: ISkillService, userEmail?: string)
|
|
|
104
163
|
'Load a skill by name: returns its full instructions (SKILL.md body) to follow, plus the skill ' +
|
|
105
164
|
'folder path and the list of bundled files. Pass `file` to fetch a bundled file’s content ' +
|
|
106
165
|
'(e.g. a script) instead of the body. Loads the latest copy unless `version` names an earlier ' +
|
|
107
|
-
'one the skill declared. ' +
|
|
166
|
+
'one the skill declared, or `branch` names a draft to load it from. ' +
|
|
108
167
|
(await availableSkillsLine(skillService, userEmail)),
|
|
109
168
|
path: '/api/agent/tools/get_skill',
|
|
110
169
|
inputs: {
|
|
@@ -126,6 +185,7 @@ async function buildGetSkillDef(skillService: ISkillService, userEmail?: string)
|
|
|
126
185
|
'recent commit that declared that version; a version the skill never declared answers ' +
|
|
127
186
|
'`version_not_found` with the versions it did declare.',
|
|
128
187
|
},
|
|
188
|
+
branch: BRANCH_SKILLS_INPUT,
|
|
129
189
|
},
|
|
130
190
|
required: ['name'],
|
|
131
191
|
additionalProperties: false,
|
|
@@ -134,8 +194,20 @@ async function buildGetSkillDef(skillService: ISkillService, userEmail?: string)
|
|
|
134
194
|
type: 'object',
|
|
135
195
|
description: 'On success carries `skill` (or `file` when `file` was passed); on failure carries `error`.',
|
|
136
196
|
properties: {
|
|
137
|
-
skill: {
|
|
138
|
-
|
|
197
|
+
skill: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
description:
|
|
200
|
+
'The loaded skill: name, description, body, files, …. Read from a `branch` that changed it, ' +
|
|
201
|
+
'it also carries `unmerged: true` and that branch, and its `body` BEGINS with one line ' +
|
|
202
|
+
'saying the skill comes from that unmerged branch and is not approved — treat it as a ' +
|
|
203
|
+
'proposal you are testing, not as approved instructions.',
|
|
204
|
+
},
|
|
205
|
+
file: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
description:
|
|
208
|
+
'A bundled file: name, file, path, content — plus `unmerged` and `branch` when it came off ' +
|
|
209
|
+
'a branch that changed the skill (the note stays beside the content, never inside it).',
|
|
210
|
+
},
|
|
139
211
|
warnings: {
|
|
140
212
|
type: 'array',
|
|
141
213
|
description:
|
|
@@ -8,6 +8,7 @@ import { InternalTokenService } from '../internal-token.service.js';
|
|
|
8
8
|
import { createManualAuthMiddleware } from '../tool-auth.middleware.js';
|
|
9
9
|
import type { IExternalApiKeyService } from '../external-api-key.interface.js';
|
|
10
10
|
import type { AuthService } from '../../auth/auth.service.js';
|
|
11
|
+
import { AuthBackendError } from '../../auth/account-admission.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* The manual endpoints (`/agent/utcp`, `/agent/internal/utcp`) accept a browser
|
|
@@ -17,6 +18,8 @@ import type { AuthService } from '../../auth/auth.service.js';
|
|
|
17
18
|
*/
|
|
18
19
|
const KEY = 'bevel_validkey';
|
|
19
20
|
const JWT = 'browser-jwt';
|
|
21
|
+
/** A valid session, presented while its account cannot be looked up. */
|
|
22
|
+
const JWT_DURING_OUTAGE = 'browser-jwt-during-outage';
|
|
20
23
|
|
|
21
24
|
const fakeExternalApiKeys = {
|
|
22
25
|
looksLikeExternalApiKey: (t: string) => typeof t === 'string' && t.startsWith('bevel_'),
|
|
@@ -25,10 +28,12 @@ const fakeExternalApiKeys = {
|
|
|
25
28
|
} as unknown as IExternalApiKeyService;
|
|
26
29
|
|
|
27
30
|
const fakeAuth = {
|
|
28
|
-
|
|
31
|
+
resolveSession: async (t: string) => {
|
|
29
32
|
if (t === JWT) return { userId: 'u-jwt', email: 'jwt@x' };
|
|
33
|
+
if (t === JWT_DURING_OUTAGE) throw new AuthBackendError(new Error('the database is down'));
|
|
30
34
|
throw new Error('bad jwt');
|
|
31
35
|
},
|
|
36
|
+
isActive: async () => true,
|
|
32
37
|
} as unknown as AuthService;
|
|
33
38
|
|
|
34
39
|
let server: HttpServer | undefined;
|
|
@@ -75,6 +80,14 @@ describe('createManualAuthMiddleware (read-only manual endpoints)', () => {
|
|
|
75
80
|
expect((await get(base, '/api/agent/utcp', 'garbage')).status).toBe(401);
|
|
76
81
|
});
|
|
77
82
|
|
|
83
|
+
it('answers 500, not 401, when the session’s account cannot be looked up', async () => {
|
|
84
|
+
const { base } = await start();
|
|
85
|
+
const r = await get(base, '/api/agent/utcp', JWT_DURING_OUTAGE);
|
|
86
|
+
// A 401 would sign a valid caller out over an outage.
|
|
87
|
+
expect(r.status).toBe(500);
|
|
88
|
+
expect(r.headers.get('www-authenticate')).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
78
91
|
it('refuses the INTERNAL manual to a browser JWT (403) but allows an internal token', async () => {
|
|
79
92
|
const { base, intToken } = await start();
|
|
80
93
|
expect((await get(base, '/api/agent/internal/utcp', JWT)).status).toBe(403);
|
|
@@ -108,7 +108,9 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
|
|
|
108
108
|
// Join users so a single round-trip resolves both "token is valid" and
|
|
109
109
|
// "load the user it belongs to". The unique index on token_hash makes
|
|
110
110
|
// this a point-lookup. `isNull(revokedAt)` is what enforces revocation
|
|
111
|
-
// — the row remains for audit
|
|
111
|
+
// — the row remains for audit — and `isNull(deactivatedAt)` is what
|
|
112
|
+
// stops a switched-off account's keys without revoking them, so
|
|
113
|
+
// switching it back on restores them.
|
|
112
114
|
const [row] = await this.db
|
|
113
115
|
.select({
|
|
114
116
|
tokenId: externalApiKeys.id,
|
|
@@ -119,7 +121,7 @@ export class ExternalApiKeyService implements IExternalApiKeyService {
|
|
|
119
121
|
})
|
|
120
122
|
.from(externalApiKeys)
|
|
121
123
|
.innerJoin(users, eq(externalApiKeys.userId, users.id))
|
|
122
|
-
.where(and(eq(externalApiKeys.tokenHash, tokenHash), isNull(externalApiKeys.revokedAt)))
|
|
124
|
+
.where(and(eq(externalApiKeys.tokenHash, tokenHash), isNull(externalApiKeys.revokedAt), isNull(users.deactivatedAt)))
|
|
123
125
|
.limit(1);
|
|
124
126
|
|
|
125
127
|
if (!row) return null;
|
|
@@ -12,9 +12,9 @@ export interface InternalTokenClaim {
|
|
|
12
12
|
/**
|
|
13
13
|
* The agent run / conversation id (the Mastra thread id) this token is scoped
|
|
14
14
|
* to, when minted per-run. Surfaced as `ToolContext.sessionId` so the
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* agent-access hooks can tell the in-process agent's run apart (the
|
|
16
|
+
* external/MCP path carries `sessionId` on the tool body instead). Absent
|
|
17
|
+
* for identity-only tokens.
|
|
18
18
|
*/
|
|
19
19
|
sessionId?: string;
|
|
20
20
|
/**
|
|
@@ -4,6 +4,7 @@ import { logger } from '../../shared/logging.js';
|
|
|
4
4
|
const log = logger('tools');
|
|
5
5
|
import type { IExternalApiKeyService } from './external-api-key.interface.js';
|
|
6
6
|
import type { AuthService } from '../auth/auth.service.js';
|
|
7
|
+
import { ACCOUNT_DEACTIVATED_MESSAGE, AuthBackendError } from '../auth/account-admission.js';
|
|
7
8
|
import { InternalTokenService } from './internal-token.service.js';
|
|
8
9
|
import { INVALID_CONNECTION_KEY_CHALLENGE, INVALID_CONNECTION_KEY_MESSAGE } from './connection-key-rejection.js';
|
|
9
10
|
|
|
@@ -79,10 +80,10 @@ export const requireInternalSource: RequestHandler = (req, res, next) => {
|
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
82
|
* Mirror of `requireInternalSource` for surfaces that must admit ONLY external
|
|
82
|
-
* callers. `start_session` mints a fresh chat thread as the caller's
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
83
|
+
* callers. `start_session` mints a fresh chat thread as the caller's session
|
|
84
|
+
* id; an internal (in-process agent) token already carries its run's
|
|
85
|
+
* `sessionId`, so letting it mint a new thread mid-run would silently split
|
|
86
|
+
* one conversation in two.
|
|
86
87
|
*
|
|
87
88
|
* "External" is decided by the VERIFIER, not by which credential shape
|
|
88
89
|
* authenticated: a `bevel_…` connection key resolves external, and so does an
|
|
@@ -122,6 +123,15 @@ export type VerifyResult =
|
|
|
122
123
|
export function createTokenVerifier(
|
|
123
124
|
externalApiKeyService: IExternalApiKeyService,
|
|
124
125
|
internalTokenService: InternalTokenService,
|
|
126
|
+
/**
|
|
127
|
+
* Whether the account an internal token was minted for is still on. An
|
|
128
|
+
* internal token carries only a user id and lives up to an hour (longer
|
|
129
|
+
* for the MCP loopback), so without this a switched-off account's running
|
|
130
|
+
* agent would keep working until it expired. Connection keys need no
|
|
131
|
+
* second look: their lookup reads the account. Every deployment passes
|
|
132
|
+
* it; absent, internal tokens are judged on their signature alone.
|
|
133
|
+
*/
|
|
134
|
+
accounts?: Pick<AuthService, 'isActive'>,
|
|
125
135
|
): (token: string | undefined) => Promise<VerifyResult> {
|
|
126
136
|
const missing = { ok: false as const, status: 401, message: 'A connection key or internal token is required' };
|
|
127
137
|
return async (token) => {
|
|
@@ -136,6 +146,9 @@ export function createTokenVerifier(
|
|
|
136
146
|
if (internalTokenService.looksLikeInternalToken(token)) {
|
|
137
147
|
const claim = internalTokenService.verify(token);
|
|
138
148
|
if (!claim) return { ok: false, status: 401, message: 'Invalid or expired internal token' };
|
|
149
|
+
if (accounts && !(await accounts.isActive(claim.userId))) {
|
|
150
|
+
return { ok: false, status: 401, message: ACCOUNT_DEACTIVATED_MESSAGE };
|
|
151
|
+
}
|
|
139
152
|
return claim.externalProxy
|
|
140
153
|
? {
|
|
141
154
|
ok: true,
|
|
@@ -170,8 +183,9 @@ export function createTokenVerifier(
|
|
|
170
183
|
export function createToolAuthMiddleware(
|
|
171
184
|
externalApiKeyService: IExternalApiKeyService,
|
|
172
185
|
internalTokenService: InternalTokenService,
|
|
186
|
+
accounts?: Pick<AuthService, 'isActive'>,
|
|
173
187
|
): RequestHandler {
|
|
174
|
-
const verify = createTokenVerifier(externalApiKeyService, internalTokenService);
|
|
188
|
+
const verify = createTokenVerifier(externalApiKeyService, internalTokenService, accounts);
|
|
175
189
|
return async (req: Request, res: Response, next: NextFunction) => {
|
|
176
190
|
const header = req.headers.authorization;
|
|
177
191
|
if (!header || !header.toLowerCase().startsWith('bearer ')) {
|
|
@@ -210,9 +224,9 @@ export function createToolAuthMiddleware(
|
|
|
210
224
|
export function createManualAuthMiddleware(
|
|
211
225
|
externalApiKeyService: IExternalApiKeyService,
|
|
212
226
|
internalTokenService: InternalTokenService,
|
|
213
|
-
authService: AuthService,
|
|
227
|
+
authService: Pick<AuthService, 'isActive' | 'resolveSession'>,
|
|
214
228
|
): RequestHandler {
|
|
215
|
-
const verify = createTokenVerifier(externalApiKeyService, internalTokenService);
|
|
229
|
+
const verify = createTokenVerifier(externalApiKeyService, internalTokenService, authService);
|
|
216
230
|
return async (req: Request, res: Response, next: NextFunction) => {
|
|
217
231
|
const header = req.headers.authorization;
|
|
218
232
|
if (!header || !header.toLowerCase().startsWith('bearer ')) {
|
|
@@ -241,13 +255,22 @@ export function createManualAuthMiddleware(
|
|
|
241
255
|
}
|
|
242
256
|
|
|
243
257
|
// Otherwise treat the bearer as a browser session JWT.
|
|
258
|
+
let userId: string;
|
|
244
259
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
260
|
+
({ userId } = await authService.resolveSession(token));
|
|
261
|
+
} catch (err) {
|
|
262
|
+
// The account could not be looked up: an outage, not a verdict on the
|
|
263
|
+
// session, and a 401 would sign a valid caller out over it.
|
|
264
|
+
if (err instanceof AuthBackendError) {
|
|
265
|
+
log.error('session account lookup failed:', { err: err.cause });
|
|
266
|
+
res.status(500).json({ error: 'Authentication backend unavailable' });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
249
269
|
res.setHeader('WWW-Authenticate', WWW_AUTH);
|
|
250
270
|
res.status(401).json({ error: 'Invalid or expired token' });
|
|
271
|
+
return;
|
|
251
272
|
}
|
|
273
|
+
req.toolAuth = { source: 'session', userId, scope: 'write' };
|
|
274
|
+
next();
|
|
252
275
|
};
|
|
253
276
|
}
|
|
@@ -12,6 +12,7 @@ import { createToolHandlerFactory } from '../tool-handler.js';
|
|
|
12
12
|
import { registerWorkspaceTools } from '../../workspace/workspace.tools.js';
|
|
13
13
|
import { RoutineWritePolicyService } from '../../workspace/routine-write-policy.js';
|
|
14
14
|
import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
|
|
15
|
+
import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
|
|
15
16
|
import { SpillStore } from '../../workspace/spill-store.js';
|
|
16
17
|
import { DocExtractService } from '../../workspace/file-readers/doc-extract.service.js';
|
|
17
18
|
import { NEW_ROLE_GUIDANCE } from '../../access-model/roles-yaml-guard.js';
|
|
@@ -68,13 +69,14 @@ async function start(): Promise<string> {
|
|
|
68
69
|
canOwner: async () => true,
|
|
69
70
|
canWriteBatchAtRef: async () => null,
|
|
70
71
|
canReadBatch: async (_w: string, _u: string, paths: string[]) => new Map(paths.map((p) => [p, true])),
|
|
72
|
+
// The `after` half of a move's or copy's preview — allowing everything, like the gates above.
|
|
73
|
+
previewAccessAfterRelocation: async () => ({ read: true, write: true, download: true, owner: true }),
|
|
71
74
|
} as never;
|
|
72
75
|
registerWorkspaceTools(registry, router, toolAuth, createToolHandlerFactory(resolve), new SpillStore(path.join(os.tmpdir(), 'bevel-test-spills')), new DocExtractService(docCache), allowAll, testKbContext({ kbDirName: KB }), {
|
|
73
|
-
|
|
74
|
-
enabled: false, // ontology boundary not under test here
|
|
75
|
-
kb: testKbContext({ kbDirName: KB }),
|
|
76
|
+
// No hook registered: the agent-access gate refuses nothing here.
|
|
76
77
|
recoveryBotEmail: 'recovery-bot@bevel.local',
|
|
77
78
|
hooks: new WorkflowHooks(),
|
|
79
|
+
notes: new ToolDescriptionNotes(),
|
|
78
80
|
}, new RoutineWritePolicyService(), {} as never);
|
|
79
81
|
const app = express();
|
|
80
82
|
app.use(express.json());
|
|
@@ -10,6 +10,7 @@ import { createManualRoutes } from '../../tool-registry/manual.routes.js';
|
|
|
10
10
|
import { registerWorkspaceTools } from '../../workspace/workspace.tools.js';
|
|
11
11
|
import { RoutineWritePolicyService } from '../../workspace/routine-write-policy.js';
|
|
12
12
|
import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
|
|
13
|
+
import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
|
|
13
14
|
import { SpillStore } from '../../workspace/spill-store.js';
|
|
14
15
|
import { DocExtractService } from '../../workspace/file-readers/doc-extract.service.js';
|
|
15
16
|
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
@@ -65,11 +66,10 @@ async function start(): Promise<string> {
|
|
|
65
66
|
// and their web_search / submit_feedback / cite_kb_node cases — moved to
|
|
66
67
|
// the enterprise repo with their modules; core registers only core tools.
|
|
67
68
|
registerWorkspaceTools(registry, router, toolAuth, th, new SpillStore('/tmp/bevel-test-spills'), new DocExtractService('/tmp/bevel-test-doc-extract'), accessControl, testKbContext(), {
|
|
68
|
-
|
|
69
|
-
enabled: false, // ontology boundary not under test here
|
|
70
|
-
kb: testKbContext(),
|
|
69
|
+
// No hook registered: the agent-access gate refuses nothing here.
|
|
71
70
|
recoveryBotEmail: 'recovery-bot@bevel.local',
|
|
72
71
|
hooks: new WorkflowHooks(),
|
|
72
|
+
notes: new ToolDescriptionNotes(),
|
|
73
73
|
}, new RoutineWritePolicyService(), {} as never /* sessionSink — start_session not exercised here */);
|
|
74
74
|
router.use(createManualRoutes(registry, toolAuth));
|
|
75
75
|
|
|
@@ -18,7 +18,11 @@ const externalApiKeyService = {
|
|
|
18
18
|
verifyAndLoadToken: async (t: string) =>
|
|
19
19
|
t === 'bevel_ok' ? { user: { id: 'u-ext', email: 'e@x', name: 'Ext' }, tokenId: 'tk-1' } : null,
|
|
20
20
|
} as never;
|
|
21
|
-
|
|
21
|
+
// Every account is on except `u-off`, which an admin switched off.
|
|
22
|
+
const authService = {
|
|
23
|
+
getUserById: async (id: string) => ({ id, email: 'e@x', name: 'N' }),
|
|
24
|
+
isActive: async (id: string) => id !== 'u-off',
|
|
25
|
+
} as never;
|
|
22
26
|
|
|
23
27
|
let tempDir = '';
|
|
24
28
|
const workspaceService = {
|
|
@@ -66,6 +70,12 @@ describe('validateToken (tool-author SDK entry)', () => {
|
|
|
66
70
|
expect((await fs.readFile('a.md')).toString()).toBe('hi');
|
|
67
71
|
});
|
|
68
72
|
|
|
73
|
+
it('throws a 401 ToolError for a valid internal token whose account was switched off', async () => {
|
|
74
|
+
const err = await validateToken(internalToken.mint({ userId: 'u-off' })).catch((e) => e);
|
|
75
|
+
expect(hasHttpStatus(err) && err.status).toBe(401);
|
|
76
|
+
expect((err as Error).message).toContain('switched off');
|
|
77
|
+
});
|
|
78
|
+
|
|
69
79
|
it('throws a 401 ToolError on a bad token and an empty token', async () => {
|
|
70
80
|
for (const bad of ['garbage', 'bevel_nope', '', undefined]) {
|
|
71
81
|
const err = await validateToken(bad as string).catch((e) => e);
|
|
@@ -12,6 +12,7 @@ import type { ICreatorAccess } from '../access-model/creator.js';
|
|
|
12
12
|
import type { GroupsIndex } from '../access-model/group-files.js';
|
|
13
13
|
import type { WorkspaceService } from '../workspace/workspace.service.js';
|
|
14
14
|
import { isAbsence } from '../../shared/fs.contract.js';
|
|
15
|
+
import { assertBranchProvided } from '../../shared/domain-errors.js';
|
|
15
16
|
import { branchForWorkspaceId } from '../../shared/workspace-id.js';
|
|
16
17
|
import type { WorkflowEventBus } from '../workflow/event-bus.js';
|
|
17
18
|
import type { AuthService } from '../auth/auth.service.js';
|
|
@@ -77,6 +78,20 @@ export function createToolContextResolver(deps: ToolContextDeps): ResolveToolCon
|
|
|
77
78
|
const { loadActiveGroups } = deps;
|
|
78
79
|
const fsCache = new Map<string, LocalFilesystem>();
|
|
79
80
|
const getFilesystem = async (branch: string): Promise<LocalFilesystem> => {
|
|
81
|
+
// THE choke point: every knowledge-base tool resolves its workspace
|
|
82
|
+
// through here, and a `call_tool_chain` call reaches the very same route
|
|
83
|
+
// over loopback — so refusing a branch-less call here refuses it on both
|
|
84
|
+
// paths at once, with no per-tool guard to keep in step.
|
|
85
|
+
//
|
|
86
|
+
// Before ANY workspace work, because both things downstream do with this
|
|
87
|
+
// value are wrong when it is absent: `getOrCreateForUser` defaults a
|
|
88
|
+
// missing branch to the deployment's default (protected) branch, so an
|
|
89
|
+
// omitted argument would silently act on `main`; and a present-but-absent
|
|
90
|
+
// value like the string "undefined" goes to `workspaceIdForBranch`, which
|
|
91
|
+
// makes it a workspace directory of that name and then tries to clone a
|
|
92
|
+
// branch nobody ever pushed. Failing closed here means no workspace is
|
|
93
|
+
// created and no clone is attempted.
|
|
94
|
+
assertBranchProvided(branch);
|
|
80
95
|
const cached = fsCache.get(branch);
|
|
81
96
|
if (cached) return cached;
|
|
82
97
|
const ws = await deps.workspaceService.getOrCreateForUser(user, branch);
|
|
@@ -6,6 +6,7 @@ import { hasHttpStatus, ToolError, type ToolHandler } from './tool.contract.js';
|
|
|
6
6
|
import { WorkflowDomainError } from '../../shared/domain-errors.js';
|
|
7
7
|
import { domainErrorBody } from '../../shared/http-errors.js';
|
|
8
8
|
import type { ResolveToolContext } from './tool-context.js';
|
|
9
|
+
import { alwaysWritable, READ_ONLY_CODE, refuseWriteTool, type IWriteAccess } from '../write-access/write-access.js';
|
|
9
10
|
import '../tool-auth/tool-auth.middleware.js'; // Express Request.toolAuth augmentation
|
|
10
11
|
|
|
11
12
|
function isAsyncIterable(v: unknown): v is AsyncIterable<unknown> {
|
|
@@ -29,7 +30,7 @@ export interface ToolHandlerOptions {
|
|
|
29
30
|
* The handler receives `req.body` as the flat args (UTCP's `body_field` already
|
|
30
31
|
* delivered the inner body as the request body).
|
|
31
32
|
*/
|
|
32
|
-
export function createToolHandlerFactory(resolve: ResolveToolContext) {
|
|
33
|
+
export function createToolHandlerFactory(resolve: ResolveToolContext, writeAccess: IWriteAccess = alwaysWritable) {
|
|
33
34
|
return function toolHandler(handler: ToolHandler, opts: ToolHandlerOptions = {}) {
|
|
34
35
|
return async (req: Request, res: Response): Promise<void> => {
|
|
35
36
|
const auth = req.toolAuth;
|
|
@@ -41,10 +42,29 @@ export function createToolHandlerFactory(resolve: ResolveToolContext) {
|
|
|
41
42
|
res.status(403).json({ error: 'This tool requires write access.' });
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
45
|
+
// Before anything is awaited: a client that goes away during the
|
|
46
|
+
// write-access check below must still abort the call.
|
|
47
|
+
//
|
|
48
|
+
// Asked of the RESPONSE. The request closes as soon as its body has
|
|
49
|
+
// been read, with the client still there and waiting, so its `close`
|
|
50
|
+
// says nothing about the client; the response closes when it has been
|
|
51
|
+
// sent or the connection is gone, and `writableEnded` tells the two
|
|
52
|
+
// apart.
|
|
44
53
|
const abort = new AbortController();
|
|
45
|
-
|
|
54
|
+
res.on('close', () => {
|
|
46
55
|
if (!res.writableEnded) abort.abort();
|
|
47
56
|
});
|
|
57
|
+
// The tool layer's half of the read-only gate: the HTTP gate lets every
|
|
58
|
+
// tool call through, since only here is a write tool told from a read.
|
|
59
|
+
if (opts.write) {
|
|
60
|
+
const refusal = await refuseWriteTool(writeAccess);
|
|
61
|
+
if (refusal !== null) {
|
|
62
|
+
res.status(403).json({ error: refusal, code: READ_ONLY_CODE });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// The client left while the verdict was awaited: nobody to answer.
|
|
66
|
+
if (abort.signal.aborted) return;
|
|
67
|
+
}
|
|
48
68
|
const body: unknown = req.body;
|
|
49
69
|
if (body !== undefined && body !== null && (typeof body !== 'object' || Array.isArray(body))) {
|
|
50
70
|
res.status(400).json({ error: 'Request body must be a JSON object.' });
|
|
@@ -53,8 +73,8 @@ export function createToolHandlerFactory(resolve: ResolveToolContext) {
|
|
|
53
73
|
const args = (body ?? {}) as Record<string, unknown>;
|
|
54
74
|
// `sessionId` rides the tool body like `branch` does: the external MCP
|
|
55
75
|
// proxy injects it (ask-tool continuity convention) and the in-process
|
|
56
|
-
// agent passes its thread id. Surfaced on the context so the
|
|
57
|
-
// gate can
|
|
76
|
+
// agent passes its thread id. Surfaced on the context so the
|
|
77
|
+
// agent-access gate can name the run without each handler re-reading args.
|
|
58
78
|
const sessionId = typeof args.sessionId === 'string' && args.sessionId.length > 0
|
|
59
79
|
? args.sessionId
|
|
60
80
|
: undefined;
|
|
@@ -34,8 +34,9 @@ export interface ToolContext {
|
|
|
34
34
|
* The agent run / conversation id this call belongs to (the agent thread id).
|
|
35
35
|
* Supplied as a `sessionId` field on the tool's input body — the external MCP
|
|
36
36
|
* proxy injects it via the `ask`-tool continuity convention, and the
|
|
37
|
-
* in-process agent passes its thread id.
|
|
38
|
-
*
|
|
37
|
+
* in-process agent passes its thread id. Handed to the agent-access hooks,
|
|
38
|
+
* which is how a deployment scopes a rule to one conversation. Absent for
|
|
39
|
+
* non-agent or unthreaded calls.
|
|
39
40
|
*/
|
|
40
41
|
sessionId?: string;
|
|
41
42
|
/**
|
|
@@ -35,7 +35,7 @@ export type ValidateToken = (
|
|
|
35
35
|
) => Promise<ToolContext>;
|
|
36
36
|
|
|
37
37
|
export function createToolValidator(deps: ToolValidatorDeps): ValidateToken {
|
|
38
|
-
const verify = createTokenVerifier(deps.externalApiKeyService, deps.internalTokenService);
|
|
38
|
+
const verify = createTokenVerifier(deps.externalApiKeyService, deps.internalTokenService, deps.authService);
|
|
39
39
|
const resolve = createToolContextResolver(deps);
|
|
40
40
|
return async (token, signal) => {
|
|
41
41
|
let result;
|
|
@@ -582,7 +582,7 @@ describe('WorkflowService — a refusal clears when a change makes it obsolete',
|
|
|
582
582
|
const { svc } = service(
|
|
583
583
|
store.db,
|
|
584
584
|
(e) => emitted.push(e as { kind: string }),
|
|
585
|
-
{},
|
|
585
|
+
{ carryApprovalsForward: vi.fn(async () => 0) },
|
|
586
586
|
{
|
|
587
587
|
git: {
|
|
588
588
|
pull: vi.fn(async () => ({ treeChanged: false })),
|
|
@@ -591,6 +591,14 @@ describe('WorkflowService — a refusal clears when a change makes it obsolete',
|
|
|
591
591
|
return { kind: 'merged', alreadyUpToDate: false };
|
|
592
592
|
}),
|
|
593
593
|
push: vi.fn(async () => undefined),
|
|
594
|
+
hasUnpushedCommits: vi.fn(async () => true),
|
|
595
|
+
pathsChangedBetween: vi.fn(async () => []),
|
|
596
|
+
// The head this test is named for: one sha before the merge,
|
|
597
|
+
// another after it.
|
|
598
|
+
resolvePrShas: vi
|
|
599
|
+
.fn()
|
|
600
|
+
.mockResolvedValueOnce({ baseSha: 'b'.repeat(40), headSha: 'a'.repeat(40) })
|
|
601
|
+
.mockResolvedValue({ baseSha: 'b'.repeat(40), headSha: 'd'.repeat(40) }),
|
|
594
602
|
} as unknown as Partial<GitService>,
|
|
595
603
|
prs: {
|
|
596
604
|
getPrDetail: vi.fn(async () => ({ state: 'open', branch: 'bo/suggestions', base: 'main', viewerCanUpdate: true })),
|