@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
|
@@ -16,8 +16,11 @@ import {
|
|
|
16
16
|
failureOf,
|
|
17
17
|
gitFailure,
|
|
18
18
|
type GitFailure,
|
|
19
|
+
type GitFailureKind,
|
|
19
20
|
} from '../../../shared/git-failure.js';
|
|
20
21
|
import { redactSecret, urlQuerySecrets } from '../../../shared/redact-secret.js';
|
|
22
|
+
import { normalizeRepositoryAddress, sameRepository } from '../../kb-fs/remote-url.js';
|
|
23
|
+
import { setAsideClone, setAsideRootFor, setAsideStamp } from '../set-aside-clone.js';
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* The KB startup phase: run every registered {@link OnServerStart} step, in
|
|
@@ -28,7 +31,9 @@ import { redactSecret, urlQuerySecrets } from '../../../shared/redact-secret.js'
|
|
|
28
31
|
*
|
|
29
32
|
* Fully fail-closed: any failure this phase cannot DECLARE (an unhandled
|
|
30
33
|
* step throw, an unreachable remote, a clone that will not come down, a
|
|
31
|
-
* refused write) throws out of `runAll
|
|
34
|
+
* refused write) throws out of `runAll`. The boot stops on it unless the
|
|
35
|
+
* host ANSWERED about the repository or the credentials, in which case it
|
|
36
|
+
* comes up gated instead — see `bootMaySurvive`. The container's
|
|
32
37
|
* restart policy is the retry — each attempt at boot time on quiet trees —
|
|
33
38
|
* so an environmental failure converges without a human the moment the
|
|
34
39
|
* environment returns. The one carve-out: a push rejected because a
|
|
@@ -51,6 +56,15 @@ export interface KbStartupRunnerOptions {
|
|
|
51
56
|
gitRunner: IGitRunner;
|
|
52
57
|
kbRepoUrl: () => string;
|
|
53
58
|
workspacesRoot: string;
|
|
59
|
+
/**
|
|
60
|
+
* Where a working copy of a repository that is no longer the configured one
|
|
61
|
+
* is moved to, under a folder named for the moment it happened — see
|
|
62
|
+
* `reconcileClonesWithConfiguredRepository`. THE SAME VALUE THE WORKSPACE
|
|
63
|
+
* SERVICE IS GIVEN.
|
|
64
|
+
*
|
|
65
|
+
* Why, in one place: `setAsideRootFor` in `set-aside-clone.ts`.
|
|
66
|
+
*/
|
|
67
|
+
setAsideRoot?: string;
|
|
54
68
|
kbDirName: string;
|
|
55
69
|
templateDir: string;
|
|
56
70
|
defaultBranch: () => string;
|
|
@@ -67,6 +81,47 @@ export interface KbStartupRunnerOptions {
|
|
|
67
81
|
template `.gitignore` rule can never silently drop a required seed file
|
|
68
82
|
from the commit. */
|
|
69
83
|
buildSeedTree: (dir: string) => Promise<string[]>;
|
|
84
|
+
/**
|
|
85
|
+
* Called with the workspace id of each working copy the phase is ABOUT TO
|
|
86
|
+
* set aside for being a clone of another repository, and awaited first.
|
|
87
|
+
* Whatever is still queued to be committed into that copy was written
|
|
88
|
+
* against the repository that was left, so it must be held back before a
|
|
89
|
+
* fresh clone of another repository appears at the same path.
|
|
90
|
+
*
|
|
91
|
+
* A PRECONDITION, so a failure here STOPS THE PHASE with the copy still
|
|
92
|
+
* where it was. Carrying on would clone the replacement with the queue
|
|
93
|
+
* untouched, and the worker would then commit those bytes into a
|
|
94
|
+
* repository they were never meant for. Stopped before anything moved, the
|
|
95
|
+
* next run finds the same copy and asks again. Optional: a minimal graph
|
|
96
|
+
* has no queue.
|
|
97
|
+
*/
|
|
98
|
+
beforeCloneSetAside?: (workspaceId: string) => Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Called with the workspace id of each working copy the phase SET ASIDE for
|
|
101
|
+
* being a clone of another repository. The workspace service keeps a
|
|
102
|
+
* branch→directory cache and adopts whatever is on disk, so on a RUNNING
|
|
103
|
+
* server (the save that moves the deployment to another repository) the
|
|
104
|
+
* copy would otherwise stay in that cache as a path to nothing. Optional:
|
|
105
|
+
* at boot nothing has been cached yet.
|
|
106
|
+
*
|
|
107
|
+
* Never throws into the phase: a listener that fails must not stop a boot.
|
|
108
|
+
*/
|
|
109
|
+
onCloneDiscarded?: (workspaceId: string) => void | Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Called with the workspace id of each working copy the phase CLONED fresh.
|
|
112
|
+
* A clone has just downloaded every ref, which is a successful fetch by any
|
|
113
|
+
* measure — but the git layer keeps its own per-workspace record of the last
|
|
114
|
+
* fetch and whether it worked, and the one that drove the replacement is a
|
|
115
|
+
* FAILED fetch of the repository that is gone. Left standing, a strict
|
|
116
|
+
* branch listing inside that record's TTL refuses the new clone's refs as
|
|
117
|
+
* unproven, having never touched it.
|
|
118
|
+
*
|
|
119
|
+
* The ordinary path already says this (`WorkspaceService`'s cloned
|
|
120
|
+
* listener); the phase clones with git directly, so it has to say it itself.
|
|
121
|
+
*
|
|
122
|
+
* Never throws into the phase, for the same reason as above.
|
|
123
|
+
*/
|
|
124
|
+
onCloneCreated?: (workspaceId: string) => void;
|
|
70
125
|
}
|
|
71
126
|
|
|
72
127
|
/**
|
|
@@ -97,6 +152,42 @@ export class KbRemoteUnreachableError extends ClassifiedFailure {
|
|
|
97
152
|
}
|
|
98
153
|
}
|
|
99
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The failure kinds a boot may survive GATED rather than stop on: the remote
|
|
157
|
+
* ANSWERED (or could not be reached at all), and what it said is about the
|
|
158
|
+
* repository or the credentials — not about what this deployment would write.
|
|
159
|
+
*
|
|
160
|
+
* All four are fixed by an operator somewhere other than this process: the
|
|
161
|
+
* host comes back, the repository is created or the token is granted access
|
|
162
|
+
* to it, a rotated token is entered on the setup screen. Refusing to boot
|
|
163
|
+
* over any of them takes away the very screen the fix is entered on — which
|
|
164
|
+
* is what happened on 2026-09-28, when a replaced repository answered
|
|
165
|
+
* `not found` inside a step and the container crash-looped.
|
|
166
|
+
*
|
|
167
|
+
* Every OTHER failure still stops the boot: it means a step or the template
|
|
168
|
+
* would write something wrong, and booting over that is worse than not
|
|
169
|
+
* booting at all.
|
|
170
|
+
*/
|
|
171
|
+
const GATED_BOOT_KINDS: ReadonlySet<GitFailureKind> = new Set([
|
|
172
|
+
'unreachable',
|
|
173
|
+
'not-found',
|
|
174
|
+
'credentials-rejected',
|
|
175
|
+
'write-refused',
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Whether the boot may come up GATED on this failure instead of stopping.
|
|
180
|
+
*
|
|
181
|
+
* {@link KbRemoteUnreachableError} says so by its type — it is raised where
|
|
182
|
+
* the remote is first contacted, before any step. A failure raised INSIDE the
|
|
183
|
+
* phase says so by its classification: the same fetch that stops a boot when
|
|
184
|
+
* it is the first contact must not stop one when a step happened to make it.
|
|
185
|
+
*/
|
|
186
|
+
export function bootMaySurvive(err: unknown): boolean {
|
|
187
|
+
if (err instanceof KbRemoteUnreachableError) return true;
|
|
188
|
+
return err instanceof ClassifiedFailure && GATED_BOOT_KINDS.has(err.failure.kind);
|
|
189
|
+
}
|
|
190
|
+
|
|
100
191
|
export interface RetryOptions {
|
|
101
192
|
/** First wait before trying again. Default 30s. */
|
|
102
193
|
initialDelayMs?: number;
|
|
@@ -241,7 +332,7 @@ export class KbStartupRunner {
|
|
|
241
332
|
// Stopped by either: a failure that is no longer the remote at all
|
|
242
333
|
// (the knowledge base itself is wrong — asking again will not change
|
|
243
334
|
// it), or a remote answer that a retry cannot change (see above).
|
|
244
|
-
if (!(err
|
|
335
|
+
if (!bootMaySurvive(err) || !worthRetrying()) return stopOnStanding();
|
|
245
336
|
delay = Math.min(delay * 2, max);
|
|
246
337
|
log(`remote still unreachable; trying again in ${Math.round(delay / 1000)}s`);
|
|
247
338
|
}
|
|
@@ -301,6 +392,10 @@ export class KbStartupRunner {
|
|
|
301
392
|
);
|
|
302
393
|
}
|
|
303
394
|
const heads = await this.ensureRemote();
|
|
395
|
+
// AFTER the remote answered: the configured address is known to be a
|
|
396
|
+
// repository we can reach, so a clone of a different one is stale, not
|
|
397
|
+
// a casualty of a typo in the address.
|
|
398
|
+
await this.reconcileClonesWithConfiguredRepository();
|
|
304
399
|
const ctx = this.buildContext(heads, handles);
|
|
305
400
|
|
|
306
401
|
for (const step of this.opts.steps) {
|
|
@@ -501,6 +596,141 @@ export class KbStartupRunner {
|
|
|
501
596
|
};
|
|
502
597
|
}
|
|
503
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Bring every working copy on disk into line with the configured
|
|
601
|
+
* repository, WITHOUT DELETING ANYTHING.
|
|
602
|
+
*
|
|
603
|
+
* A clone fetches and pushes through the address stored in its own
|
|
604
|
+
* `remote.origin.url`, which nothing updates when the configured address
|
|
605
|
+
* changes. After an operator replaced the repository (2026-09-28: the old
|
|
606
|
+
* one deleted, a new one saved on the setup screen) every surviving clone
|
|
607
|
+
* kept asking the old address: Save and Retry failed, and the restart
|
|
608
|
+
* stopped the boot on `repository … not found`, taking the setup screen
|
|
609
|
+
* with it.
|
|
610
|
+
*
|
|
611
|
+
* A clone whose address differs from the configured one is one of two
|
|
612
|
+
* things, and the histories say which:
|
|
613
|
+
*
|
|
614
|
+
* - THE SAME REPOSITORY AT A NEW ADDRESS — moved, renamed, mirrored. The
|
|
615
|
+
* one change the setup screen tells an admin to make ("only change this
|
|
616
|
+
* if the same repository was moved or renamed"). Its history is on the
|
|
617
|
+
* configured remote, so the clone is pointed at the new address and
|
|
618
|
+
* kept, unpushed commits included: they are pushed where they belonged
|
|
619
|
+
* all along.
|
|
620
|
+
* - ANOTHER REPOSITORY. Re-pointing it would push one repository's
|
|
621
|
+
* commits into the other, so it cannot stay where the workspace service
|
|
622
|
+
* would adopt it. It is MOVED to {@link KbStartupRunnerOptions.setAsideRoot}
|
|
623
|
+
* and the log says where. Never removed: when the old repository is gone
|
|
624
|
+
* — as it was that day — these clones are the last copy of the
|
|
625
|
+
* knowledge base that exists, and the unpushed work in them exists
|
|
626
|
+
* nowhere else at all.
|
|
627
|
+
*
|
|
628
|
+
* Every clone under the workspaces root, not only the branches this phase
|
|
629
|
+
* touches: the workspace service adopts whatever it finds there. A clone
|
|
630
|
+
* whose address cannot be read is left where it is — "could not tell" is
|
|
631
|
+
* no reason to move someone's work. One that cannot be set aside stops the
|
|
632
|
+
* boot with the reason: leaving it would fail the boot a step later with
|
|
633
|
+
* git's words about a repository nobody configured.
|
|
634
|
+
*/
|
|
635
|
+
private async reconcileClonesWithConfiguredRepository(): Promise<void> {
|
|
636
|
+
const configured = this.opts.kbRepoUrl();
|
|
637
|
+
let entries: import('node:fs').Dirent[];
|
|
638
|
+
try {
|
|
639
|
+
entries = await fs.readdir(this.opts.workspacesRoot, { withFileTypes: true });
|
|
640
|
+
} catch {
|
|
641
|
+
return; // no workspaces yet
|
|
642
|
+
}
|
|
643
|
+
// One folder per run, so what was set aside together stays together.
|
|
644
|
+
const stamp = setAsideStamp();
|
|
645
|
+
const setAsideRoot = setAsideRootFor(this.opts.workspacesRoot, this.opts.setAsideRoot);
|
|
646
|
+
for (const entry of entries) {
|
|
647
|
+
if (!entry.isDirectory()) continue;
|
|
648
|
+
const repoDir = path.join(this.opts.workspacesRoot, entry.name, this.opts.kbDirName);
|
|
649
|
+
const hasGit = await fs.access(path.join(repoDir, '.git')).then(() => true, () => false);
|
|
650
|
+
if (!hasGit) continue;
|
|
651
|
+
let origin: string;
|
|
652
|
+
try {
|
|
653
|
+
origin = (await git(this.opts.gitRunner, repoDir, ['config', '--get', 'remote.origin.url'])).trim();
|
|
654
|
+
} catch {
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (origin === '' || sameRepository(origin, configured)) continue;
|
|
658
|
+
// The normalized form carries no userinfo, and the scrub covers the rest.
|
|
659
|
+
const was = this.redact(normalizeRepositoryAddress(origin));
|
|
660
|
+
if (await this.sharesHistoryWithConfigured(repoDir, configured)) {
|
|
661
|
+
await git(this.opts.gitRunner, repoDir, ['remote', 'set-url', 'origin', configured]);
|
|
662
|
+
startupLog.warn(
|
|
663
|
+
`working copy "${entry.name}" was cloned from ${was}, whose history the configured repository holds: ` +
|
|
664
|
+
'the same repository at a new address. Pointed at the configured address and kept, unpushed work included.',
|
|
665
|
+
);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
const kept = path.join(setAsideRoot, stamp, entry.name);
|
|
669
|
+
// First, and allowed to stop the phase: see `beforeCloneSetAside`. The
|
|
670
|
+
// directory name IS the workspace id (`workspaceIdForBranch`).
|
|
671
|
+
await this.opts.beforeCloneSetAside?.(entry.name);
|
|
672
|
+
await setAsideClone(repoDir, kept);
|
|
673
|
+
startupLog.warn(
|
|
674
|
+
`working copy "${entry.name}" is a clone of another repository (${was}). Set aside at ${kept}; nothing was ` +
|
|
675
|
+
'deleted, and it will be cloned fresh from the configured one. Work that was never pushed is in that ' +
|
|
676
|
+
'folder: `git log` there shows it, and `git push <address> <branch>` from there sends it to a repository.',
|
|
677
|
+
);
|
|
678
|
+
// Setting it aside already removed it from the workspaces root, so the
|
|
679
|
+
// cached handle the workspace service holds for it now points at
|
|
680
|
+
// nothing.
|
|
681
|
+
try {
|
|
682
|
+
await this.opts.onCloneDiscarded?.(entry.name);
|
|
683
|
+
} catch (err) {
|
|
684
|
+
startupLog.warn(`could not finish setting the working copy "${entry.name}" aside:`, {
|
|
685
|
+
detail: this.redact(err instanceof Error ? err.message : String(err)),
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Whether the configured repository holds history this clone has too —
|
|
693
|
+
* asked of git, not guessed from the two addresses. The configured
|
|
694
|
+
* remote's branches are fetched under a namespace of their own and the
|
|
695
|
+
* clone's HEAD is tested for a common ancestor with any of them.
|
|
696
|
+
*
|
|
697
|
+
* "Could not tell" answers false: a fetch that fails, a clone with no
|
|
698
|
+
* commit to compare. False sets the clone aside, which loses nothing, where
|
|
699
|
+
* a wrong true would push a stranger's commits into the configured
|
|
700
|
+
* repository.
|
|
701
|
+
*/
|
|
702
|
+
private async sharesHistoryWithConfigured(repoDir: string, configured: string): Promise<boolean> {
|
|
703
|
+
const probe = 'refs/hexis-probe';
|
|
704
|
+
let theirs: string[] = [];
|
|
705
|
+
try {
|
|
706
|
+
await git(this.opts.gitRunner, repoDir, ['fetch', '--no-tags', '--quiet', configured, `+refs/heads/*:${probe}/*`]);
|
|
707
|
+
theirs = (await git(this.opts.gitRunner, repoDir, ['for-each-ref', '--format=%(refname)', `${probe}/`]))
|
|
708
|
+
.split('\n')
|
|
709
|
+
.map((ref) => ref.trim())
|
|
710
|
+
.filter(Boolean);
|
|
711
|
+
if (theirs.length === 0) return false;
|
|
712
|
+
// One question for all of them: a common ancestor of HEAD and ANY of
|
|
713
|
+
// their branches is a common ancestor of HEAD and their merge. Capped,
|
|
714
|
+
// so a remote with thousands of branches cannot outgrow a command line.
|
|
715
|
+
await git(this.opts.gitRunner, repoDir, ['merge-base', 'HEAD', ...theirs.slice(0, 200)]);
|
|
716
|
+
return true;
|
|
717
|
+
} catch {
|
|
718
|
+
return false;
|
|
719
|
+
} finally {
|
|
720
|
+
// The probe leaves nothing behind in a clone that stays in use.
|
|
721
|
+
for (const ref of theirs) {
|
|
722
|
+
await git(this.opts.gitRunner, repoDir, ['update-ref', '-d', ref]).catch(() => undefined);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Move a working copy out of the workspaces root, whole. A rename where
|
|
729
|
+
* the two folders share a volume; a copy and then a removal where they do
|
|
730
|
+
* not (the backups root is a volume of its own in the shipped compose
|
|
731
|
+
* files), with the original removed only once the copy is complete.
|
|
732
|
+
*/
|
|
733
|
+
|
|
504
734
|
/**
|
|
505
735
|
* The branch's working copy at the runtime layout
|
|
506
736
|
* (`<workspacesRoot>/<id>/<kbDirName>`), so the workspace service finds it
|
|
@@ -518,6 +748,16 @@ export class KbStartupRunner {
|
|
|
518
748
|
await fs.mkdir(workspaceDir, { recursive: true });
|
|
519
749
|
await fs.rm(repoDir, { recursive: true, force: true });
|
|
520
750
|
await git(this.opts.gitRunner, workspaceDir, ['clone', '-b', branch, this.opts.kbRepoUrl(), repoDir]);
|
|
751
|
+
// Said as soon as the clone is there, before its configuration: if a
|
|
752
|
+
// command below fails, the clone stays on disk, and the retry finds it
|
|
753
|
+
// and never comes back through here.
|
|
754
|
+
try {
|
|
755
|
+
this.opts.onCloneCreated?.(workspaceIdForBranch(branch));
|
|
756
|
+
} catch (err) {
|
|
757
|
+
startupLog.warn(`could not announce the fresh working copy for "${branch}":`, {
|
|
758
|
+
detail: this.redact(err instanceof Error ? err.message : String(err)),
|
|
759
|
+
});
|
|
760
|
+
}
|
|
521
761
|
await git(this.opts.gitRunner, repoDir, ['config', 'core.longpaths', 'true']);
|
|
522
762
|
await stampIdentity(this.opts.gitRunner, repoDir);
|
|
523
763
|
return repoDir;
|
|
@@ -739,7 +739,7 @@ export function createWorkspaceRoutes(
|
|
|
739
739
|
}
|
|
740
740
|
|
|
741
741
|
/**
|
|
742
|
-
* GET /workspace/:id/file/raw?path=<file>[&download=1][&v=<n>]
|
|
742
|
+
* GET /workspace/:id/file/raw?path=<file>[&download=1][&v=<n>][&ref=<sha>[&side=before]]
|
|
743
743
|
*
|
|
744
744
|
* The bytes of one workspace file: for the document renderers' fetches, the
|
|
745
745
|
* file tree's Download, and the `<img>` tags the markdown pipeline emits for
|
|
@@ -750,7 +750,8 @@ export function createWorkspaceRoutes(
|
|
|
750
750
|
* request ──▶ auth: Bearer, else the cookie
|
|
751
751
|
* ──▶ read gate on the path 403 if the caller may not read it
|
|
752
752
|
* ──▶ download gate, if ?download=1 403 without the download: verb
|
|
753
|
-
* ──▶ readFileBinary
|
|
753
|
+
* ──▶ readFileBinary, or the bytes at 404 missing, 403 traversal
|
|
754
|
+
* ?ref when one is named
|
|
754
755
|
* ──▶ Content-Type from the extension, nosniff, a CSP sandbox for
|
|
755
756
|
* inline svg, attachment disposition for a download, and
|
|
756
757
|
* Cache-Control: private, no-cache
|
|
@@ -760,6 +761,30 @@ export function createWorkspaceRoutes(
|
|
|
760
761
|
*
|
|
761
762
|
* `?v=` is not read here. The frontend bumps it when it learns an image
|
|
762
763
|
* changed, so the browser asks for a URL it has not cached.
|
|
764
|
+
*
|
|
765
|
+
* `?ref=<sha>` serves the file AS IT WAS at that save instead of the working
|
|
766
|
+
* tree, so Version history can mount the very viewers this route already
|
|
767
|
+
* feeds. `&side=before` reads `<sha>^` — the version just before a save,
|
|
768
|
+
* which is the only thing the save that DELETED a file can show. Everything
|
|
769
|
+
* around the read is deliberately unchanged: the same read gate, the same
|
|
770
|
+
* download verb, the same content type, the same svg sandbox. What the ref
|
|
771
|
+
* changes is WHICH bytes, never WHO may have them:
|
|
772
|
+
*
|
|
773
|
+
* - both gates resolve against TODAY's tree, not the rules as they were at
|
|
774
|
+
* that save (hx-history-file-preview, decision 3): anyone who may read
|
|
775
|
+
* the file now may read any of its past saves, and anyone who may not is
|
|
776
|
+
* refused with the same sentence they get for the file itself;
|
|
777
|
+
* - the save must be in the history of the branch this workspace has
|
|
778
|
+
* checked out — `fileBytesAtChange` refuses anything else with a 404
|
|
779
|
+
* carrying `VERSION_NOT_ON_BRANCH_MESSAGE`;
|
|
780
|
+
* - a ref that is present but malformed is a 400, never a silent fall back
|
|
781
|
+
* to `readFileBinary`. Serving today's bytes for a request that asked for
|
|
782
|
+
* a past version would be the worst possible answer — it looks like a
|
|
783
|
+
* success — so the ref branch and the working-tree branch are exclusive;
|
|
784
|
+
* - and a read that fails for OUR reasons (a blob over the git runner's
|
|
785
|
+
* output ceiling, a timed-out `cat-file`) is a logged 500, not the
|
|
786
|
+
* blanket 404 the working-tree read answers with: "not found" about a
|
|
787
|
+
* save the reader can see listed is a lie about their own history.
|
|
763
788
|
*/
|
|
764
789
|
router.get('/workspace/:id/file/raw', async (req, res) => {
|
|
765
790
|
const id = authenticated(req, res);
|
|
@@ -771,6 +796,27 @@ export function createWorkspaceRoutes(
|
|
|
771
796
|
}
|
|
772
797
|
const filePath = inRepo(res, requested);
|
|
773
798
|
if (filePath === null) return;
|
|
799
|
+
// A past save, asked for by sha. Validated BEFORE the gates so a
|
|
800
|
+
// mistyped ref is a 400 rather than a 200 carrying today's bytes.
|
|
801
|
+
const rawRef = req.query.ref;
|
|
802
|
+
if (rawRef !== undefined && (typeof rawRef !== 'string' || !/^[a-f0-9]{7,40}$/i.test(rawRef))) {
|
|
803
|
+
res.status(400).json({ error: 'ref must be a commit sha' });
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const ref = typeof rawRef === 'string' ? rawRef : null;
|
|
807
|
+
// Only the two sides exist; `side` without `ref` names nothing, and an
|
|
808
|
+
// unrecognised value is a spelling mistake in a security-relevant
|
|
809
|
+
// parameter, not a default to guess at.
|
|
810
|
+
const rawSide = req.query.side;
|
|
811
|
+
if (rawSide !== undefined && rawSide !== 'before' && rawSide !== 'after') {
|
|
812
|
+
res.status(400).json({ error: "side must be 'before' or 'after'" });
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (rawSide !== undefined && ref === null) {
|
|
816
|
+
res.status(400).json({ error: 'side requires ref' });
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const side = rawSide === 'before' ? 'before' : 'after';
|
|
774
820
|
// `?download=1` flips this from inline-serve (used by PdfRenderer and
|
|
775
821
|
// the image renderers) to "save to disk" — and the save path is gated
|
|
776
822
|
// on per-path `download:` rules in access.md. The inline path stays
|
|
@@ -785,7 +831,28 @@ export function createWorkspaceRoutes(
|
|
|
785
831
|
if (!(await requireDownloadPermission(req, res, id, filePath))) return;
|
|
786
832
|
}
|
|
787
833
|
try {
|
|
788
|
-
|
|
834
|
+
let buffer: Buffer;
|
|
835
|
+
if (ref === null) {
|
|
836
|
+
buffer = await workspaceService.readFileBinary(id, filePath);
|
|
837
|
+
} else {
|
|
838
|
+
const at = await workflowService.fileBytesAtChange(id, filePath, ref, side);
|
|
839
|
+
// Nothing at that path on that side of the save — for `before`, also
|
|
840
|
+
// how a root commit answers, since it has no parent to read.
|
|
841
|
+
if (at === null) {
|
|
842
|
+
res.status(404).json({ error: 'File not found' });
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
// `IWorkflowService` is isomorphic, so the bytes arrive typed as a
|
|
846
|
+
// `Uint8Array`. `res.send` recognises a Buffer and JSON-encodes
|
|
847
|
+
// anything else, so a zero-copy view over the same memory is what it
|
|
848
|
+
// has to be handed — never a copy of a file that may be tens of MB.
|
|
849
|
+
buffer = Buffer.from(at.bytes.buffer, at.bytes.byteOffset, at.bytes.byteLength);
|
|
850
|
+
// The blob's object id IS the content hash, and a past version can
|
|
851
|
+
// never change, so this is the strongest ETag available and costs no
|
|
852
|
+
// second pass over the bytes. Set BEFORE `res.send`, which then skips
|
|
853
|
+
// its own weak ETag and answers a matching If-None-Match with a 304.
|
|
854
|
+
res.setHeader('ETag', `"${at.blobId}"`);
|
|
855
|
+
}
|
|
789
856
|
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase();
|
|
790
857
|
const mimeTypes: Record<string, string> = {
|
|
791
858
|
'.png': 'image/png',
|
|
@@ -844,6 +911,31 @@ export function createWorkspaceRoutes(
|
|
|
844
911
|
sendError(res, error);
|
|
845
912
|
return;
|
|
846
913
|
}
|
|
914
|
+
// A refusal the ref branch raised has to reach the caller AS ITSELF: a
|
|
915
|
+
// save from another branch is answered with
|
|
916
|
+
// `VERSION_NOT_ON_BRANCH_MESSAGE`, and flattening it into the generic
|
|
917
|
+
// "File not found" would tell a reader their file had vanished. Only
|
|
918
|
+
// the ref branch can raise one, so the working-tree read keeps the
|
|
919
|
+
// blanket 404 it has always answered with.
|
|
920
|
+
if (ref !== null && error instanceof WorkflowDomainError) {
|
|
921
|
+
sendError(res, error);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
// Anything else out of the ref branch is OUR failure, not a missing
|
|
925
|
+
// version: the read goes through the git runner, where a blob over
|
|
926
|
+
// `MAX_OUTPUT_BYTES` and a timed-out `cat-file` both arrive here as
|
|
927
|
+
// plain errors. A 404 would tell a reader the save they can see listed
|
|
928
|
+
// had vanished, and would do it silently — so it is a logged 500, and
|
|
929
|
+
// "Try again" in the pane is then a truthful offer.
|
|
930
|
+
if (ref !== null) {
|
|
931
|
+
log.error(
|
|
932
|
+
`could not read ${printable(filePath)} at ${printable(ref)}: ${printable(
|
|
933
|
+
error instanceof Error ? error.message : String(error),
|
|
934
|
+
)}`,
|
|
935
|
+
);
|
|
936
|
+
res.status(500).json({ error: 'Could not read this version of the file' });
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
847
939
|
res.status(404).json({ error: 'File not found' });
|
|
848
940
|
}
|
|
849
941
|
});
|