@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
|
@@ -35,10 +35,12 @@ import {
|
|
|
35
35
|
ProtectedBranchError,
|
|
36
36
|
PullRebaseConflictError,
|
|
37
37
|
RemoteBranchGoneError,
|
|
38
|
+
VersionNotOnBranchError,
|
|
38
39
|
isMissingRemoteBranchFailure,
|
|
39
40
|
} from '../../../shared/domain-errors.js';
|
|
40
41
|
import {
|
|
41
42
|
GitRunError,
|
|
43
|
+
assertOneSpecPerLine,
|
|
42
44
|
isGitTimeout,
|
|
43
45
|
redactGitToken,
|
|
44
46
|
type GitCredentials,
|
|
@@ -1760,7 +1762,12 @@ export class GitService implements IGitService {
|
|
|
1760
1762
|
* conflict into the typed error the workflow layer queues recovery for.
|
|
1761
1763
|
* Shared by `pull` and `syncFromRemote` so the two cannot drift.
|
|
1762
1764
|
*/
|
|
1763
|
-
private async rebaseOntoRemote(
|
|
1765
|
+
private async rebaseOntoRemote(
|
|
1766
|
+
cwd: string,
|
|
1767
|
+
branch: string,
|
|
1768
|
+
remoteRef: string,
|
|
1769
|
+
opts: { preserveMerges?: boolean } = {},
|
|
1770
|
+
): Promise<void> {
|
|
1764
1771
|
// An UNBORN HEAD (a clone of an upstream that was empty when cloned) has
|
|
1765
1772
|
// no commit to replay and `git rebase` refuses to start from it, so the
|
|
1766
1773
|
// branch is moved to the remote commit instead. That is only safe while
|
|
@@ -1798,7 +1805,20 @@ export class GitService implements IGitService {
|
|
|
1798
1805
|
// the local commits onto origin, then reapplies them, so the retry
|
|
1799
1806
|
// push can fast-forward. Untracked files don't block rebase and are
|
|
1800
1807
|
// left untouched.
|
|
1801
|
-
|
|
1808
|
+
// `--rebase-merges` is what keeps an unpushed MERGE a merge. Plain
|
|
1809
|
+
// rebase replays `--no-merges upstream..HEAD`, which turns a merge
|
|
1810
|
+
// commit the clone holds but origin has not seen into cherry-picks of
|
|
1811
|
+
// the commits it merged — the merged branch stops being a parent, so
|
|
1812
|
+
// "does the published head contain the target's head" answers no
|
|
1813
|
+
// however many times the update runs. Only the change-request update
|
|
1814
|
+
// asks for it; every other pull replays plain saves, where the two
|
|
1815
|
+
// spellings produce the same history.
|
|
1816
|
+
await this.git(cwd, [
|
|
1817
|
+
'rebase',
|
|
1818
|
+
'--autostash',
|
|
1819
|
+
...(opts.preserveMerges ? ['--rebase-merges'] : []),
|
|
1820
|
+
remoteRef,
|
|
1821
|
+
]);
|
|
1802
1822
|
} catch (err) {
|
|
1803
1823
|
// Capture the contested paths BEFORE the abort wipes the rebase
|
|
1804
1824
|
// state — `--diff-filter=U` lists exactly the files whose replay
|
|
@@ -1922,7 +1942,10 @@ export class GitService implements IGitService {
|
|
|
1922
1942
|
});
|
|
1923
1943
|
}
|
|
1924
1944
|
|
|
1925
|
-
async pull(
|
|
1945
|
+
async pull(
|
|
1946
|
+
workspaceId: string,
|
|
1947
|
+
opts: { preserveMerges?: boolean } = {},
|
|
1948
|
+
): Promise<{ treeChanged: boolean }> {
|
|
1926
1949
|
return this.mutex.run(workspaceId, async () => {
|
|
1927
1950
|
const cwd = await this.repoDir(workspaceId);
|
|
1928
1951
|
const branch = await this.checkedOutBranch(cwd);
|
|
@@ -1935,7 +1958,7 @@ export class GitService implements IGitService {
|
|
|
1935
1958
|
// Refresh WITHOUT `git pull`, and without touching `FETCH_HEAD` — see
|
|
1936
1959
|
// `refreshRemoteBranchRef` for why both halves are load-bearing.
|
|
1937
1960
|
const remoteRef = await this.refreshRemoteBranchRef(cwd, branch);
|
|
1938
|
-
await this.rebaseOntoRemote(cwd, branch, remoteRef);
|
|
1961
|
+
await this.rebaseOntoRemote(cwd, branch, remoteRef, opts);
|
|
1939
1962
|
this.accessControl?.invalidate(workspaceId);
|
|
1940
1963
|
const treeAfter = (await this.revParseOrNull(cwd, 'HEAD^{tree}')) ?? '';
|
|
1941
1964
|
return { treeChanged: treeAfter !== treeBefore };
|
|
@@ -2310,6 +2333,10 @@ export class GitService implements IGitService {
|
|
|
2310
2333
|
const repoRelativePath = this.stripRepoPrefix(relativePath);
|
|
2311
2334
|
return this.mutex.run(workspaceId, async () => {
|
|
2312
2335
|
const cwd = await this.repoDir(workspaceId);
|
|
2336
|
+
// Branch-scoped, like the bytes route: a patch is a past version too,
|
|
2337
|
+
// and leaving this route open would let a sha refused there be read
|
|
2338
|
+
// here instead.
|
|
2339
|
+
await this.assertOnBranchHistory(cwd, sha);
|
|
2313
2340
|
// BOTH sides: at a commit that DELETES a directory the path is absent
|
|
2314
2341
|
// at `sha` but a tree at `sha^` — and the diff of that deletion is
|
|
2315
2342
|
// every child's content, exactly what the per-file gate never checked.
|
|
@@ -2361,6 +2388,10 @@ export class GitService implements IGitService {
|
|
|
2361
2388
|
const repoRelativePath = this.stripRepoPrefix(relativePath);
|
|
2362
2389
|
return this.mutex.run(workspaceId, async () => {
|
|
2363
2390
|
const cwd = await this.repoDir(workspaceId);
|
|
2391
|
+
// Same branch rule as the patch and the bytes: this route serves the
|
|
2392
|
+
// FULL text of both sides, so it is the most generous of the three and
|
|
2393
|
+
// the least safe one to leave ungated.
|
|
2394
|
+
await this.assertOnBranchHistory(cwd, sha);
|
|
2364
2395
|
await this.assertNotTreeAtRef(cwd, sha, repoRelativePath, relativePath);
|
|
2365
2396
|
await this.assertNotTreeAtRef(cwd, `${sha}^`, repoRelativePath, relativePath);
|
|
2366
2397
|
const current = await this.readFileAtRef(workspaceId, sha, repoRelativePath);
|
|
@@ -3248,6 +3279,143 @@ export class GitService implements IGitService {
|
|
|
3248
3279
|
}
|
|
3249
3280
|
}
|
|
3250
3281
|
|
|
3282
|
+
/**
|
|
3283
|
+
* A repo-relative file's BYTES at an arbitrary git ref — the binary sibling
|
|
3284
|
+
* of {@link readFileAtRef}, with the blob's object id alongside them.
|
|
3285
|
+
*
|
|
3286
|
+
* `readFileAtRef` decodes stdout as UTF-8, which for a PNG or a PDF is not a
|
|
3287
|
+
* lossless round trip: every byte sequence that is not valid UTF-8 comes
|
|
3288
|
+
* back as U+FFFD and the file is ruined. A past version has to be delivered
|
|
3289
|
+
* byte for byte, so this reads through `cat-file --batch`, whose framing is
|
|
3290
|
+
* in BYTES (`<oid> <type> <size>\n<size bytes>\n`) rather than characters.
|
|
3291
|
+
*
|
|
3292
|
+
* One invocation, and absence is a PROTOCOL answer (`<spec> missing`) rather
|
|
3293
|
+
* than a parsed error message — which also covers `<sha>^` of a root commit,
|
|
3294
|
+
* where there is no "before" side at all. `null` therefore means exactly
|
|
3295
|
+
* "nothing is at that path on that side".
|
|
3296
|
+
*
|
|
3297
|
+
* The `oid` is the content's own hash, so it is what the HTTP route uses as
|
|
3298
|
+
* an ETag: cheaper than hashing the body again, and stable for a version
|
|
3299
|
+
* that by definition can never change.
|
|
3300
|
+
*/
|
|
3301
|
+
private async blobAtRef(
|
|
3302
|
+
cwd: string,
|
|
3303
|
+
ref: string,
|
|
3304
|
+
repoRelativePath: string,
|
|
3305
|
+
): Promise<{ bytes: Buffer; blobId: string } | null> {
|
|
3306
|
+
// The spec has to be ONE line: the path is refused a control character
|
|
3307
|
+
// twice over before it gets here (at the route's normaliser and again at
|
|
3308
|
+
// `assertValidRelativePath`), and this is the fence at the protocol —
|
|
3309
|
+
// `--batch` would otherwise read `<sha>:Docs/x.md\nzzz` as two object
|
|
3310
|
+
// names and answer about the first, under the gates of the second.
|
|
3311
|
+
const spec = `${ref}:${repoRelativePath}`;
|
|
3312
|
+
assertOneSpecPerLine([spec]);
|
|
3313
|
+
const { stdout } = await this.gitBytes(cwd, ['cat-file', '--batch'], {
|
|
3314
|
+
input: `${spec}\n`,
|
|
3315
|
+
});
|
|
3316
|
+
const nl = stdout.indexOf(0x0a);
|
|
3317
|
+
if (nl < 0) throw new Error('unexpected end of git cat-file output');
|
|
3318
|
+
// The spec may itself contain spaces (a KB file routinely does), hence the
|
|
3319
|
+
// endsWith checks rather than a split — same protocol reading as the
|
|
3320
|
+
// access resolver's `catFileBatch`.
|
|
3321
|
+
const header = stdout.subarray(0, nl).toString('utf-8');
|
|
3322
|
+
if (header.endsWith(' missing') || header.endsWith(' ambiguous')) return null;
|
|
3323
|
+
const [blobId, type, rawSize] = header.split(' ');
|
|
3324
|
+
// A tree here would be the per-file gate bypassed, and `assertNotTreeAtRef`
|
|
3325
|
+
// is what refuses it before this runs; anything else non-blob (a tag, a
|
|
3326
|
+
// commit) is not a file either. Nothing to serve in either case.
|
|
3327
|
+
if (type !== 'blob' || blobId === undefined) return null;
|
|
3328
|
+
const size = Number(rawSize);
|
|
3329
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
3330
|
+
throw new Error(`unexpected git cat-file header: ${header}`);
|
|
3331
|
+
}
|
|
3332
|
+
return { bytes: stdout.subarray(nl + 1, nl + 1 + size), blobId };
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
/**
|
|
3336
|
+
* Refuse a sha that is not in the history of the branch this workspace has
|
|
3337
|
+
* checked out.
|
|
3338
|
+
*
|
|
3339
|
+
* A file's past is served per BRANCH: the history panel lists what `git log`
|
|
3340
|
+
* on this workspace's own branch touched, and every read of a past save has
|
|
3341
|
+
* to stay inside that list. Without this, a sha lifted from another branch —
|
|
3342
|
+
* or from a change request nobody approved — would be readable through the
|
|
3343
|
+
* workspace of a branch whose history never contained it, which is the
|
|
3344
|
+
* access rules of the viewed branch bypassed by a query parameter.
|
|
3345
|
+
*
|
|
3346
|
+
* `merge-base --is-ancestor` is the exact question, and a commit is its own
|
|
3347
|
+
* ancestor, so the branch head passes. Exit 1 is git's answer for "resolved,
|
|
3348
|
+
* and not an ancestor". An unresolvable object name — a made-up id — gets
|
|
3349
|
+
* the SAME refusal on purpose (see {@link VersionNotOnBranchError}), but only
|
|
3350
|
+
* when git's complaint NAMES that id; every other failure propagates, a
|
|
3351
|
+
* broken repository included. A guard that fails open on its own errors is
|
|
3352
|
+
* not a guard, and one that reports them as a refusal is worse than no
|
|
3353
|
+
* message at all.
|
|
3354
|
+
*/
|
|
3355
|
+
private async assertOnBranchHistory(cwd: string, sha: string): Promise<void> {
|
|
3356
|
+
try {
|
|
3357
|
+
await this.git(cwd, ['merge-base', '--is-ancestor', sha, 'HEAD']);
|
|
3358
|
+
} catch (err) {
|
|
3359
|
+
if (err instanceof GitRunError && !err.timedOut) {
|
|
3360
|
+
if (err.exitCode === 1) throw new VersionNotOnBranchError();
|
|
3361
|
+
const stderr = err.stderr ?? err.message;
|
|
3362
|
+
if (
|
|
3363
|
+
// The complaint has to be about THE SHA WE ASKED FOR. `HEAD` is the
|
|
3364
|
+
// other argument, and git spells its own failures the same way: a
|
|
3365
|
+
// repository with no commits answers "ambiguous argument 'HEAD'",
|
|
3366
|
+
// which is a broken workspace, not a version off the branch.
|
|
3367
|
+
// Refusing it as "not in this file's history" would hide an
|
|
3368
|
+
// infrastructure failure behind a sentence about access.
|
|
3369
|
+
stderr.toLowerCase().includes(sha.toLowerCase()) &&
|
|
3370
|
+
// git's wording for "I cannot resolve that" varies by subcommand and
|
|
3371
|
+
// by version — `merge-base` says "Not a valid commit name", others
|
|
3372
|
+
// "not a valid object name" / "unknown revision" — so every spelling
|
|
3373
|
+
// that means the same thing is listed rather than one guessed at.
|
|
3374
|
+
/not a valid (object|commit) name|no such commit|bad object|unknown revision|malformed object name|ambiguous argument/i.test(
|
|
3375
|
+
stderr,
|
|
3376
|
+
)
|
|
3377
|
+
) {
|
|
3378
|
+
throw new VersionNotOnBranchError();
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
throw err;
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
/**
|
|
3386
|
+
* One file's BYTES at a save, exactly as they were — the `after` side of
|
|
3387
|
+
* `sha`, or the `before` side (`<sha>^`) for the save that deleted it.
|
|
3388
|
+
*
|
|
3389
|
+
* Every guard the text history surfaces apply, in the same order: the sha is
|
|
3390
|
+
* a sha, the save is on this branch, and the pathspec is not a directory at
|
|
3391
|
+
* that ref (a per-file gate upstream authorized one path, and a tree read
|
|
3392
|
+
* would hand over every child). `null` means the file was not there on the
|
|
3393
|
+
* side asked for.
|
|
3394
|
+
*
|
|
3395
|
+
* Runs inside the workspace's git turn — one turn for the whole read, so the
|
|
3396
|
+
* ancestry check and the bytes it authorizes cannot be separated by another
|
|
3397
|
+
* caller's checkout.
|
|
3398
|
+
*/
|
|
3399
|
+
async fileBytesAtCommit(
|
|
3400
|
+
workspaceId: string,
|
|
3401
|
+
relativePath: string,
|
|
3402
|
+
sha: string,
|
|
3403
|
+
side: 'after' | 'before',
|
|
3404
|
+
): Promise<{ bytes: Buffer; blobId: string } | null> {
|
|
3405
|
+
assertValidRelativePath(relativePath);
|
|
3406
|
+
if (!/^[a-f0-9]{7,40}$/i.test(sha)) {
|
|
3407
|
+
throw new WorkflowValidationError('invalid commit sha');
|
|
3408
|
+
}
|
|
3409
|
+
const repoRelativePath = this.stripRepoPrefix(relativePath);
|
|
3410
|
+
return this.mutex.run(workspaceId, async () => {
|
|
3411
|
+
const cwd = await this.repoDir(workspaceId);
|
|
3412
|
+
await this.assertOnBranchHistory(cwd, sha);
|
|
3413
|
+
const ref = side === 'before' ? `${sha}^` : sha;
|
|
3414
|
+
await this.assertNotTreeAtRef(cwd, ref, repoRelativePath, relativePath);
|
|
3415
|
+
return this.blobAtRef(cwd, ref, repoRelativePath);
|
|
3416
|
+
});
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3251
3419
|
/**
|
|
3252
3420
|
* The commits on `ref` that touched `repoRelativePath`, newest first, at
|
|
3253
3421
|
* most `limit` of them. What a skill's `version` is looked up through: each
|
|
@@ -3356,6 +3524,27 @@ export class GitService implements IGitService {
|
|
|
3356
3524
|
env: { GIT_LITERAL_PATHSPECS: '1', ...opts?.env },
|
|
3357
3525
|
});
|
|
3358
3526
|
}
|
|
3527
|
+
|
|
3528
|
+
/**
|
|
3529
|
+
* {@link git}, with stdout as raw BYTES.
|
|
3530
|
+
*
|
|
3531
|
+
* A sibling rather than an `encoding` option on `git()` itself: every other
|
|
3532
|
+
* caller in this service reads text, and the two return types (`string` vs
|
|
3533
|
+
* `Buffer`) are not interchangeable at any call site. Same
|
|
3534
|
+
* `GIT_LITERAL_PATHSPECS` and same runner deadline — the only difference is
|
|
3535
|
+
* that the bytes are not decoded.
|
|
3536
|
+
*/
|
|
3537
|
+
private gitBytes(
|
|
3538
|
+
cwd: string,
|
|
3539
|
+
args: string[],
|
|
3540
|
+
opts?: Omit<GitRunOptions, 'encoding'>,
|
|
3541
|
+
): Promise<GitRunResult<Buffer>> {
|
|
3542
|
+
return this.gitRunner.run(cwd, args, {
|
|
3543
|
+
...opts,
|
|
3544
|
+
encoding: 'buffer',
|
|
3545
|
+
env: { GIT_LITERAL_PATHSPECS: '1', ...opts?.env },
|
|
3546
|
+
});
|
|
3547
|
+
}
|
|
3359
3548
|
}
|
|
3360
3549
|
|
|
3361
3550
|
/** Map a git status letter (`git diff --name-status`) to the DTO's `PrFileStatus`. */
|
|
@@ -250,6 +250,12 @@ export class NodeGitRunner implements IGitRunner {
|
|
|
250
250
|
const timers: NodeJS.Timeout[] = [];
|
|
251
251
|
let timedOut = false;
|
|
252
252
|
|
|
253
|
+
// A credential that expires is renewed before it is read. Renewing is
|
|
254
|
+
// the credential's own business and its failure is not this call's:
|
|
255
|
+
// plenty of git never leaves the disk, and a call that does is told by
|
|
256
|
+
// the host what the token in hand is worth.
|
|
257
|
+
await this.credentials.prepare?.().catch(() => undefined);
|
|
258
|
+
|
|
253
259
|
try {
|
|
254
260
|
const { promise: pending, child } = spawnGit(args, {
|
|
255
261
|
cwd,
|
|
@@ -366,6 +366,60 @@ export class PullRequestService implements IPullRequestService {
|
|
|
366
366
|
return this.summaryOf(row, workspaceId);
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Did the target change, since the fork point, any file this request also
|
|
371
|
+
* changes? The question behind `needsUpdate`.
|
|
372
|
+
*
|
|
373
|
+
* The intersection is taken over BOTH names of every rename on both sides,
|
|
374
|
+
* because a wrong "no" is the expensive answer: it would open a request on
|
|
375
|
+
* a diff read against text that has since moved, with Approve live. So
|
|
376
|
+
* `pathsChangedBetween` runs without rename detection (a rename on the
|
|
377
|
+
* TARGET reports the path it left as well as the one it arrived at), and
|
|
378
|
+
* the request's own side contributes each file's `previousPath` alongside
|
|
379
|
+
* its `path`.
|
|
380
|
+
*
|
|
381
|
+
* Answers true without asking git in the two cases where nothing may be
|
|
382
|
+
* assumed: no fork point (the branches share no history, so there is no
|
|
383
|
+
* range to diff) and a failed diff (an infra error must not read as "this
|
|
384
|
+
* request is fine"). Answers false without asking git when the request is
|
|
385
|
+
* not behind at all, or changes no files — the common case, and the one
|
|
386
|
+
* that must stay free.
|
|
387
|
+
*/
|
|
388
|
+
private async targetTouchesRequestFiles(
|
|
389
|
+
workspaceId: string,
|
|
390
|
+
forkPoint: { mergeBaseSha: string | null; behind: boolean },
|
|
391
|
+
baseSha: string,
|
|
392
|
+
files: PullRequestFile[],
|
|
393
|
+
): Promise<boolean> {
|
|
394
|
+
if (!forkPoint.behind) return false;
|
|
395
|
+
if (files.length === 0) return false;
|
|
396
|
+
// Diverged with no common ancestor: the target's whole history is
|
|
397
|
+
// "changed since the fork point", so anything this request touches is
|
|
398
|
+
// touched. Nothing to intersect against, and nothing to skip for.
|
|
399
|
+
if (!forkPoint.mergeBaseSha) return true;
|
|
400
|
+
const requestPaths = new Set<string>();
|
|
401
|
+
for (const f of files) {
|
|
402
|
+
requestPaths.add(f.path);
|
|
403
|
+
if (f.previousPath) requestPaths.add(f.previousPath);
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const onTarget = await this.gitService.pathsChangedBetween(
|
|
407
|
+
workspaceId,
|
|
408
|
+
forkPoint.mergeBaseSha,
|
|
409
|
+
// The target tip the WHOLE detail is pinned to, so this range ends
|
|
410
|
+
// exactly where `behind` and the file list were computed.
|
|
411
|
+
baseSha,
|
|
412
|
+
);
|
|
413
|
+
return onTarget.some((path) => requestPaths.has(path));
|
|
414
|
+
} catch (err) {
|
|
415
|
+
log.warn(
|
|
416
|
+
'could not compare the target against a change request\'s files; treating it as needing an update',
|
|
417
|
+
{ err },
|
|
418
|
+
);
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
369
423
|
async getPrDetail(
|
|
370
424
|
prNumber: number,
|
|
371
425
|
opts: { fresh?: boolean; workspaceId?: string; viewerEmail?: string; patches?: boolean } = {},
|
|
@@ -392,6 +446,8 @@ export class PullRequestService implements IPullRequestService {
|
|
|
392
446
|
mergeBaseSha: null,
|
|
393
447
|
behind: false,
|
|
394
448
|
};
|
|
449
|
+
/** Did the target change, since the fork point, a file this request changes? */
|
|
450
|
+
let targetChangedShared = false;
|
|
395
451
|
if (workspaceId) {
|
|
396
452
|
const shas = await this.gitService.resolvePrShas(
|
|
397
453
|
workspaceId,
|
|
@@ -417,6 +473,17 @@ export class PullRequestService implements IPullRequestService {
|
|
|
417
473
|
// Pinned to the same two commits as the file list, so "needs updating"
|
|
418
474
|
// and the diff it qualifies can never describe different heads.
|
|
419
475
|
forkPoint = await this.gitService.forkPointForPr(workspaceId, { baseSha, headSha });
|
|
476
|
+
// Only a target that moved in a file THIS request also changes makes the
|
|
477
|
+
// proposal's diff describe text that has moved. One extra
|
|
478
|
+
// `diff --name-only` (and only when the branches have diverged at all)
|
|
479
|
+
// buys the difference between opening instantly and paying for a merge,
|
|
480
|
+
// a push and a second detail read.
|
|
481
|
+
targetChangedShared = await this.targetTouchesRequestFiles(
|
|
482
|
+
workspaceId,
|
|
483
|
+
forkPoint,
|
|
484
|
+
baseSha,
|
|
485
|
+
files,
|
|
486
|
+
);
|
|
420
487
|
}
|
|
421
488
|
|
|
422
489
|
// Validated cache hit: TTL fresh AND head SHA unchanged since we cached.
|
|
@@ -511,6 +578,22 @@ export class PullRequestService implements IPullRequestService {
|
|
|
511
578
|
approvals,
|
|
512
579
|
});
|
|
513
580
|
|
|
581
|
+
// Hashed here, not on the client: the detail carries an author HASH and no
|
|
582
|
+
// author email, so the comparison can only happen where the viewer's email
|
|
583
|
+
// is already known.
|
|
584
|
+
const viewerIsAuthor = !!(
|
|
585
|
+
opts.viewerEmail &&
|
|
586
|
+
summary.authorId &&
|
|
587
|
+
summary.authorId === hashEmail(opts.viewerEmail)
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
const viewerCanDelete = computeViewerCanDelete({
|
|
591
|
+
state: summary.state,
|
|
592
|
+
authorId: summary.authorId,
|
|
593
|
+
viewerEmail: opts.viewerEmail,
|
|
594
|
+
viewerCanBypassMerge,
|
|
595
|
+
});
|
|
596
|
+
|
|
514
597
|
const detail: PullRequestDetail = {
|
|
515
598
|
...summary,
|
|
516
599
|
body: row.body,
|
|
@@ -526,7 +609,12 @@ export class PullRequestService implements IPullRequestService {
|
|
|
526
609
|
viewerCanCancel,
|
|
527
610
|
mergeBaseSha: forkPoint.mergeBaseSha,
|
|
528
611
|
behind: summary.state === 'open' && forkPoint.behind,
|
|
612
|
+
// `behind` AND the divergence actually reaches this request's files.
|
|
613
|
+
// Both halves, in this order, so a closed request is never either.
|
|
614
|
+
needsUpdate: summary.state === 'open' && forkPoint.behind && targetChangedShared,
|
|
529
615
|
viewerCanUpdate,
|
|
616
|
+
viewerIsAuthor,
|
|
617
|
+
viewerCanDelete,
|
|
530
618
|
};
|
|
531
619
|
|
|
532
620
|
// A patch-less detail is an internal read; it must not be served to the
|
|
@@ -631,7 +719,38 @@ export function computeViewerCanUpdate(input: {
|
|
|
631
719
|
return viewerIsAuthor || input.viewerCanBypassMerge || viewerMayApply;
|
|
632
720
|
}
|
|
633
721
|
|
|
722
|
+
/**
|
|
723
|
+
* Pure predicate for the `viewerCanDelete` hint — who may delete a request
|
|
724
|
+
* outright, closing it AND retiring its branch. The author (their own
|
|
725
|
+
* proposal and their own branch) or an admin (`viewerCanBypassMerge` is the
|
|
726
|
+
* proxy — the same `canWriteAtRef('roles.yaml')` predicate the DELETE route
|
|
727
|
+
* enforces). Mirrors `deleteChangeRequest` exactly, which is why it differs
|
|
728
|
+
* from its two siblings in both directions:
|
|
729
|
+
*
|
|
730
|
+
* - It does NOT grant the owner of every changed file, as
|
|
731
|
+
* `computeViewerCanCancel` does. Declining someone else's request leaves
|
|
732
|
+
* their branch to rework; deleting it destroys their text.
|
|
733
|
+
* - It bars only `merged`, not everything that is not `open`. A request
|
|
734
|
+
* withdrawn in another tab is `closed` and still has a leftover branch the
|
|
735
|
+
* server will retire, so the button must survive it. Applied history is
|
|
736
|
+
* nobody's to delete.
|
|
737
|
+
*
|
|
738
|
+
* Fail-closed: no viewer email → false, whatever the grants say.
|
|
739
|
+
*/
|
|
740
|
+
export function computeViewerCanDelete(input: {
|
|
741
|
+
state: PullRequestState;
|
|
742
|
+
authorId: string | undefined;
|
|
743
|
+
viewerEmail: string | undefined;
|
|
744
|
+
viewerCanBypassMerge: boolean;
|
|
745
|
+
}): boolean {
|
|
746
|
+
if (input.state === 'merged') return false;
|
|
747
|
+
if (!input.viewerEmail) return false;
|
|
748
|
+
const viewerIsAuthor = !!(input.authorId && input.authorId === hashEmail(input.viewerEmail));
|
|
749
|
+
return viewerIsAuthor || input.viewerCanBypassMerge;
|
|
750
|
+
}
|
|
751
|
+
|
|
634
752
|
export const __testing = {
|
|
635
753
|
computeViewerCanCancel,
|
|
636
754
|
computeViewerCanUpdate,
|
|
755
|
+
computeViewerCanDelete,
|
|
637
756
|
};
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* (status='needs_attention')
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
-
import { and, eq, inArray, isNull, min, or, sql } from 'drizzle-orm';
|
|
37
|
+
import { and, eq, inArray, isNull, min, or, sql, type SQL } from 'drizzle-orm';
|
|
38
38
|
import { logger } from '../../shared/logging.js';
|
|
39
39
|
|
|
40
40
|
const log = logger('pending-commits');
|
|
@@ -384,6 +384,64 @@ export class PendingCommitsService {
|
|
|
384
384
|
.where(eq(pendingCommits.id, id));
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Take every commit still queued for `branch` out of the worker's reach and
|
|
389
|
+
* onto the admin surface, and answer how many moved.
|
|
390
|
+
*
|
|
391
|
+
* The one caller is the knowledge-base repository being REPLACED. A release
|
|
392
|
+
* ENQUEUES the bytes and only then drops its lock, so the moment the locks
|
|
393
|
+
* on a branch are dropped there may be rows already waiting — bytes that
|
|
394
|
+
* were written against a working copy of the repository that is gone. Left
|
|
395
|
+
* `pending`, the worker would pick them up after the re-clone and commit
|
|
396
|
+
* them onto a same-named branch of a DIFFERENT repository: someone else's
|
|
397
|
+
* file, overwritten by a change nobody made to it.
|
|
398
|
+
*
|
|
399
|
+
* `needs_attention`, not deleted — the same rule as everything else about a
|
|
400
|
+
* replacement. The bytes are still in the row, the reason is on it, and they
|
|
401
|
+
* show up on the pending-commit admin surface for a person to decide about.
|
|
402
|
+
* `running` rows go too: the worker checks the row back in when it finishes,
|
|
403
|
+
* and a row it is mid-flight with is exactly one whose destination just
|
|
404
|
+
* changed under it.
|
|
405
|
+
*/
|
|
406
|
+
async markNeedsAttentionOnBranch(branch: string, error: string): Promise<number> {
|
|
407
|
+
return this.escalateQueued(eq(pendingCommits.branch, branch), error);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Every commit still queued that `which` selects goes to a person, with the reason. Answers how many. */
|
|
411
|
+
private async escalateQueued(which: SQL, error: string): Promise<number> {
|
|
412
|
+
const moved = await this.db
|
|
413
|
+
.update(pendingCommits)
|
|
414
|
+
.set({
|
|
415
|
+
status: 'needs_attention',
|
|
416
|
+
lastError: error,
|
|
417
|
+
lastAttemptedAt: new Date(),
|
|
418
|
+
})
|
|
419
|
+
.where(and(which, inArray(pendingCommits.status, ['pending', 'running'])))
|
|
420
|
+
.returning({ id: pendingCommits.id });
|
|
421
|
+
return moved.length;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The same, for every commit still queued against ONE WORKING COPY, and
|
|
426
|
+
* answer how many moved.
|
|
427
|
+
*
|
|
428
|
+
* The caller is the startup phase setting a working copy aside: a clone of
|
|
429
|
+
* a repository the configured one shares no history with. Whatever was
|
|
430
|
+
* queued for it was written against that clone, on any branch, the default
|
|
431
|
+
* one included, and the directory the worker would write to is now a fresh
|
|
432
|
+
* clone of another repository. Closing change requests covers the branches
|
|
433
|
+
* of the requests that were closed and nothing else, which is why this is
|
|
434
|
+
* keyed on the working copy: it is the thing that was replaced.
|
|
435
|
+
*
|
|
436
|
+
* A working copy that was only pointed at a new address (the same
|
|
437
|
+
* repository, moved) keeps its queue, and is never passed here.
|
|
438
|
+
*/
|
|
439
|
+
async markNeedsAttentionInWorkspace(workspaceId: string, error: string): Promise<number> {
|
|
440
|
+
// The canonical form `enqueue` stores (see `canonicalWorkspaceId`), so the
|
|
441
|
+
// caller's spelling of the id cannot make this match nothing.
|
|
442
|
+
return this.escalateQueued(eq(pendingCommits.workspaceId, canonicalWorkspaceId(workspaceId)), error);
|
|
443
|
+
}
|
|
444
|
+
|
|
387
445
|
/**
|
|
388
446
|
* When the oldest commit still waiting to land was queued, or null when
|
|
389
447
|
* nothing waits — the readiness answer's one number. `pending` and
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import type { ValidationReport } from '@bevel-software/platform-shared';
|
|
1
|
+
import type { AuthUser, ValidationReport } from '@bevel-software/platform-shared';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Workflow lifecycle hooks — the generic seam that replaced the
|
|
5
|
-
* constructor-injected KB validator
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* registers none, so a core-only deployment runs every hook point as a no-op.
|
|
5
|
+
* constructor-injected KB validator. Core code invokes the hooks at fixed
|
|
6
|
+
* lifecycle points; the modules that OWN the behavior register handlers in the
|
|
7
|
+
* composition root, right after `createCoreServices` returns. Core registers
|
|
8
|
+
* none, so a core-only deployment runs every hook point as a no-op.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
10
|
+
* Three hook kinds, with deliberately different failure semantics:
|
|
12
11
|
*
|
|
13
12
|
* - `commitValidation` — ADVISORY. Runs at commit time in `GitService`
|
|
14
13
|
* (`commit` / `commitFile`), exactly where the injected `IKbValidator`
|
|
@@ -18,12 +17,18 @@ import type { ValidationReport } from '@bevel-software/platform-shared';
|
|
|
18
17
|
* `runValidation(workspaceId)` shape so `KbValidatorService` registers
|
|
19
18
|
* with a one-line adapter.
|
|
20
19
|
*
|
|
21
|
-
* - `
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
20
|
+
* - `agentRead` — BLOCKING. Runs before a gated agent READ of a
|
|
21
|
+
* knowledge-base path (reads, `delete_file`, `delete_folder`, and each
|
|
22
|
+
* file a `grep` walk opens). A hook that throws REFUSES the read.
|
|
23
|
+
*
|
|
24
|
+
* - `preWrite` — BLOCKING. Runs before a gated agent WRITE (the file-write
|
|
25
|
+
* tools, once per path) or a write-capable shell command, which carries no
|
|
26
|
+
* path. A hook that throws REFUSES the operation, and the caller sees the
|
|
27
|
+
* hook's own message and status.
|
|
28
|
+
*
|
|
29
|
+
* The two blocking hooks are the seam a deployment builds a per-conversation
|
|
30
|
+
* rule on: they carry the session id the call named, and core itself neither
|
|
31
|
+
* decides nor records anything (see `workspace/agent-access.gate.ts`).
|
|
27
32
|
*/
|
|
28
33
|
|
|
29
34
|
/** What `GitService` knows at the advisory commit-validation point. */
|
|
@@ -47,19 +52,35 @@ export type CommitValidationHook = (
|
|
|
47
52
|
) => Promise<ValidationReport | void>;
|
|
48
53
|
|
|
49
54
|
/**
|
|
50
|
-
* What
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
55
|
+
* What an agent operation looks like to the blocking hooks. The core gate has
|
|
56
|
+
* already resolved the only two skips it owns (the caller is not an agent, or
|
|
57
|
+
* is the recovery bot), so every hook call describes a real agent operation.
|
|
58
|
+
*
|
|
59
|
+
* `sessionId` is present only when the call carried one — core does NOT fail
|
|
60
|
+
* closed on its absence, because whether a path needs a session is the
|
|
61
|
+
* registering deployment's rule, not core's.
|
|
54
62
|
*/
|
|
55
|
-
export
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
|
|
63
|
+
export interface AgentOperationContext {
|
|
64
|
+
/** The conversation the call belongs to, when it named one. */
|
|
65
|
+
sessionId?: string;
|
|
66
|
+
/**
|
|
67
|
+
* The workspace-relative path the operation targets. Absent for a
|
|
68
|
+
* write-capable operation with no resolvable target (`execute_command`).
|
|
69
|
+
*/
|
|
70
|
+
wsPath?: string;
|
|
71
|
+
/** The branch (draft) whose workspace the operation acts on. */
|
|
72
|
+
branch: string;
|
|
73
|
+
/** The calling user. */
|
|
74
|
+
user: AuthUser;
|
|
75
|
+
/** Whether the caller is the in-app agent (`internal`) or an external one. */
|
|
76
|
+
source: 'internal' | 'external';
|
|
77
|
+
}
|
|
60
78
|
|
|
61
|
-
/** Blocking pre-
|
|
62
|
-
export type
|
|
79
|
+
/** Blocking pre-read hook: throw to refuse the read; return to allow. */
|
|
80
|
+
export type AgentReadHook = (ctx: AgentOperationContext) => Promise<void>;
|
|
81
|
+
|
|
82
|
+
/** Blocking pre-write hook: throw to refuse the write; return to allow. */
|
|
83
|
+
export type PreWriteHook = (ctx: AgentOperationContext) => Promise<void>;
|
|
63
84
|
|
|
64
85
|
/**
|
|
65
86
|
* The registry instance. ONE per composition (created in
|
|
@@ -68,6 +89,7 @@ export type PreWriteHook = (ctx: PreWriteContext) => Promise<void>;
|
|
|
68
89
|
*/
|
|
69
90
|
export class WorkflowHooks {
|
|
70
91
|
private readonly commitValidation: CommitValidationHook[] = [];
|
|
92
|
+
private readonly agentRead: AgentReadHook[] = [];
|
|
71
93
|
private readonly preWrite: PreWriteHook[] = [];
|
|
72
94
|
|
|
73
95
|
/** Register an advisory commit-time validation hook. */
|
|
@@ -75,6 +97,11 @@ export class WorkflowHooks {
|
|
|
75
97
|
this.commitValidation.push(hook);
|
|
76
98
|
}
|
|
77
99
|
|
|
100
|
+
/** Register a blocking pre-read hook. */
|
|
101
|
+
onAgentRead(hook: AgentReadHook): void {
|
|
102
|
+
this.agentRead.push(hook);
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
/** Register a blocking pre-write hook. */
|
|
79
106
|
onPreWrite(hook: PreWriteHook): void {
|
|
80
107
|
this.preWrite.push(hook);
|
|
@@ -88,12 +115,23 @@ export class WorkflowHooks {
|
|
|
88
115
|
return this.commitValidation;
|
|
89
116
|
}
|
|
90
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Run every blocking pre-read hook in registration order. The first throw
|
|
120
|
+
* propagates and refuses the read; with no hooks registered (core-only)
|
|
121
|
+
* this resolves immediately.
|
|
122
|
+
*/
|
|
123
|
+
async runAgentRead(ctx: AgentOperationContext): Promise<void> {
|
|
124
|
+
for (const hook of this.agentRead) {
|
|
125
|
+
await hook(ctx);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
91
129
|
/**
|
|
92
130
|
* Run every blocking pre-write hook in registration order. The first throw
|
|
93
|
-
* propagates and
|
|
131
|
+
* propagates and refuses the write; with no hooks registered (core-only)
|
|
94
132
|
* this resolves immediately.
|
|
95
133
|
*/
|
|
96
|
-
async runPreWrite(ctx:
|
|
134
|
+
async runPreWrite(ctx: AgentOperationContext): Promise<void> {
|
|
97
135
|
for (const hook of this.preWrite) {
|
|
98
136
|
await hook(ctx);
|
|
99
137
|
}
|
|
@@ -790,7 +790,9 @@ export function createWorkflowRoutes(
|
|
|
790
790
|
|
|
791
791
|
/**
|
|
792
792
|
* Delete a change request outright — close it and retire its source
|
|
793
|
-
* branch.
|
|
793
|
+
* branch. The request's author or an admin: their own proposal to throw
|
|
794
|
+
* away, and the moderation verb for a shared deployment. The changed files'
|
|
795
|
+
* owners are NOT included — they keep reject, which leaves the branch.
|
|
794
796
|
*/
|
|
795
797
|
router.delete('/workflow/change-requests/:number', async (req, res) => {
|
|
796
798
|
const num = parsePrNumber(req.params.number);
|