@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
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import type { Server } from 'node:http';
|
|
4
|
+
import type { AddressInfo } from 'node:net';
|
|
5
|
+
import type { Request, Response } from 'express';
|
|
6
|
+
import {
|
|
7
|
+
alwaysWritable,
|
|
8
|
+
createWriteAccessRoutes,
|
|
9
|
+
createWriteGateMiddleware,
|
|
10
|
+
isAlwaysWritable,
|
|
11
|
+
READ_ONLY_CODE,
|
|
12
|
+
type IWriteAccess,
|
|
13
|
+
} from '../write-access.js';
|
|
14
|
+
import { createToolHandlerFactory } from '../../tool-helpers/tool-handler.js';
|
|
15
|
+
import type { ResolveToolContext } from '../../tool-helpers/tool-context.js';
|
|
16
|
+
|
|
17
|
+
const READ_ONLY_MESSAGE = 'This workspace has 5 people switched on and room for 3.';
|
|
18
|
+
const readOnly: IWriteAccess = { canWrite: async () => ({ ok: false, message: READ_ONLY_MESSAGE }) };
|
|
19
|
+
|
|
20
|
+
const servers: Server[] = [];
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await Promise.all(servers.splice(0).map((s) => new Promise<void>((resolve) => s.close(() => resolve()))));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
async function listen(app: express.Express): Promise<string> {
|
|
26
|
+
const server = app.listen(0);
|
|
27
|
+
servers.push(server);
|
|
28
|
+
await new Promise<void>((resolve) => server.once('listening', () => resolve()));
|
|
29
|
+
const { port } = server.address() as AddressInfo;
|
|
30
|
+
return `http://127.0.0.1:${port}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('isAlwaysWritable', () => {
|
|
34
|
+
it.each([
|
|
35
|
+
['GET', '/api/workspace/w1/file'],
|
|
36
|
+
['HEAD', '/api/workspace/w1/file'],
|
|
37
|
+
['POST', '/health'],
|
|
38
|
+
['POST', '/api/auth/login'],
|
|
39
|
+
['POST', '/api/mcp'],
|
|
40
|
+
['POST', '/api/mcp/external-api-keys'],
|
|
41
|
+
['POST', '/api/agent/tools/write_file'],
|
|
42
|
+
['POST', '/api/admin/accounts'],
|
|
43
|
+
['POST', '/api/admin/accounts/u1/deactivate'],
|
|
44
|
+
['DELETE', '/api/admin/accounts/u1'],
|
|
45
|
+
['POST', '/api/sync'],
|
|
46
|
+
['POST', '/api/setup/settings'],
|
|
47
|
+
['POST', '/api/workspace/w1/access/batch'],
|
|
48
|
+
['POST', '/api/workspace/w1/workflow/locks/heartbeat'],
|
|
49
|
+
['POST', '/api/events/s1/focus'],
|
|
50
|
+
])('lets %s %s through', (method, path) => {
|
|
51
|
+
expect(isAlwaysWritable(method, path)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it.each([
|
|
55
|
+
['PUT', '/api/workspace/w1/file'],
|
|
56
|
+
['DELETE', '/api/workspace/w1/file'],
|
|
57
|
+
['POST', '/api/workspace/w1/workflow/changes'],
|
|
58
|
+
['POST', '/api/workflow/change-requests/7/merge'],
|
|
59
|
+
['POST', '/api/workspace/w1/workflow/locks'],
|
|
60
|
+
['POST', '/api/plugins'],
|
|
61
|
+
['POST', '/api/admin/groups'],
|
|
62
|
+
// A look-alike of an allowed path is not that path.
|
|
63
|
+
['POST', '/api/admin/accountsx'],
|
|
64
|
+
['POST', '/api/workspace/w1/access/batch/extra'],
|
|
65
|
+
// A route nobody listed is refused by default.
|
|
66
|
+
['POST', '/api/some-new-route'],
|
|
67
|
+
// The API root is inside the gated namespace too.
|
|
68
|
+
['POST', '/api'],
|
|
69
|
+
])('holds %s %s for the port', (method, path) => {
|
|
70
|
+
expect(isAlwaysWritable(method, path)).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("lets the host's own paths through", () => {
|
|
74
|
+
expect(isAlwaysWritable('PUT', '/api/cloud/workspace/billing/seats', ['/api/cloud/workspace/billing'])).toBe(true);
|
|
75
|
+
expect(isAlwaysWritable('PUT', '/api/cloud/workspace/sign-in/domains', ['/api/cloud/workspace/billing'])).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('createWriteGateMiddleware', () => {
|
|
80
|
+
function appWith(writeAccess: IWriteAccess) {
|
|
81
|
+
const app = express();
|
|
82
|
+
app.use(createWriteGateMiddleware(writeAccess));
|
|
83
|
+
app.all('/{*any}', (_req, res) => {
|
|
84
|
+
res.json({ reached: true });
|
|
85
|
+
});
|
|
86
|
+
return app;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
it("refuses a change with the port's words and a code the app recognises", async () => {
|
|
90
|
+
const base = await listen(appWith(readOnly));
|
|
91
|
+
const res = await fetch(`${base}/api/workspace/w1/file`, { method: 'PUT' });
|
|
92
|
+
expect(res.status).toBe(403);
|
|
93
|
+
expect(await res.json()).toEqual({ error: READ_ONLY_MESSAGE, code: READ_ONLY_CODE });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The router matches a path whatever its letter case, so the gate has to
|
|
98
|
+
* read it the same way: here the route is mounted as the server mounts
|
|
99
|
+
* its own, under `/api`, and a spelling the gate did not recognise as
|
|
100
|
+
* that namespace would reach it.
|
|
101
|
+
*/
|
|
102
|
+
it('refuses a change however the path is cased, since the router matches it either way', async () => {
|
|
103
|
+
const app = express();
|
|
104
|
+
app.use(createWriteGateMiddleware(readOnly));
|
|
105
|
+
const routes = express.Router();
|
|
106
|
+
const wrote = vi.fn();
|
|
107
|
+
routes.put('/workspace/:id/file', (_req, res) => {
|
|
108
|
+
wrote();
|
|
109
|
+
res.json({ wrote: true });
|
|
110
|
+
});
|
|
111
|
+
app.use('/api', routes);
|
|
112
|
+
const base = await listen(app);
|
|
113
|
+
for (const path of ['/api/workspace/w1/file', '/API/workspace/w1/file', '/Api/Workspace/w1/File']) {
|
|
114
|
+
const res = await fetch(`${base}${path}`, { method: 'PUT' });
|
|
115
|
+
expect({ path, status: res.status }).toEqual({ path, status: 403 });
|
|
116
|
+
}
|
|
117
|
+
expect(wrote).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('keeps an open route open however it is cased', async () => {
|
|
121
|
+
const base = await listen(appWith(readOnly));
|
|
122
|
+
expect((await fetch(`${base}/API/Auth/login`, { method: 'POST' })).status).toBe(200);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('still serves reads, and the routes that sign people in and manage accounts', async () => {
|
|
126
|
+
const base = await listen(appWith(readOnly));
|
|
127
|
+
expect((await fetch(`${base}/api/workspace/w1/file`)).status).toBe(200);
|
|
128
|
+
expect((await fetch(`${base}/api/auth/login`, { method: 'POST' })).status).toBe(200);
|
|
129
|
+
expect((await fetch(`${base}/api/admin/accounts/u1/deactivate`, { method: 'POST' })).status).toBe(200);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('does not ask the port about a request it lets through anyway', async () => {
|
|
133
|
+
const canWrite = vi.fn(readOnly.canWrite);
|
|
134
|
+
const base = await listen(appWith({ canWrite }));
|
|
135
|
+
await fetch(`${base}/api/workspace/w1/file`);
|
|
136
|
+
await fetch(`${base}/api/auth/login`, { method: 'POST' });
|
|
137
|
+
expect(canWrite).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('lets a change through while the port says yes', async () => {
|
|
141
|
+
const base = await listen(appWith({ canWrite: async () => ({ ok: true }) }));
|
|
142
|
+
expect((await fetch(`${base}/api/workspace/w1/file`, { method: 'PUT' })).status).toBe(200);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('lets a change through when the port itself fails, rather than freezing the workspace', async () => {
|
|
146
|
+
const base = await listen(appWith({ canWrite: async () => Promise.reject(new Error('billing down')) }));
|
|
147
|
+
expect((await fetch(`${base}/api/workspace/w1/file`, { method: 'PUT' })).status).toBe(200);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('is a no-op on core, which is always writable', async () => {
|
|
151
|
+
const base = await listen(appWith(alwaysWritable));
|
|
152
|
+
expect((await fetch(`${base}/api/workspace/w1/file`, { method: 'PUT' })).status).toBe(200);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('the tool layer', () => {
|
|
157
|
+
/**
|
|
158
|
+
* One tool, mounted the way the server mounts it and called over HTTP: a
|
|
159
|
+
* parsed body, then an auth step that does not wait for anything, then the
|
|
160
|
+
* handler. A request closes once its body has been read, so only a real
|
|
161
|
+
* request shows what the handler makes of that.
|
|
162
|
+
*/
|
|
163
|
+
async function mounted(writeAccess: IWriteAccess, write: boolean) {
|
|
164
|
+
const resolve = (async () => ({})) as unknown as ResolveToolContext;
|
|
165
|
+
const handler = vi.fn(async () => ({ done: true }));
|
|
166
|
+
const closed = { count: 0 };
|
|
167
|
+
const app = express();
|
|
168
|
+
app.use(express.json());
|
|
169
|
+
app.use((req: Request, res: Response, next) => {
|
|
170
|
+
req.toolAuth = { source: 'external', userId: 'u1', scope: 'write' } as Request['toolAuth'];
|
|
171
|
+
res.on('close', () => {
|
|
172
|
+
closed.count += 1;
|
|
173
|
+
});
|
|
174
|
+
next();
|
|
175
|
+
});
|
|
176
|
+
app.post('/api/agent/tools/a_tool', createToolHandlerFactory(resolve, writeAccess)(handler as never, { write }));
|
|
177
|
+
const url = `${await listen(app)}/api/agent/tools/a_tool`;
|
|
178
|
+
const post = (signal?: AbortSignal) =>
|
|
179
|
+
fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}', signal });
|
|
180
|
+
return { post, handler, closed };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
it('refuses a write tool while the deployment is read-only', async () => {
|
|
184
|
+
const { post, handler } = await mounted(readOnly, true);
|
|
185
|
+
const res = await post();
|
|
186
|
+
expect(res.status).toBe(403);
|
|
187
|
+
expect(await res.json()).toEqual({ error: READ_ONLY_MESSAGE, code: READ_ONLY_CODE });
|
|
188
|
+
expect(handler).not.toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The verdict is awaited, and by then the request has closed: its body was
|
|
193
|
+
* read. Taken for the client leaving, that left every write tool on a
|
|
194
|
+
* deployment with a port unanswered, for as long as the caller would wait.
|
|
195
|
+
*/
|
|
196
|
+
it('answers a write tool the port allows, with the client still there', async () => {
|
|
197
|
+
const { post, handler } = await mounted({ canWrite: async () => ({ ok: true }) }, true);
|
|
198
|
+
const res = await post();
|
|
199
|
+
expect(res.status).toBe(200);
|
|
200
|
+
expect(await res.json()).toEqual({ done: true });
|
|
201
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('does not run a write tool whose client left while the verdict was awaited', async () => {
|
|
205
|
+
const asked = { count: 0 };
|
|
206
|
+
let allow: () => void = () => undefined;
|
|
207
|
+
const { post, handler, closed } = await mounted(
|
|
208
|
+
{
|
|
209
|
+
canWrite: () => {
|
|
210
|
+
asked.count += 1;
|
|
211
|
+
return new Promise((resolve) => {
|
|
212
|
+
allow = () => resolve({ ok: true });
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
true,
|
|
217
|
+
);
|
|
218
|
+
const leaving = new AbortController();
|
|
219
|
+
const call = post(leaving.signal).catch(() => undefined);
|
|
220
|
+
await vi.waitFor(() => expect(asked.count).toBe(1));
|
|
221
|
+
leaving.abort();
|
|
222
|
+
await call;
|
|
223
|
+
await vi.waitFor(() => expect(closed.count).toBe(1));
|
|
224
|
+
allow();
|
|
225
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
226
|
+
expect(handler).not.toHaveBeenCalled();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('still runs a read tool', async () => {
|
|
230
|
+
const { post, handler } = await mounted(readOnly, false);
|
|
231
|
+
expect((await post()).status).toBe(200);
|
|
232
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('GET /write-access', () => {
|
|
237
|
+
it("says whether the deployment can be changed, and the port's words when not", async () => {
|
|
238
|
+
const app = express();
|
|
239
|
+
app.use('/api', createWriteAccessRoutes(readOnly));
|
|
240
|
+
const base = await listen(app);
|
|
241
|
+
expect(await (await fetch(`${base}/api/write-access`)).json()).toEqual({ writable: false, message: READ_ONLY_MESSAGE });
|
|
242
|
+
|
|
243
|
+
const open = express();
|
|
244
|
+
open.use('/api', createWriteAccessRoutes(alwaysWritable));
|
|
245
|
+
const openBase = await listen(open);
|
|
246
|
+
expect(await (await fetch(`${openBase}/api/write-access`)).json()).toEqual({ writable: true });
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import express, { type RequestHandler } from 'express';
|
|
2
|
+
import { logger } from '../../shared/logging.js';
|
|
3
|
+
|
|
4
|
+
const log = logger('write-access');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Whether the deployment may be CHANGED right now. The seam a host that
|
|
8
|
+
* sells seats needs for a workspace that has more people on it than its plan
|
|
9
|
+
* allows: nobody is removed and everyone can still sign in and read, but
|
|
10
|
+
* nothing can be edited until an admin brings the count down or the plan up
|
|
11
|
+
* — the way Notion handles a lapsed workspace.
|
|
12
|
+
*
|
|
13
|
+
* Core is always writable. The port is asked on every request that would
|
|
14
|
+
* change something (a mutating HTTP route, a write tool), so a host answers
|
|
15
|
+
* from something cheap — its own cached view of the plan.
|
|
16
|
+
*/
|
|
17
|
+
export type WriteAccessVerdict = { ok: true } | { ok: false; message: string };
|
|
18
|
+
|
|
19
|
+
export interface IWriteAccess {
|
|
20
|
+
canWrite(): Promise<WriteAccessVerdict>;
|
|
21
|
+
/**
|
|
22
|
+
* Paths of the host's own routes that must stay usable while the
|
|
23
|
+
* deployment is read-only — above all, the ones that END it (buying
|
|
24
|
+
* seats, the billing portal). Matched as prefixes of the request path.
|
|
25
|
+
*/
|
|
26
|
+
readonly alwaysWritablePaths?: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Core's default: always writable. */
|
|
30
|
+
export const alwaysWritable: IWriteAccess = {
|
|
31
|
+
canWrite: async () => ({ ok: true }),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** The `code` a refused write answers with, so a client can tell it from a permission error. */
|
|
35
|
+
export const READ_ONLY_CODE = 'workspace_read_only';
|
|
36
|
+
|
|
37
|
+
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The mutating routes that stay open while the deployment is read-only.
|
|
41
|
+
* EVERY OTHER mutating route under `/api` is refused, so a route added
|
|
42
|
+
* later is read-only until someone decides otherwise — the safe default for
|
|
43
|
+
* a list nobody re-reads when adding a route.
|
|
44
|
+
*
|
|
45
|
+
* What stays open, and why:
|
|
46
|
+
* - Signing in and out of everything: sessions, agent connections and
|
|
47
|
+
* their keys. A read-only workspace is still one people read.
|
|
48
|
+
* - Account administration: the way an admin brings the number of people
|
|
49
|
+
* down. Erasing an account with `removeFromAccess` commits to the
|
|
50
|
+
* knowledge base, and that commit is part of the same act.
|
|
51
|
+
* - The tool surface: every tool call is judged by the tool layer itself,
|
|
52
|
+
* which knows a read tool from a write tool (see `toolHandler`); the MCP
|
|
53
|
+
* endpoint only relays to it.
|
|
54
|
+
* - Deployment setup, tool secrets and pulling remote changes in: the
|
|
55
|
+
* deployment's configuration, not its content, and what an admin needs
|
|
56
|
+
* to keep it working.
|
|
57
|
+
* - Requests that are POST only because they carry a body: permission
|
|
58
|
+
* queries, previews, probes, focus and heartbeat signals, fetching
|
|
59
|
+
* remotes, accepting a review baseline. None of them changes content.
|
|
60
|
+
*/
|
|
61
|
+
const ALWAYS_WRITABLE: readonly RegExp[] = [
|
|
62
|
+
/^\/api\/auth\//,
|
|
63
|
+
/^\/api\/mcp(\/|$)/,
|
|
64
|
+
/^\/api\/agent\//,
|
|
65
|
+
/^\/api\/admin\/accounts(\/|$)/,
|
|
66
|
+
/^\/api\/admin\/connection-keys\//,
|
|
67
|
+
/^\/api\/audit\//,
|
|
68
|
+
/^\/api\/sync(\/|$)/,
|
|
69
|
+
/^\/api\/setup\//,
|
|
70
|
+
/^\/api\/secrets\//,
|
|
71
|
+
/^\/api\/admin\/github-facade\//,
|
|
72
|
+
/^\/api\/workspace\/[^/]+\/access\/batch$/,
|
|
73
|
+
/^\/api\/workspace\/[^/]+\/review\/accept$/,
|
|
74
|
+
/^\/api\/workspace\/[^/]+\/flush$/,
|
|
75
|
+
/^\/api\/workspace\/[^/]+\/workflow\/refresh-remotes$/,
|
|
76
|
+
/^\/api\/workspace\/[^/]+\/workflow\/locks\/heartbeat$/,
|
|
77
|
+
/^\/api\/events\/[^/]+\/focus$/,
|
|
78
|
+
/^\/api\/tools\/preview$/,
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Whether a request may go ahead while the deployment is read-only, without asking.
|
|
83
|
+
*
|
|
84
|
+
* Judged on the path in LOWER CASE, because that is how the router reads
|
|
85
|
+
* it: routes match whatever the letter case, so `/API/workspace/…` reaches
|
|
86
|
+
* the same handler as `/api/workspace/…`. Judged as written, it would be
|
|
87
|
+
* outside the gated namespace and walk past the gate.
|
|
88
|
+
*/
|
|
89
|
+
export function isAlwaysWritable(method: string, path: string, hostPaths: readonly string[] = []): boolean {
|
|
90
|
+
if (!MUTATING_METHODS.has(method.toUpperCase())) return true;
|
|
91
|
+
const asRouted = path.toLowerCase();
|
|
92
|
+
if (asRouted !== '/api' && !asRouted.startsWith('/api/')) return true;
|
|
93
|
+
return (
|
|
94
|
+
ALWAYS_WRITABLE.some((rule) => rule.test(asRouted)) ||
|
|
95
|
+
hostPaths.some((prefix) => asRouted.startsWith(prefix.toLowerCase()))
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Refuse every change to the deployment while the port says it is
|
|
101
|
+
* read-only, with a 403 the app recognises by {@link READ_ONLY_CODE}.
|
|
102
|
+
* Mounted ahead of every route under `/api`; a request the port is not
|
|
103
|
+
* asked about costs nothing. A port that fails is treated as writable —
|
|
104
|
+
* a billing lookup that is down must not freeze every workspace.
|
|
105
|
+
*/
|
|
106
|
+
export function createWriteGateMiddleware(writeAccess: IWriteAccess): RequestHandler {
|
|
107
|
+
if (writeAccess === alwaysWritable) return (_req, _res, next) => next();
|
|
108
|
+
return async (req, res, next) => {
|
|
109
|
+
if (isAlwaysWritable(req.method, req.path, writeAccess.alwaysWritablePaths)) return next();
|
|
110
|
+
let verdict: WriteAccessVerdict;
|
|
111
|
+
try {
|
|
112
|
+
verdict = await writeAccess.canWrite();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
log.warn('the write-access port failed; letting the request through:', { err });
|
|
115
|
+
return next();
|
|
116
|
+
}
|
|
117
|
+
if (verdict.ok) return next();
|
|
118
|
+
res.status(403).json({ error: verdict.message, code: READ_ONLY_CODE });
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The verdict for a write tool, the tool layer's half of the gate: `null`
|
|
124
|
+
* when it may run, the refusal otherwise. Same failure rule as the HTTP gate.
|
|
125
|
+
*/
|
|
126
|
+
export async function refuseWriteTool(writeAccess: IWriteAccess): Promise<string | null> {
|
|
127
|
+
if (writeAccess === alwaysWritable) return null;
|
|
128
|
+
try {
|
|
129
|
+
const verdict = await writeAccess.canWrite();
|
|
130
|
+
return verdict.ok ? null : verdict.message;
|
|
131
|
+
} catch (err) {
|
|
132
|
+
log.warn('the write-access port failed; letting the tool run:', { err });
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* GET /api/write-access — whether the deployment can be changed right now,
|
|
139
|
+
* and if not, the host's words for why. The app reads it to say so once, in
|
|
140
|
+
* a banner, instead of letting every save fail on its own.
|
|
141
|
+
*/
|
|
142
|
+
export function createWriteAccessRoutes(writeAccess: IWriteAccess): express.Router {
|
|
143
|
+
const router = express.Router();
|
|
144
|
+
router.get('/write-access', async (_req, res) => {
|
|
145
|
+
try {
|
|
146
|
+
const verdict = await writeAccess.canWrite();
|
|
147
|
+
res.json(verdict.ok ? { writable: true } : { writable: false, message: verdict.message });
|
|
148
|
+
} catch {
|
|
149
|
+
res.json({ writable: true });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return router;
|
|
153
|
+
}
|
|
@@ -167,6 +167,64 @@ export class PullRebaseConflictError extends WorkflowDomainError {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* The one sentence a branch-less call is answered with. Names the input and
|
|
172
|
+
* what to pass, and carries NO stringified value of what was actually
|
|
173
|
+
* received: the whole point of this refusal is that the caller sent nothing,
|
|
174
|
+
* and echoing `undefined` back at them is how the branch "undefined" got
|
|
175
|
+
* invented in the first place.
|
|
176
|
+
*/
|
|
177
|
+
export const BRANCH_REQUIRED_MESSAGE =
|
|
178
|
+
'`branch` is required: pass the branch (draft) you are working on.';
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The caller named no branch — the input is missing, empty, not a string, or
|
|
182
|
+
* one of the stringified absent values (`"undefined"` / `"null"`) that a
|
|
183
|
+
* client produces by interpolating a variable it never set.
|
|
184
|
+
*
|
|
185
|
+
* Refused, never defaulted. A knowledge-base tool's workspace is NEVER implied
|
|
186
|
+
* by the credential (identity-only) — it always comes from this argument — so
|
|
187
|
+
* falling back to the deployment's default branch would make an omitted
|
|
188
|
+
* argument silently act on the protected branch. And it is refused HERE, at
|
|
189
|
+
* the boundary, because everything downstream treats the value as a real
|
|
190
|
+
* branch name: `workspaceIdForBranch` would turn it into a workspace directory
|
|
191
|
+
* literally named `undefined`, and the clone of that "branch" would fail with
|
|
192
|
+
* a story about a branch that never existed.
|
|
193
|
+
*
|
|
194
|
+
* 400 with kind `branch-required`, so a client tells it apart from the 404
|
|
195
|
+
* (no such branch) and the 410 (the branch was deleted) without reading prose.
|
|
196
|
+
*/
|
|
197
|
+
export class BranchRequiredError extends WorkflowDomainError {
|
|
198
|
+
readonly kind = 'branch-required' as const;
|
|
199
|
+
constructor() {
|
|
200
|
+
super(BRANCH_REQUIRED_MESSAGE, 400, { kind: 'branch-required' });
|
|
201
|
+
this.name = 'BranchRequiredError';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The stringified absent values. Both are syntactically valid git branch
|
|
207
|
+
* names, so the shape validator (`assertValidBranchName`) accepts them
|
|
208
|
+
* happily — accepting one is exactly the bug this guard exists for. They are
|
|
209
|
+
* refused BY NAME, which is why this check cannot be folded into the shape
|
|
210
|
+
* check however tempting that looks.
|
|
211
|
+
*/
|
|
212
|
+
const STRINGIFIED_ABSENT_VALUES = new Set(['undefined', 'null']);
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Refuse a call that names no branch, before anything downstream can turn the
|
|
216
|
+
* missing value into a directory name, a clone attempt or a log line.
|
|
217
|
+
*
|
|
218
|
+
* Deliberately NOT a shape check: a well-formed name this platform has never
|
|
219
|
+
* seen is a 404 the caller can act on, and a malformed one is a
|
|
220
|
+
* `BranchNameError` from the canonical validator. This answers only "you sent
|
|
221
|
+
* nothing", which is the one case where naming the branch back is impossible.
|
|
222
|
+
*/
|
|
223
|
+
export function assertBranchProvided(branch: unknown): asserts branch is string {
|
|
224
|
+
if (typeof branch !== 'string' || branch.length === 0) throw new BranchRequiredError();
|
|
225
|
+
if (STRINGIFIED_ABSENT_VALUES.has(branch)) throw new BranchRequiredError();
|
|
226
|
+
}
|
|
227
|
+
|
|
170
228
|
/**
|
|
171
229
|
* The platform has never heard of this branch: nothing it has cloned, and
|
|
172
230
|
* nothing any listing of origin's branches has mentioned. Distinct from
|
|
@@ -319,6 +377,37 @@ export class PathNotFoundError extends WorkflowDomainError {
|
|
|
319
377
|
}
|
|
320
378
|
}
|
|
321
379
|
|
|
380
|
+
/**
|
|
381
|
+
* The one sentence every read of a past save answers when that save is not in
|
|
382
|
+
* the history of the branch being viewed.
|
|
383
|
+
*
|
|
384
|
+
* Written once, here, because four surfaces have to give the same answer: the
|
|
385
|
+
* bytes route (`?ref=`), the patch route (`show-file`), the before/after
|
|
386
|
+
* contents route (`file-at-change`), and anything later that serves a file at
|
|
387
|
+
* a commit. A save the history panel listed came off `git log` on this
|
|
388
|
+
* workspace's own branch, so it can never be refused this way — what this
|
|
389
|
+
* refuses is a sha from someone else's branch, or one that was invented.
|
|
390
|
+
*/
|
|
391
|
+
export const VERSION_NOT_ON_BRANCH_MESSAGE =
|
|
392
|
+
"This version is not in this file's history on this branch.";
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The caller named a save that is not an ancestor of the branch this
|
|
396
|
+
* workspace has checked out.
|
|
397
|
+
*
|
|
398
|
+
* 404, and DELIBERATELY the same 404 for a sha that exists on another branch
|
|
399
|
+
* as for one that exists nowhere: telling the two apart would turn the route
|
|
400
|
+
* into an oracle for "does this commit exist in the repository", which is
|
|
401
|
+
* exactly the reading a branch-scoped rule exists to prevent.
|
|
402
|
+
*/
|
|
403
|
+
export class VersionNotOnBranchError extends WorkflowDomainError {
|
|
404
|
+
readonly kind = 'version-not-on-branch' as const;
|
|
405
|
+
constructor() {
|
|
406
|
+
super(VERSION_NOT_ON_BRANCH_MESSAGE, 404, { kind: 'version-not-on-branch' });
|
|
407
|
+
this.name = 'VersionNotOnBranchError';
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
322
411
|
/**
|
|
323
412
|
* An archive the caller uploaded that the zip reader cannot open. 422, not
|
|
324
413
|
* 400: the request is well-formed and the path is fine — the BYTES are not a
|
|
@@ -62,6 +62,16 @@ export interface GitCredentials {
|
|
|
62
62
|
username(): string;
|
|
63
63
|
/** The token in effect, or null when the deployment has none configured. */
|
|
64
64
|
token(): string | null;
|
|
65
|
+
/**
|
|
66
|
+
* Make sure {@link token} answers with a credential that is still good,
|
|
67
|
+
* for one that is not fixed: a token a host issues for an hour has to be
|
|
68
|
+
* renewed by asking the host, which `token` cannot do, being read where
|
|
69
|
+
* nothing can wait. The runner awaits this before every call it makes. A
|
|
70
|
+
* failure here is not the call's failure: git runs with the token in
|
|
71
|
+
* hand, and the host says what it thinks of it. Absent for a credential
|
|
72
|
+
* that does not expire.
|
|
73
|
+
*/
|
|
74
|
+
prepare?(): Promise<void>;
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
/** The username git is given when a deployment names none. */
|
|
@@ -199,6 +209,33 @@ export class GitRunError extends Error {
|
|
|
199
209
|
}
|
|
200
210
|
}
|
|
201
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Refuse a `cat-file --batch` spec that is not ONE line.
|
|
214
|
+
*
|
|
215
|
+
* `--batch` reads one object name per LINE, so a spec carrying a `\n` is two
|
|
216
|
+
* requests. For a single-spec read that means git answers about the part
|
|
217
|
+
* before the break — bytes the caller then presents as the whole path's,
|
|
218
|
+
* having had its access gates answered about a string that names nothing. For
|
|
219
|
+
* a batch it is worse: the extra answer shifts every result after it, and a
|
|
220
|
+
* walk that steps one header at a time hands each caller the object that
|
|
221
|
+
* belonged to the next one.
|
|
222
|
+
*
|
|
223
|
+
* A path is refused a control character long before this — by
|
|
224
|
+
* `assertValidRelativePath` and by `normalizeWorkspacePath`, which is where
|
|
225
|
+
* the rule belongs because it protects every route and tool at once. This is
|
|
226
|
+
* the fence AT the protocol, for a spec assembled from somewhere neither of
|
|
227
|
+
* those covers (a ref, a branch name, a path from a future caller). Nothing
|
|
228
|
+
* should ever trip it, so it throws rather than filtering: a request that
|
|
229
|
+
* cannot be expressed in this protocol has no correct truncation.
|
|
230
|
+
*/
|
|
231
|
+
export function assertOneSpecPerLine(specs: readonly string[]): void {
|
|
232
|
+
for (const spec of specs) {
|
|
233
|
+
if (/[\r\n]/.test(spec)) {
|
|
234
|
+
throw new Error('git cat-file --batch spec contains a line break');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
202
239
|
/** Whether a failure came from the deadline rather than from git's own answer. */
|
|
203
240
|
export function isGitTimeout(err: unknown): boolean {
|
|
204
241
|
return err instanceof GitRunError && err.timedOut;
|
|
@@ -12,7 +12,6 @@ const settings: TenantHostSettings = {
|
|
|
12
12
|
workspacesRoot: path.resolve('/srv/hexis/workspaces'),
|
|
13
13
|
kbTemplateDir: '/srv/hexis/template',
|
|
14
14
|
gitTimeoutMs: 120_000,
|
|
15
|
-
ontologySessionBlock: true,
|
|
16
15
|
updateCheckEnabled: false,
|
|
17
16
|
masterKey: 'a-master-key-that-is-long-enough-to-count-as-random',
|
|
18
17
|
};
|
|
@@ -23,7 +23,6 @@ export interface TenantHostSettings {
|
|
|
23
23
|
readonly workspacesRoot: string;
|
|
24
24
|
readonly kbTemplateDir: string;
|
|
25
25
|
readonly gitTimeoutMs: number;
|
|
26
|
-
readonly ontologySessionBlock: boolean;
|
|
27
26
|
readonly updateCheckEnabled: boolean;
|
|
28
27
|
/** See `tenant-secrets.ts`. */
|
|
29
28
|
readonly masterKey: string;
|
|
@@ -182,7 +181,6 @@ export function tenantConfigFrom(record: TenantRecord, settings: TenantHostSetti
|
|
|
182
181
|
gitUsername,
|
|
183
182
|
gitToken: (record.gitToken ?? '').trim(),
|
|
184
183
|
kbTemplateDir: settings.kbTemplateDir,
|
|
185
|
-
ontologySessionBlock: settings.ontologySessionBlock,
|
|
186
184
|
updateCheckEnabled: settings.updateCheckEnabled,
|
|
187
185
|
allowedEmailDomains,
|
|
188
186
|
trustProxy: settings.trustProxy,
|
|
@@ -289,7 +287,6 @@ export function tenantHostEnv(env: NodeJS.ProcessEnv = process.env): TenantHostE
|
|
|
289
287
|
kbTemplateDir: env.KB_TEMPLATE_DIR || defaultKbTemplateDir(),
|
|
290
288
|
gitTimeoutMs:
|
|
291
289
|
Number.isFinite(gitTimeout) && gitTimeout > 0 && gitTimeout <= MAX_TIMER_MS ? gitTimeout : DEFAULT_GIT_TIMEOUT_MS,
|
|
292
|
-
ontologySessionBlock: (env.ONTOLOGY_SESSION_BLOCK ?? 'true').trim().toLowerCase() !== 'false',
|
|
293
290
|
updateCheckEnabled: (env.UPDATE_CHECK ?? 'true').trim().toLowerCase() !== 'false',
|
|
294
291
|
idleMinutes: Number.isFinite(idle) && idle > 0 ? idle : 30,
|
|
295
292
|
};
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ontology-session boundary — the PURE decision core (no IO, no DB).
|
|
3
|
-
*
|
|
4
|
-
* Model: each run/session accumulates the SET of named ontologies it has
|
|
5
|
-
* touched (via any read or write). The rule is asymmetric:
|
|
6
|
-
*
|
|
7
|
-
* READS — always allowed. A read that touches a named ontology RECORDS it
|
|
8
|
-
* into the session's touched set (so it can poison later writes).
|
|
9
|
-
* WRITES — allowed only while the session is confined to a SINGLE named
|
|
10
|
-
* ontology and the write targets that same ontology. The moment a
|
|
11
|
-
* session has touched two or more named ontologies (typically by
|
|
12
|
-
* reading across them), EVERY subsequent write is blocked — "read
|
|
13
|
-
* across ontologies → you can't write anymore at all."
|
|
14
|
-
*
|
|
15
|
-
* Neutral touches (`ontologyOf` → null: root config, Skills, root-level
|
|
16
|
-
* Knowledge, non-KB) never record and never block. See
|
|
17
|
-
* `docs/rp4-ontology-boundary-plan-v2.md`.
|
|
18
|
-
*
|
|
19
|
-
* `SessionOntologyService` (Postgres-backed) persists the touched set and calls
|
|
20
|
-
* these helpers; this module keeps the rule itself testable in isolation.
|
|
21
|
-
*/
|
|
22
|
-
import type { Ontology } from '@bevel-software/platform-shared';
|
|
23
|
-
/** Outcome of a write check against the session's touched-ontology set. */
|
|
24
|
-
export type WriteDecision = {
|
|
25
|
-
allow: true;
|
|
26
|
-
} | {
|
|
27
|
-
allow: false;
|
|
28
|
-
reason: 'multi-ontology' | 'different-ontology';
|
|
29
|
-
touched: string[];
|
|
30
|
-
attempted: string;
|
|
31
|
-
};
|
|
32
|
-
/**
|
|
33
|
-
* The set of NAMED ontologies a session has touched after this operation. A
|
|
34
|
-
* neutral operation (`ontology === null`) adds nothing. Pure — returns a new
|
|
35
|
-
* set, never mutates the input.
|
|
36
|
-
*/
|
|
37
|
-
export declare function recordTouch(touched: ReadonlySet<string>, ontology: Ontology): Set<string>;
|
|
38
|
-
/**
|
|
39
|
-
* Decide whether a WRITE to `attempted` is allowed given the ontologies the
|
|
40
|
-
* session has already touched (NOT counting this write yet).
|
|
41
|
-
*
|
|
42
|
-
* - A write to a neutral path (`attempted === null`) is always allowed.
|
|
43
|
-
* - If the session has already touched two or more named ontologies, the
|
|
44
|
-
* write is blocked (`multi-ontology`) — reading across ontologies poisoned
|
|
45
|
-
* the session's write ability.
|
|
46
|
-
* - If the session has touched exactly one named ontology and it differs from
|
|
47
|
-
* the write target, the write is blocked (`different-ontology`).
|
|
48
|
-
* - If the session is unpinned (touched none) or its single touched ontology
|
|
49
|
-
* equals the target, the write is allowed.
|
|
50
|
-
*/
|
|
51
|
-
export declare function decideWrite(touched: ReadonlySet<string>, attempted: Ontology): WriteDecision;
|
|
52
|
-
//# sourceMappingURL=session-ontology.policy.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"session-ontology.policy.d.ts","sourceRoot":"","sources":["../../../src/modules/workflow/session-ontology.policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAEhE,2EAA2E;AAC3E,MAAM,MAAM,aAAa,GACrB;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GACf;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,gBAAgB,GAAG,oBAAoB,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5G;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,CAIzF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,QAAQ,GAAG,aAAa,CAgB5F"}
|