@bevel-software/platform-core-backend 0.22.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 +30 -11
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +9 -2
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +112 -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.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/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 +70 -2
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +176 -7
- 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/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/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/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +8 -3
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts +52 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +207 -19
- package/dist/modules/settings/setup.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-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 +75 -35
- 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 +69 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +153 -2
- package/dist/modules/workflow/git/git.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.service.d.ts +60 -6
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +111 -4
- 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 +59 -9
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +65 -24
- 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 +255 -25
- 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 +309 -115
- 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 +25 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +28 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/git.contract.d.ts +20 -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 +32 -14
- package/src/core/create-core-services.ts +129 -23
- 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.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.routes.ts +13 -15
- 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 +1 -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 +179 -6
- package/src/modules/auth/oidc-auth-provider.ts +7 -2
- package/src/modules/database/core-schema.ts +35 -27
- package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -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/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__/plugins.tools.test.ts +2 -1
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
- package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +91 -58
- package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +25 -8
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +14 -11
- 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 +8 -3
- package/src/modules/settings/setup.routes.ts +263 -19
- 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__/skills.service.test.ts +229 -5
- 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-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__/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.repository-replaced.test.ts +249 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +264 -6
- package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
- package/src/modules/workflow/agent-tools/workflow.tools.ts +86 -35
- 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/git.service.ts +168 -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.service.ts +121 -4
- 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.tools.agents-file.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +685 -14
- 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 +86 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +115 -34
- package/src/modules/workspace/workspace.routes.ts +95 -3
- package/src/modules/workspace/workspace.service.ts +256 -24
- package/src/modules/workspace/workspace.tools.ts +338 -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 +31 -0
- package/src/shared/git.contract.ts +27 -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,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { WorkflowValidationError } from '../../shared/domain-errors.js';
|
|
3
|
+
import { sanitizedPath } from '../../shared/printable.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Workspace-relative paths and the repository folder.
|
|
@@ -20,6 +21,51 @@ import { WorkflowValidationError } from '../../shared/domain-errors.js';
|
|
|
20
21
|
* rule were allowed to exist.
|
|
21
22
|
*/
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* NUL, the C0 controls (a line break among them) and DEL — refused in a
|
|
26
|
+
* workspace path wherever one enters.
|
|
27
|
+
*
|
|
28
|
+
* Not merely hygiene about names nobody can type. A `\n` is a SEPARATOR in
|
|
29
|
+
* git's own line-oriented stdin protocols: `cat-file --batch` reads one
|
|
30
|
+
* `<ref>:<path>` object name per LINE, so `Docs/secret.md\nzzz` is TWO specs.
|
|
31
|
+
* Everything upstream — the read gate, the download gate, the not-a-directory
|
|
32
|
+
* check — is then asked about the whole string, which names nothing, while git
|
|
33
|
+
* answers about `Docs/secret.md` and hands over its bytes. A file denied by
|
|
34
|
+
* its own frontmatter inside a folder its reader may list would be readable
|
|
35
|
+
* that way, and in a batch of several specs the extra answer shifts every
|
|
36
|
+
* result after it, so the access resolver reads one directory's `access.md` as
|
|
37
|
+
* another's.
|
|
38
|
+
*
|
|
39
|
+
* Refused HERE, at the normaliser every accepted path goes through, rather
|
|
40
|
+
* than at each route: the fence has to be at the mechanism, because the next
|
|
41
|
+
* caller to assemble a git spec from a path will not remember this. The
|
|
42
|
+
* shared `validateFilename` has always refused the same range per segment, so
|
|
43
|
+
* no name any surface can create is lost — this closes the gap between it and
|
|
44
|
+
* the paths that reach git. U+2028/U+2029 are NOT in it: they are nothing to
|
|
45
|
+
* git's protocols (which read bytes, and a line break there is 0x0A), a name
|
|
46
|
+
* carrying one is creatable today, and refusing it here would make an existing
|
|
47
|
+
* file unreadable. They are escaped for display instead, which is what
|
|
48
|
+
* `sanitizedPath` is for.
|
|
49
|
+
*/
|
|
50
|
+
// eslint-disable-next-line no-control-regex
|
|
51
|
+
const CONTROL_CHARACTERS = /[\x00-\x1F\x7F]/;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether `value` carries one of them — THE predicate, so the places that
|
|
55
|
+
* accept a NAME enforce the same rule as the normaliser that joins it into a
|
|
56
|
+
* path. The checkout folder name is the one that matters: it is not a caller's
|
|
57
|
+
* path, it is deployment configuration (`KB_DIR_NAME`, and the `kbDirName`
|
|
58
|
+
* setting), and it is PREFIXED onto every workspace-relative path. A name with
|
|
59
|
+
* a line break in it would therefore make the normaliser emit exactly the path
|
|
60
|
+
* this rule exists to exclude, from inputs that were each clean. Refused where
|
|
61
|
+
* the name is set — at boot in `core-config` and in the settings validator —
|
|
62
|
+
* so the deployment fails loudly instead of serving a path nothing downstream
|
|
63
|
+
* can safely read.
|
|
64
|
+
*/
|
|
65
|
+
export function hasControlCharacter(value: string): boolean {
|
|
66
|
+
return CONTROL_CHARACTERS.test(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
23
69
|
/**
|
|
24
70
|
* True when `wsPath` is the repository folder or lies under it.
|
|
25
71
|
*
|
|
@@ -33,6 +79,10 @@ import { WorkflowValidationError } from '../../shared/domain-errors.js';
|
|
|
33
79
|
*/
|
|
34
80
|
export function isInsideRepo(wsPath: string, kbDirName: string): boolean {
|
|
35
81
|
if (typeof wsPath !== 'string' || wsPath.includes('\\')) return false;
|
|
82
|
+
// A control character is not a spelling of any path under the repository —
|
|
83
|
+
// see {@link CONTROL_CHARACTERS}. Refused here too, so the check AFTER the
|
|
84
|
+
// normaliser cannot pass one that somehow skipped it.
|
|
85
|
+
if (CONTROL_CHARACTERS.test(wsPath)) return false;
|
|
36
86
|
const segments = wsPath.replace(/\/$/, '').split('/');
|
|
37
87
|
if (segments[0] !== kbDirName) return false;
|
|
38
88
|
return segments.every((segment) => segment !== '' && segment !== '.' && segment !== '..');
|
|
@@ -52,6 +102,22 @@ export function isInsideRepo(wsPath: string, kbDirName: string): boolean {
|
|
|
52
102
|
*/
|
|
53
103
|
export function assertInsideRepo(wsPath: string, kbDirName: string): void {
|
|
54
104
|
if (isInsideRepo(wsPath, kbDirName)) return;
|
|
105
|
+
// Said as what it is, before the suggestion below: a control character is
|
|
106
|
+
// not a path that landed in the wrong place, and a "use this instead"
|
|
107
|
+
// carrying an invisible character back to the caller would be nonsense.
|
|
108
|
+
if (typeof wsPath === 'string' && CONTROL_CHARACTERS.test(wsPath)) {
|
|
109
|
+
// Through `sanitizedPath`, like every other refusal that names a path: the
|
|
110
|
+
// character that makes this path invalid is also the character that would
|
|
111
|
+
// forge a second line of the answer, in the message and in the `path` a
|
|
112
|
+
// JSON consumer reads.
|
|
113
|
+
const shown = sanitizedPath(wsPath);
|
|
114
|
+
throw new WorkflowValidationError(
|
|
115
|
+
`"${shown}" contains a control character: a line break, a NUL or another invisible byte is not part of any ` +
|
|
116
|
+
'file name, and git reads a line break in a path as the end of one object name and the start of another. ' +
|
|
117
|
+
'Remove it and send the name as it is written.',
|
|
118
|
+
{ kind: 'path-control-character', path: shown, kbDirName },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
55
121
|
// The suggestion collapses what made the path wrong: backslashes, leading
|
|
56
122
|
// `./` or `/`, and any `..` climbing back out (a bare `..` included, so the
|
|
57
123
|
// suggestion is never itself a climb). The agent gets a path it can use.
|
|
@@ -103,6 +169,25 @@ export function normalizeWorkspacePath(wsPath: string, kbDirName: string): strin
|
|
|
103
169
|
// `\` separates segments on Windows, so `foo\..\..` is a climb this cannot
|
|
104
170
|
// read. Refused before anything is collapsed, never rewritten to `/`.
|
|
105
171
|
if (wsPath.includes('\\')) refuse();
|
|
172
|
+
// A line break (or any other control character) is a SEPARATOR to git, not
|
|
173
|
+
// part of a name — see {@link CONTROL_CHARACTERS}. Refused before the path
|
|
174
|
+
// is normalised, so no gate downstream is ever asked about a string that
|
|
175
|
+
// means one thing to it and two things to git.
|
|
176
|
+
if (CONTROL_CHARACTERS.test(wsPath)) refuse();
|
|
177
|
+
// …and the FOLDER NAME, which is prefixed onto the result rather than read
|
|
178
|
+
// from it: a clean path joined to `kb\nname` comes out carrying the line
|
|
179
|
+
// break, so the normaliser would itself emit what the check above refuses.
|
|
180
|
+
// Said as the configuration error it is — the caller's path is fine, and an
|
|
181
|
+
// operator reading a refusal about their own path would have nothing to fix.
|
|
182
|
+
// `core-config` and the `kbDirName` setting validator both refuse this at
|
|
183
|
+
// the point it is SET, so reaching here means the two drifted.
|
|
184
|
+
if (CONTROL_CHARACTERS.test(kbDirName)) {
|
|
185
|
+
throw new WorkflowValidationError(
|
|
186
|
+
`the configured checkout folder name (kbDirName / KB_DIR_NAME) contains a control character: "${sanitizedPath(kbDirName)}". ` +
|
|
187
|
+
'Every workspace path is prefixed with it, so no path can be resolved until the setting is corrected.',
|
|
188
|
+
{ kind: 'kb-dir-name-control-character', kbDirName: sanitizedPath(kbDirName) },
|
|
189
|
+
);
|
|
190
|
+
}
|
|
106
191
|
// The root-anchored form, and ONLY for the repository folder: `/<kbDirName>/…`
|
|
107
192
|
// is what the app's Copy path gives (it is the form a Markdown link resolves
|
|
108
193
|
// from) and names the same workspace path. `/tmp/x` and `//x` are not that
|
|
@@ -4,6 +4,7 @@ import express from 'express';
|
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { createKbSyncRoutes, isSyncRawBodyPath } from '../kb-sync.routes.js';
|
|
6
6
|
import type { IKbSyncService, SyncResult } from '../kb-sync.interface.js';
|
|
7
|
+
import { AuthBackendError } from '../../auth/account-admission.js';
|
|
7
8
|
|
|
8
9
|
const SECRET = 'a-very-long-and-random-sync-secret';
|
|
9
10
|
|
|
@@ -23,6 +24,8 @@ async function mount(opts: {
|
|
|
23
24
|
secret?: string;
|
|
24
25
|
result?: SyncResult;
|
|
25
26
|
admin?: boolean;
|
|
27
|
+
/** No account can be looked up: every session check fails as the database being down. */
|
|
28
|
+
outage?: boolean;
|
|
26
29
|
/** Install the production-shaped global JSON parser in front of the router. */
|
|
27
30
|
globalJson?: boolean;
|
|
28
31
|
}): Promise<{ base: string; sync: ReturnType<typeof vi.fn> }> {
|
|
@@ -44,7 +47,8 @@ async function mount(opts: {
|
|
|
44
47
|
kbSync,
|
|
45
48
|
syncSecret: () => opts.secret ?? SECRET,
|
|
46
49
|
authService: {
|
|
47
|
-
|
|
50
|
+
resolveSession: async (token: string) => {
|
|
51
|
+
if (opts.outage) throw new AuthBackendError(new Error('the database is down'));
|
|
48
52
|
if (token !== 'jwt-ok') throw new Error('bad token');
|
|
49
53
|
return { userId: 'u1', email: 'person@example.com' };
|
|
50
54
|
},
|
|
@@ -94,6 +98,27 @@ describe('POST /api/sync — credentials', () => {
|
|
|
94
98
|
expect((await post(person.base, { headers: { authorization: 'Bearer jwt-ok' } })).status).toBe(401);
|
|
95
99
|
});
|
|
96
100
|
|
|
101
|
+
it('500, not 401, when the session’s account cannot be looked up', async () => {
|
|
102
|
+
const { base, sync } = await mount({ admin: true, outage: true });
|
|
103
|
+
const res = await post(base, { headers: { authorization: 'Bearer jwt-ok' } });
|
|
104
|
+
// An outage is not a wrong credential: a 401 would send the admin to sign in again.
|
|
105
|
+
expect(res.status).toBe(500);
|
|
106
|
+
expect(res.headers.get('www-authenticate')).toBeNull();
|
|
107
|
+
expect(sync).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('the shared secret still works while accounts cannot be looked up', async () => {
|
|
111
|
+
const { base, sync } = await mount({ outage: true });
|
|
112
|
+
expect((await post(base, { headers: { authorization: `Bearer ${SECRET}` } })).status).toBe(200);
|
|
113
|
+
expect(sync).toHaveBeenCalledTimes(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('a request with no credential is still a 401 during an outage', async () => {
|
|
117
|
+
const { base } = await mount({ outage: true });
|
|
118
|
+
// No bearer at all: nothing was looked up, so nothing failed.
|
|
119
|
+
expect((await post(base)).status).toBe(401);
|
|
120
|
+
});
|
|
121
|
+
|
|
97
122
|
it('accepts the GitLab token header', async () => {
|
|
98
123
|
const { base } = await mount({});
|
|
99
124
|
const res = await post(base, { headers: { 'x-gitlab-token': SECRET } });
|
|
@@ -7,6 +7,7 @@ import { assertValidBranchName } from '../kb-fs/branch-name.js';
|
|
|
7
7
|
import type { IKbSyncService, SyncResult } from './kb-sync.interface.js';
|
|
8
8
|
import { parseSyncPayload, type SyncPayloadSource } from './sync-payload.js';
|
|
9
9
|
import { verifySyncCredential } from './sync-auth.js';
|
|
10
|
+
import { AuthBackendError } from '../auth/account-admission.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Remote sync — a git host or a pipeline tells Hexis the repository changed.
|
|
@@ -45,7 +46,7 @@ export interface KbSyncRouteDeps {
|
|
|
45
46
|
kbSync: IKbSyncService;
|
|
46
47
|
/** The configured secret, read per request so a setup-screen save applies at once. */
|
|
47
48
|
syncSecret: () => string;
|
|
48
|
-
authService: {
|
|
49
|
+
authService: { resolveSession(token: string): Promise<{ userId: string; email: string }> };
|
|
49
50
|
adminAccess: IAdminAccessService;
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -107,6 +108,7 @@ export function createKbSyncRoutes(deps: KbSyncRouteDeps): express.Router {
|
|
|
107
108
|
): Promise<void> {
|
|
108
109
|
res.setHeader(SYNC_RESPONSE_HEADER, SYNC_RESPONSE_MARKER);
|
|
109
110
|
const raw: Buffer | undefined = Buffer.isBuffer(req.body) ? req.body : undefined;
|
|
111
|
+
let lookupFailed: AuthBackendError | null = null as AuthBackendError | null;
|
|
110
112
|
|
|
111
113
|
const auth = await verifySyncCredential(
|
|
112
114
|
{
|
|
@@ -117,10 +119,11 @@ export function createKbSyncRoutes(deps: KbSyncRouteDeps): express.Router {
|
|
|
117
119
|
rawBody: raw,
|
|
118
120
|
},
|
|
119
121
|
{
|
|
120
|
-
verifyJwt: (token) => {
|
|
122
|
+
verifyJwt: async (token) => {
|
|
121
123
|
try {
|
|
122
|
-
return { email: deps.authService.
|
|
123
|
-
} catch {
|
|
124
|
+
return { email: (await deps.authService.resolveSession(token)).email };
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (err instanceof AuthBackendError) lookupFailed = err;
|
|
124
127
|
return null;
|
|
125
128
|
}
|
|
126
129
|
},
|
|
@@ -128,6 +131,13 @@ export function createKbSyncRoutes(deps: KbSyncRouteDeps): express.Router {
|
|
|
128
131
|
},
|
|
129
132
|
);
|
|
130
133
|
if (!auth.ok) {
|
|
134
|
+
// The session's account could not be looked up: an outage, not a wrong
|
|
135
|
+
// credential, and saying 401 would send an admin to sign in again.
|
|
136
|
+
if (lookupFailed) {
|
|
137
|
+
log.error('session account lookup failed:', { err: lookupFailed.cause });
|
|
138
|
+
res.status(500).json({ error: 'Authentication backend unavailable' });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
131
141
|
if (auth.status === 401) res.setHeader('WWW-Authenticate', 'Bearer realm="hexis-sync"');
|
|
132
142
|
res.status(auth.status).json({ error: auth.message });
|
|
133
143
|
return;
|
|
@@ -42,7 +42,7 @@ export interface SyncAuthInput {
|
|
|
42
42
|
|
|
43
43
|
export interface SyncSessionVerifier {
|
|
44
44
|
/** Resolve a browser JWT to its email, or null when it is not one. */
|
|
45
|
-
verifyJwt(token: string): { email: string } | null
|
|
45
|
+
verifyJwt(token: string): { email: string } | null | Promise<{ email: string } | null>;
|
|
46
46
|
isAdmin(email: string): Promise<boolean>;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -98,7 +98,7 @@ export async function verifySyncCredential(
|
|
|
98
98
|
// An admin's own session, carried as the bearer. Checked after the secret
|
|
99
99
|
// so a secret that happens to parse as a JWT is never mistaken for one.
|
|
100
100
|
if (bearer) {
|
|
101
|
-
const claim = session.verifyJwt(bearer);
|
|
101
|
+
const claim = await session.verifyJwt(bearer);
|
|
102
102
|
if (claim && (await session.isAdmin(claim.email))) {
|
|
103
103
|
return { ok: true, credential: { kind: 'admin-session', email: claim.email } };
|
|
104
104
|
}
|
|
@@ -5,6 +5,7 @@ import type { AuthService } from '../../auth/auth.service.js';
|
|
|
5
5
|
import type { IExternalApiKeyService } from '../../tool-auth/external-api-key.interface.js';
|
|
6
6
|
import type { BevelOAuthProvider } from '../oauth/bevel-oauth-provider.js';
|
|
7
7
|
import { InternalTokenService } from '../../tool-auth/internal-token.service.js';
|
|
8
|
+
import { AccountDeactivatedError, AuthBackendError } from '../../auth/account-admission.js';
|
|
8
9
|
|
|
9
10
|
const RESOURCE_METADATA_URL = 'https://bevel.example/.well-known/oauth-protected-resource/api/mcp';
|
|
10
11
|
|
|
@@ -19,10 +20,14 @@ function makeReqRes(authorization?: string) {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
function makeAuthService(verify?: (t: string) => { userId: string; email: string }) {
|
|
23
|
+
const verifyToken = vi.fn(verify ?? (() => {
|
|
24
|
+
throw new Error('invalid');
|
|
25
|
+
}));
|
|
22
26
|
return {
|
|
23
|
-
verifyToken
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
verifyToken,
|
|
28
|
+
// The session path checks the account is still on; every account here is.
|
|
29
|
+
resolveSession: vi.fn(async (t: string) => verifyToken(t)),
|
|
30
|
+
isActive: vi.fn(async () => true),
|
|
26
31
|
} as unknown as AuthService;
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -424,3 +429,55 @@ describe('createMcpAuthMiddleware', () => {
|
|
|
424
429
|
});
|
|
425
430
|
});
|
|
426
431
|
});
|
|
432
|
+
|
|
433
|
+
describe('createMcpAuthMiddleware — a switched-off account', () => {
|
|
434
|
+
const internal = new InternalTokenService({ secret: 'test-secret-32-bytes-long-enough!!' });
|
|
435
|
+
|
|
436
|
+
it('401s a session whose account was switched off, and says why', async () => {
|
|
437
|
+
const auth = makeAuthService();
|
|
438
|
+
(auth.resolveSession as ReturnType<typeof vi.fn>) = vi.fn(async () => {
|
|
439
|
+
throw new AccountDeactivatedError();
|
|
440
|
+
});
|
|
441
|
+
const { req, res, next, status, json } = makeReqRes('Bearer some.jwt.token');
|
|
442
|
+
await makeMw({ auth })(req, res, next);
|
|
443
|
+
expect(next).not.toHaveBeenCalled();
|
|
444
|
+
expect(status).toHaveBeenCalledWith(401);
|
|
445
|
+
expect(json.mock.calls[0][0].error).toContain('switched off');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('500s — not 401s — when the session account cannot be looked up', async () => {
|
|
449
|
+
const auth = makeAuthService();
|
|
450
|
+
(auth.resolveSession as ReturnType<typeof vi.fn>) = vi.fn(async () => {
|
|
451
|
+
throw new AuthBackendError(new Error('db down'));
|
|
452
|
+
});
|
|
453
|
+
const { req, res, next, status } = makeReqRes('Bearer some.jwt.token');
|
|
454
|
+
await makeMw({ auth })(req, res, next);
|
|
455
|
+
expect(next).not.toHaveBeenCalled();
|
|
456
|
+
expect(status).toHaveBeenCalledWith(500);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('401s an internal token whose account was switched off, and says why', async () => {
|
|
460
|
+
const token = internal.mint({ userId: 'user-7', externalProxy: true }, 60_000);
|
|
461
|
+
const auth = makeAuthService();
|
|
462
|
+
(auth.getUserById as ReturnType<typeof vi.fn>) = vi.fn(async () => ({ id: 'user-7', email: 'seven@example.com' }));
|
|
463
|
+
(auth.isActive as ReturnType<typeof vi.fn>) = vi.fn(async () => false);
|
|
464
|
+
const { req, res, next, status, json } = makeReqRes(`Bearer ${token}`);
|
|
465
|
+
await makeMw({ internal, auth })(req, res, next);
|
|
466
|
+
expect(next).not.toHaveBeenCalled();
|
|
467
|
+
expect(status).toHaveBeenCalledWith(401);
|
|
468
|
+
expect(json.mock.calls[0][0].error).toContain('switched off');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('500s — not 401s — when an internal token\'s account cannot be looked up', async () => {
|
|
472
|
+
const token = internal.mint({ userId: 'user-7', externalProxy: true }, 60_000);
|
|
473
|
+
const auth = makeAuthService();
|
|
474
|
+
(auth.getUserById as ReturnType<typeof vi.fn>) = vi.fn(async () => ({ id: 'user-7', email: 'seven@example.com' }));
|
|
475
|
+
(auth.isActive as ReturnType<typeof vi.fn>) = vi.fn(async () => {
|
|
476
|
+
throw new Error('db down');
|
|
477
|
+
});
|
|
478
|
+
const { req, res, next, status } = makeReqRes(`Bearer ${token}`);
|
|
479
|
+
await makeMw({ internal, auth })(req, res, next);
|
|
480
|
+
expect(next).not.toHaveBeenCalled();
|
|
481
|
+
expect(status).toHaveBeenCalledWith(500);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
@@ -24,6 +24,8 @@ import type { AgentEventInput, IAgentEventRecorder } from '../../audit/audit.con
|
|
|
24
24
|
|
|
25
25
|
let httpServer: HttpServer | undefined;
|
|
26
26
|
const cleanups: Array<() => Promise<void>> = [];
|
|
27
|
+
/** Every loopback call the skill routes saw this test, in order — with its body. */
|
|
28
|
+
let skillRequests: { tool: string; body: Record<string, unknown> }[] = [];
|
|
27
29
|
|
|
28
30
|
// Module-hosted tool endpoints + their registered defs: a stand-in echo `ask`
|
|
29
31
|
// (mirrors the real `{text, sessionId}` contract) and an erroring `boom`.
|
|
@@ -49,6 +51,7 @@ async function setup(deps?: {
|
|
|
49
51
|
/** The agent connection an OAuth caller arrived through (with `tokenId: null`). */
|
|
50
52
|
connectionId?: string | null;
|
|
51
53
|
}) {
|
|
54
|
+
skillRequests = [];
|
|
52
55
|
const registry = new ToolRegistry();
|
|
53
56
|
registry.registerExternalTool(
|
|
54
57
|
toolDef({
|
|
@@ -99,9 +102,47 @@ async function setup(deps?: {
|
|
|
99
102
|
// The two the Audit log's skill classification reads through: a catalog of
|
|
100
103
|
// one skill, and a file read that answers for any path (registered as
|
|
101
104
|
// tools only by the tests that need them, via `extraTools`).
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
105
|
+
//
|
|
106
|
+
// Both skill routes answer like the real ones: a `branch` in the body reads
|
|
107
|
+
// that draft (an extra, unmerged skill), no `branch` reads the released
|
|
108
|
+
// catalog. Every call is recorded, so a test can assert which of the two the
|
|
109
|
+
// prompt surface asked for.
|
|
110
|
+
const RFI_BODY = '# RFI\n\nAnswer the RFI.';
|
|
111
|
+
app.post('/api/agent/tools/list_skills', (req, res) => {
|
|
112
|
+
const b = (req.body ?? {}) as Record<string, unknown>;
|
|
113
|
+
skillRequests.push({ tool: 'list_skills', body: b });
|
|
114
|
+
const released = [{ name: 'rfi', description: 'RFI answers', path: 'Plugins/Sales/rfi' }];
|
|
115
|
+
res.json({
|
|
116
|
+
skills:
|
|
117
|
+
typeof b.branch === 'string'
|
|
118
|
+
? [
|
|
119
|
+
...released,
|
|
120
|
+
{ name: 'make-deck', description: 'Decks.', path: 'Plugins/make-deck', unmerged: true, branch: b.branch },
|
|
121
|
+
]
|
|
122
|
+
: released,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
app.post('/api/agent/tools/get_skill', (req, res) => {
|
|
126
|
+
const b = (req.body ?? {}) as Record<string, unknown>;
|
|
127
|
+
skillRequests.push({ tool: 'get_skill', body: b });
|
|
128
|
+
if (b.name !== 'rfi' && typeof b.branch !== 'string') {
|
|
129
|
+
res.json({ ok: false, error: 'not_found' });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const unmergedLine =
|
|
133
|
+
typeof b.branch === 'string' ? `This skill is read from the unmerged branch "${b.branch}" and is not approved.\n\n` : '';
|
|
134
|
+
res.json({
|
|
135
|
+
ok: true,
|
|
136
|
+
kind: 'skill',
|
|
137
|
+
skill: {
|
|
138
|
+
name: b.name,
|
|
139
|
+
description: 'RFI answers',
|
|
140
|
+
path: 'Plugins/Sales/rfi',
|
|
141
|
+
body: `${unmergedLine}${RFI_BODY}`,
|
|
142
|
+
files: [],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
});
|
|
105
146
|
app.post('/api/agent/tools/read_file', (req, res) => {
|
|
106
147
|
const b = (req.body ?? {}) as { path?: string };
|
|
107
148
|
res.json({ content: `contents of ${b.path}` });
|
|
@@ -773,10 +814,31 @@ describe('McpService — the Audit log records what an agent calls', () => {
|
|
|
773
814
|
it('records a prompt read as a skill, with an error outcome when the skill is unknown', async () => {
|
|
774
815
|
const recorder = spyRecorder();
|
|
775
816
|
const client = await setup({ auditRecorder: recorder });
|
|
776
|
-
//
|
|
777
|
-
await expect(client.getPrompt({ name: '
|
|
817
|
+
// A name the harness's catalog does not have: get_skill answers not_found.
|
|
818
|
+
await expect(client.getPrompt({ name: 'nope' })).rejects.toThrow(/Unknown skill/);
|
|
778
819
|
await settle();
|
|
779
|
-
expect(recorder.events[0]).toMatchObject({ kind: 'skill', name: '
|
|
820
|
+
expect(recorder.events[0]).toMatchObject({ kind: 'skill', name: 'nope', outcome: 'error' });
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Prompts ARE skills, and a skill offered as a prompt is one the
|
|
825
|
+
* organisation released: the two prompt handlers name no branch, so a draft's
|
|
826
|
+
* skills never reach a client's slash-command menu.
|
|
827
|
+
*/
|
|
828
|
+
it('serves prompts from the default branch only, never a draft', async () => {
|
|
829
|
+
const client = await setup();
|
|
830
|
+
const { prompts } = await client.listPrompts();
|
|
831
|
+
expect(prompts.map((p) => p.name)).toEqual(['rfi']);
|
|
832
|
+
|
|
833
|
+
const got = await client.getPrompt({ name: 'rfi' });
|
|
834
|
+
const text = got.messages[0].content.text as string;
|
|
835
|
+
expect(text).toContain('Answer the RFI.');
|
|
836
|
+
expect(text).not.toContain('unmerged');
|
|
837
|
+
|
|
838
|
+
// The loopback bodies prove it: had either handler passed a branch, the
|
|
839
|
+
// harness would have answered with the draft's skill and its notice.
|
|
840
|
+
expect(skillRequests.map((r) => r.tool)).toEqual(['list_skills', 'get_skill']);
|
|
841
|
+
expect(skillRequests.every((r) => r.body.branch === undefined)).toBe(true);
|
|
780
842
|
});
|
|
781
843
|
|
|
782
844
|
it('records a call refused for a missing sign-in as denied, without running the tool', async () => {
|
|
@@ -4,6 +4,7 @@ import { logger } from '../../shared/logging.js';
|
|
|
4
4
|
const log = logger('mcp-auth');
|
|
5
5
|
import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
|
|
6
6
|
import type { AuthService } from '../auth/auth.service.js';
|
|
7
|
+
import { ACCOUNT_DEACTIVATED_MESSAGE, AccountDeactivatedError, AuthBackendError } from '../auth/account-admission.js';
|
|
7
8
|
import type { IExternalApiKeyService } from '../tool-auth/external-api-key.interface.js';
|
|
8
9
|
import type { InternalTokenService } from '../tool-auth/internal-token.service.js';
|
|
9
10
|
import { rejectConnectionKey } from '../tool-auth/connection-key-rejection.js';
|
|
@@ -170,8 +171,10 @@ export function createMcpAuthMiddleware(
|
|
|
170
171
|
return;
|
|
171
172
|
}
|
|
172
173
|
let user;
|
|
174
|
+
let active = false;
|
|
173
175
|
try {
|
|
174
176
|
user = await authService.getUserById(claim.userId);
|
|
177
|
+
if (user) active = await authService.isActive(user.id);
|
|
175
178
|
} catch (err) {
|
|
176
179
|
log.error('internal-token user lookup failed:', { err });
|
|
177
180
|
res.status(500).json({ error: 'Authentication backend unavailable' });
|
|
@@ -181,6 +184,10 @@ export function createMcpAuthMiddleware(
|
|
|
181
184
|
unauthorized(res, 'Invalid or expired internal token');
|
|
182
185
|
return;
|
|
183
186
|
}
|
|
187
|
+
if (!active) {
|
|
188
|
+
unauthorized(res, ACCOUNT_DEACTIVATED_MESSAGE);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
184
191
|
req.userId = user.id;
|
|
185
192
|
req.userEmail = user.email;
|
|
186
193
|
// The local server's exchanged grant names its agent connection (see
|
|
@@ -214,13 +221,20 @@ export function createMcpAuthMiddleware(
|
|
|
214
221
|
// JWT path — same logic as `createAuthMiddleware` in modules/auth, kept
|
|
215
222
|
// duplicated rather than imported because that one writes its own 401
|
|
216
223
|
// body shape and we want the WWW-Authenticate header set.
|
|
224
|
+
let session: { userId: string; email: string };
|
|
217
225
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
session = await authService.resolveSession(token);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
if (err instanceof AuthBackendError) {
|
|
229
|
+
log.error('session account lookup failed:', { err: err.cause });
|
|
230
|
+
res.status(500).json({ error: 'Authentication backend unavailable' });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
unauthorized(res, err instanceof AccountDeactivatedError ? err.message : 'Invalid or expired token');
|
|
234
|
+
return;
|
|
224
235
|
}
|
|
236
|
+
req.userId = session.userId;
|
|
237
|
+
req.userEmail = session.email;
|
|
238
|
+
next();
|
|
225
239
|
};
|
|
226
240
|
}
|
|
@@ -309,7 +309,8 @@ export class BevelOAuthProvider implements OAuthServerProvider {
|
|
|
309
309
|
// the revoke (a refresh that raced it) is dead on arrival rather than
|
|
310
310
|
// live until its own expiry. A token from before connections existed
|
|
311
311
|
// (null `connection_id`) has no connection to be revoked and verifies on
|
|
312
|
-
// its own row alone.
|
|
312
|
+
// its own row alone. A switched-off account's tokens fail here too, and
|
|
313
|
+
// come back with it.
|
|
313
314
|
const [row] = await this.deps.db
|
|
314
315
|
.select({
|
|
315
316
|
id: oauthTokens.id,
|
|
@@ -330,6 +331,7 @@ export class BevelOAuthProvider implements OAuthServerProvider {
|
|
|
330
331
|
isNull(oauthTokens.revokedAt),
|
|
331
332
|
gt(oauthTokens.expiresAt, now),
|
|
332
333
|
or(isNull(oauthTokens.connectionId), isNull(agentConnections.revokedAt)),
|
|
334
|
+
isNull(users.deactivatedAt),
|
|
333
335
|
),
|
|
334
336
|
)
|
|
335
337
|
.limit(1);
|
|
@@ -53,7 +53,8 @@ describe('the creation endpoints behind the key-or-session gate', () => {
|
|
|
53
53
|
} as never;
|
|
54
54
|
const authService = {
|
|
55
55
|
getUserById: async (id: string) => (id === ALICE.id ? ALICE : null),
|
|
56
|
-
|
|
56
|
+
// A session is accepted only for an account that is on; Alice's is.
|
|
57
|
+
resolveSession: async (t: string) => {
|
|
57
58
|
if (t !== 'session-alice') throw new Error('bad token');
|
|
58
59
|
return { userId: ALICE.id, email: ALICE.email };
|
|
59
60
|
},
|
|
@@ -413,6 +413,22 @@ describe('DeploymentSettingsService — validation', () => {
|
|
|
413
413
|
);
|
|
414
414
|
});
|
|
415
415
|
|
|
416
|
+
it('rejects a directory name carrying a line break, which every path is prefixed with', async () => {
|
|
417
|
+
// The name is joined onto every workspace-relative path, and a line break
|
|
418
|
+
// in a path is a separator to git's `cat-file --batch` — so a name
|
|
419
|
+
// carrying one would make the normaliser emit a path it refuses from a
|
|
420
|
+
// caller. Refused where the name is SET, which is here and at boot.
|
|
421
|
+
const { db } = makeDb();
|
|
422
|
+
const settings = new DeploymentSettingsService(db, ENC_KEY);
|
|
423
|
+
for (const bad of ['kb\nname', 'kb\rname', 'kb\u0000name', 'kb\u007Fname']) {
|
|
424
|
+
await expect(settings.save({ kbDirName: bad }, null), bad).rejects.toThrow(
|
|
425
|
+
/Invalid settings/,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
// The ordinary name beside them, so the rule is not simply refusing both.
|
|
429
|
+
await expect(settings.save({ kbDirName: 'company-brain' }, null)).resolves.toBeTruthy();
|
|
430
|
+
});
|
|
431
|
+
|
|
416
432
|
it('rejects a username that would break out of the credential-helper snippet', async () => {
|
|
417
433
|
const { db } = makeDb();
|
|
418
434
|
const settings = new DeploymentSettingsService(db, ENC_KEY);
|