@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
|
@@ -9,11 +9,76 @@ import { createToolContextResolver } from '../../../tool-helpers/tool-context.js
|
|
|
9
9
|
import { createToolHandlerFactory } from '../../../tool-helpers/tool-handler.js';
|
|
10
10
|
import { createManualRoutes } from '../../../tool-registry/manual.routes.js';
|
|
11
11
|
import { registerWorkflowTools } from '../workflow.tools.js';
|
|
12
|
-
import { OpenChangeRequestBlocksMergeError } from '../../../../shared/domain-errors.js';
|
|
12
|
+
import { ChangeRequestConflictsError, OpenChangeRequestBlocksMergeError } from '../../../../shared/domain-errors.js';
|
|
13
13
|
|
|
14
14
|
const WS = 'target-company-state';
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* One file of a change-request detail as the service returns it: a 15 KB
|
|
18
|
+
* addition, patch included. The reported failure was 27 of these — the detail
|
|
19
|
+
* answered ~445,000 characters and overflowed the agent's tool-result limit.
|
|
20
|
+
*/
|
|
21
|
+
function addedFile(i: number): Record<string, unknown> {
|
|
22
|
+
const path = `KnowledgeBase/Engineering/Knowledge/Hexis/Generated-Node-${i}.md`;
|
|
23
|
+
return {
|
|
24
|
+
path,
|
|
25
|
+
status: 'added',
|
|
26
|
+
additions: 400,
|
|
27
|
+
deletions: 0,
|
|
28
|
+
// 400 × 37 characters ≈ 15 KB, as reported.
|
|
29
|
+
patch: `@@ -0,0 +1,400 @@\n${'+a line of generated knowledge text\n'.repeat(400)}`,
|
|
30
|
+
isBinary: false,
|
|
31
|
+
sha: `blob-${i}`,
|
|
32
|
+
rawUrl: `https://raw.example.com/blob-${i}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The per-file approval state the detail carries, as the app's dialog reads it. */
|
|
37
|
+
function approvalOf(path: string): Record<string, unknown> {
|
|
38
|
+
return {
|
|
39
|
+
path,
|
|
40
|
+
eligibleApprovers: {
|
|
41
|
+
roles: ['Admin', 'Engineering'],
|
|
42
|
+
users: [{ name: 'Razvan Radulescu', email: 'razvan.radulescu@bevel.software' }],
|
|
43
|
+
},
|
|
44
|
+
approvedBy: [],
|
|
45
|
+
eligibilityResolved: true,
|
|
46
|
+
isApproved: false,
|
|
47
|
+
inMergeGate: true,
|
|
48
|
+
viewerCanApprove: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A full change-request detail over the given files — what `openChangeRequest` answers. */
|
|
53
|
+
function detailOf(files: Record<string, unknown>[]): Record<string, unknown> {
|
|
54
|
+
return {
|
|
55
|
+
number: 7,
|
|
56
|
+
url: 'https://bevel.example.com/change-requests/7',
|
|
57
|
+
title: 'My change',
|
|
58
|
+
state: 'open',
|
|
59
|
+
branch: 'me/draft',
|
|
60
|
+
base: 'target-company-state',
|
|
61
|
+
body: 'Body shown verbatim to reviewers.',
|
|
62
|
+
author: { login: 'bevel-bot' },
|
|
63
|
+
headSha: 'head-1',
|
|
64
|
+
baseSha: 'base-1',
|
|
65
|
+
files,
|
|
66
|
+
comments: [],
|
|
67
|
+
approvals: files.map((f) => approvalOf(f.path as string)),
|
|
68
|
+
mergeableInBevel: false,
|
|
69
|
+
mergeBlockedReasons: files.map((f) => `${f.path as string} is not approved`),
|
|
70
|
+
mergeWarnings: files.map((f) => `${f.path as string} is not approved`),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The reported case: 27 new files of 15 KB each. */
|
|
75
|
+
const BIG = () => detailOf(Array.from({ length: 27 }, (_, i) => addedFile(i)));
|
|
76
|
+
|
|
16
77
|
let calls: unknown[][] = [];
|
|
78
|
+
/** What the service answers `openChangeRequest` with; a test replaces it. */
|
|
79
|
+
let opened: Record<string, unknown> = BIG();
|
|
80
|
+
/** Set to make `openChangeRequest` throw instead. */
|
|
81
|
+
let openFails: Error | undefined;
|
|
17
82
|
const externalApiKeyService = {
|
|
18
83
|
looksLikeExternalApiKey: (t: string) => typeof t === 'string' && t.startsWith('bevel_'),
|
|
19
84
|
verifyAndLoadToken: async (t: string) =>
|
|
@@ -38,7 +103,8 @@ const workflowService = {
|
|
|
38
103
|
},
|
|
39
104
|
openChangeRequest: async (ws: string, user: { id: string }, body: unknown) => {
|
|
40
105
|
calls.push(['openChangeRequest', ws, user.id, body]);
|
|
41
|
-
|
|
106
|
+
if (openFails) throw openFails;
|
|
107
|
+
return opened;
|
|
42
108
|
},
|
|
43
109
|
createBranch: async (ws: string, name: string) => {
|
|
44
110
|
calls.push(['createBranch', ws, name]);
|
|
@@ -114,6 +180,8 @@ const post = (url: string, bearer: string, body: unknown = {}) =>
|
|
|
114
180
|
|
|
115
181
|
beforeEach(() => {
|
|
116
182
|
calls = [];
|
|
183
|
+
opened = BIG();
|
|
184
|
+
openFails = undefined;
|
|
117
185
|
});
|
|
118
186
|
afterEach(async () => {
|
|
119
187
|
if (httpServer) await new Promise<void>((r) => httpServer!.close(() => r()));
|
|
@@ -229,7 +297,7 @@ describe('registerWorkflowTools', () => {
|
|
|
229
297
|
expect(calls).toContainEqual(['listBranches', 'existing-ws']);
|
|
230
298
|
});
|
|
231
299
|
|
|
232
|
-
it('open_change_request
|
|
300
|
+
it('open_change_request acts on the SOURCE branch\'s workspace', async () => {
|
|
233
301
|
const base = await start();
|
|
234
302
|
const res = await post(`${base}/api/agent/tools/open_change_request`, writeTok(), {
|
|
235
303
|
sourceBranch: 'me/draft',
|
|
@@ -237,9 +305,6 @@ describe('registerWorkflowTools', () => {
|
|
|
237
305
|
title: 'My change',
|
|
238
306
|
});
|
|
239
307
|
expect(res.status).toBe(200);
|
|
240
|
-
expect(await res.json()).toMatchObject({
|
|
241
|
-
changeRequest: { number: 7, url: 'https://bevel.example.com/change-requests/7' },
|
|
242
|
-
});
|
|
243
308
|
// The workspace must be derived from the SOURCE branch (not a separate
|
|
244
309
|
// `branch` arg) — encodeURIComponent('me/draft'). Regression guard for the
|
|
245
310
|
// `-b undefined` clone bug when the model omitted `branch`.
|
|
@@ -251,6 +316,199 @@ describe('registerWorkflowTools', () => {
|
|
|
251
316
|
]);
|
|
252
317
|
});
|
|
253
318
|
|
|
319
|
+
/**
|
|
320
|
+
* One test per Scenario of the Specification
|
|
321
|
+
* (`hx-change-request-answer-is-small-spec`). The reported failure: opening a
|
|
322
|
+
* request for 27 files answered ~445,000 characters, because the answer was
|
|
323
|
+
* the detail the app's dialog reads, every patch included. It overflowed the
|
|
324
|
+
* tool-result limit, so the agent could not even read the link.
|
|
325
|
+
*/
|
|
326
|
+
describe('what open_change_request answers', () => {
|
|
327
|
+
const open = async (base: string, include?: string[]) => {
|
|
328
|
+
const res = await post(`${base}/api/agent/tools/open_change_request`, writeTok(), {
|
|
329
|
+
sourceBranch: 'me/draft',
|
|
330
|
+
targetBranch: 'target-company-state',
|
|
331
|
+
title: 'My change',
|
|
332
|
+
...(include ? { include } : {}),
|
|
333
|
+
});
|
|
334
|
+
expect(res.status).toBe(200);
|
|
335
|
+
return (await res.json()) as Record<string, unknown>;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
it('answers a summary for 27 new files: the link first, 25 paths of 27, approvers, blockers, no patch', async () => {
|
|
339
|
+
const base = await start();
|
|
340
|
+
const body = await open(base);
|
|
341
|
+
|
|
342
|
+
// The link is the FIRST field: the one thing the agent must hand the
|
|
343
|
+
// user, so it survives a truncation of anything after it.
|
|
344
|
+
expect(Object.keys(body)[0]).toBe('url');
|
|
345
|
+
expect(body).toMatchObject({
|
|
346
|
+
url: 'https://bevel.example.com/change-requests/7',
|
|
347
|
+
number: 7,
|
|
348
|
+
title: 'My change',
|
|
349
|
+
state: 'open',
|
|
350
|
+
sourceBranch: 'me/draft',
|
|
351
|
+
targetBranch: 'target-company-state',
|
|
352
|
+
totalFiles: 27,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const files = body.files as { path: string; change: string }[];
|
|
356
|
+
expect(files).toHaveLength(25);
|
|
357
|
+
expect(new Set(files.map((f) => f.change))).toEqual(new Set(['added']));
|
|
358
|
+
expect(files[0].path).toBe('KnowledgeBase/Engineering/Knowledge/Hexis/Generated-Node-0.md');
|
|
359
|
+
|
|
360
|
+
// Who must approve, per path — what Juan found useful and must stay.
|
|
361
|
+
expect(body.approvals).toHaveLength(25);
|
|
362
|
+
expect((body.approvals as unknown[])[0]).toEqual({
|
|
363
|
+
path: 'KnowledgeBase/Engineering/Knowledge/Hexis/Generated-Node-0.md',
|
|
364
|
+
roles: ['Admin', 'Engineering'],
|
|
365
|
+
users: ['Razvan Radulescu'],
|
|
366
|
+
approved: false,
|
|
367
|
+
inMergeGate: true,
|
|
368
|
+
});
|
|
369
|
+
// What blocks the merge: every reason, for cut paths too.
|
|
370
|
+
expect(body.mergeBlockedReasons).toHaveLength(27);
|
|
371
|
+
|
|
372
|
+
// No patch, and no file content under any other name.
|
|
373
|
+
const json = JSON.stringify(body);
|
|
374
|
+
expect(json).not.toContain('patch');
|
|
375
|
+
expect(json).not.toContain('a line of generated knowledge text');
|
|
376
|
+
expect(files.every((f) => !('patch' in f))).toBe(true);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('keeps the 27-file answer under 10,000 characters', async () => {
|
|
380
|
+
const base = await start();
|
|
381
|
+
const body = await open(base);
|
|
382
|
+
// The detail this was shaped from is the ~445,000-character one.
|
|
383
|
+
expect(JSON.stringify(opened).length).toBeGreaterThan(400_000);
|
|
384
|
+
// 8,918 characters as this fixture stands. The three per-path lists
|
|
385
|
+
// (`files`, `approvals`, `mergeBlockedReasons`) are what fills it, so a
|
|
386
|
+
// field added to any of them eats the margin ~25 times over.
|
|
387
|
+
expect(JSON.stringify(body).length).toBeLessThan(10_000);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('`include: ["all-paths"]` lists all 27 paths, still without patches', async () => {
|
|
391
|
+
const base = await start();
|
|
392
|
+
const body = await open(base, ['all-paths']);
|
|
393
|
+
expect(body.files).toHaveLength(27);
|
|
394
|
+
expect(body.totalFiles).toBe(27);
|
|
395
|
+
expect(body.approvals).toHaveLength(27);
|
|
396
|
+
expect(JSON.stringify(body)).not.toContain('a line of generated knowledge text');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('`include: ["patches"]` gives each listed file its patch', async () => {
|
|
400
|
+
const base = await start();
|
|
401
|
+
const body = await open(base, ['patches']);
|
|
402
|
+
const files = body.files as { path: string; patch?: string }[];
|
|
403
|
+
expect(files).toHaveLength(25);
|
|
404
|
+
expect(files.every((f) => f.patch?.startsWith('@@ -0,0 +1,400 @@'))).toBe(true);
|
|
405
|
+
// Cut to 25 still: `patches` adds the diffs, it does not lift the cut.
|
|
406
|
+
expect(body.totalFiles).toBe(27);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('both together: every path, each with its patch', async () => {
|
|
410
|
+
const base = await start();
|
|
411
|
+
const body = await open(base, ['patches', 'all-paths']);
|
|
412
|
+
const files = body.files as { patch?: string }[];
|
|
413
|
+
expect(files).toHaveLength(27);
|
|
414
|
+
expect(files.every((f) => typeof f.patch === 'string')).toBe(true);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('reads a modification `changed` and a rename `moved`, with its old path', async () => {
|
|
418
|
+
const base = await start();
|
|
419
|
+
opened = detailOf([
|
|
420
|
+
{ path: 'KnowledgeBase/Kept.md', status: 'modified', additions: 2, deletions: 1, patch: '@@ x', isBinary: false, sha: 'b1', rawUrl: 'r1' },
|
|
421
|
+
{ path: 'KnowledgeBase/New-Home.md', previousPath: 'KnowledgeBase/Old-Home.md', status: 'renamed', additions: 0, deletions: 0, isBinary: false, sha: 'b2', rawUrl: 'r2' },
|
|
422
|
+
]);
|
|
423
|
+
const body = await open(base);
|
|
424
|
+
expect(body.files).toEqual([
|
|
425
|
+
{ path: 'KnowledgeBase/Kept.md', change: 'changed' },
|
|
426
|
+
{ path: 'KnowledgeBase/New-Home.md', change: 'moved', previousPath: 'KnowledgeBase/Old-Home.md' },
|
|
427
|
+
]);
|
|
428
|
+
expect(body.totalFiles).toBe(2);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('a deletion reads `deleted`', async () => {
|
|
432
|
+
const base = await start();
|
|
433
|
+
opened = detailOf([
|
|
434
|
+
{ path: 'KnowledgeBase/Gone.md', status: 'removed', additions: 0, deletions: 9, isBinary: false, sha: 'b3', rawUrl: 'r3' },
|
|
435
|
+
]);
|
|
436
|
+
expect((await open(base)).files).toEqual([{ path: 'KnowledgeBase/Gone.md', change: 'deleted' }]);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('says the approvers are unknown rather than implying nobody must approve', async () => {
|
|
440
|
+
const base = await start();
|
|
441
|
+
opened = detailOf([
|
|
442
|
+
{ path: 'KnowledgeBase/Opaque.md', status: 'added', additions: 1, deletions: 0, isBinary: false, sha: 'b4', rawUrl: 'r4' },
|
|
443
|
+
]);
|
|
444
|
+
// No usable access config on the base: the empty approver set means "not
|
|
445
|
+
// known", and nothing may be granted on the strength of that emptiness.
|
|
446
|
+
opened.approvals = [
|
|
447
|
+
{ path: 'KnowledgeBase/Opaque.md', eligibleApprovers: { roles: [], users: [] }, approvedBy: [], eligibilityResolved: false, isApproved: false, inMergeGate: false },
|
|
448
|
+
];
|
|
449
|
+
expect((await open(base)).approvals).toEqual([
|
|
450
|
+
{ path: 'KnowledgeBase/Opaque.md', roles: [], users: [], approved: false, inMergeGate: false, approversUnknown: true },
|
|
451
|
+
]);
|
|
452
|
+
|
|
453
|
+
// A detail that carries no verdict at all reads the same way: absent is
|
|
454
|
+
// not resolved, so the emptiness still means "not known".
|
|
455
|
+
const legacy = { ...(opened.approvals as Record<string, unknown>[])[0] };
|
|
456
|
+
delete legacy.eligibilityResolved;
|
|
457
|
+
opened.approvals = [legacy];
|
|
458
|
+
expect((await open(base)).approvals).toEqual([
|
|
459
|
+
{ path: 'KnowledgeBase/Opaque.md', roles: [], users: [], approved: false, inMergeGate: false, approversUnknown: true },
|
|
460
|
+
]);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('surfaces the `change-request-conflicts` error as before', async () => {
|
|
464
|
+
const base = await start();
|
|
465
|
+
openFails = new ChangeRequestConflictsError('me/draft', 'target-company-state', ['KnowledgeBase/Clash.md']);
|
|
466
|
+
const res = await post(`${base}/api/agent/tools/open_change_request`, writeTok(), {
|
|
467
|
+
sourceBranch: 'me/draft',
|
|
468
|
+
targetBranch: 'target-company-state',
|
|
469
|
+
title: 'My change',
|
|
470
|
+
});
|
|
471
|
+
expect(res.status).toBe(409);
|
|
472
|
+
expect(await res.json()).toMatchObject({
|
|
473
|
+
kind: 'change-request-conflicts',
|
|
474
|
+
sourceBranch: 'me/draft',
|
|
475
|
+
targetBranch: 'target-company-state',
|
|
476
|
+
conflictedPaths: ['KnowledgeBase/Clash.md'],
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* The app's dialog reads the same detail from its own route. Shaping the
|
|
482
|
+
* agent's answer must not touch it — so the detail the service handed back
|
|
483
|
+
* still carries all 27 files and all their patches after the tool answered.
|
|
484
|
+
*/
|
|
485
|
+
it('leaves the detail the app\'s dialog reads exactly as the service returned it', async () => {
|
|
486
|
+
const base = await start();
|
|
487
|
+
const before = JSON.stringify(opened);
|
|
488
|
+
await open(base);
|
|
489
|
+
expect(JSON.stringify(opened)).toBe(before);
|
|
490
|
+
expect((opened.files as unknown[])).toHaveLength(27);
|
|
491
|
+
expect((opened.files as { patch?: string }[]).every((f) => typeof f.patch === 'string')).toBe(true);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('says in its description what the default answer holds and how to ask for more', async () => {
|
|
495
|
+
await start();
|
|
496
|
+
for (const tools of [await registryRef!.listInternal(), await registryRef!.listExternal()]) {
|
|
497
|
+
const def = tools.find((t) => t.name === 'open_change_request')!;
|
|
498
|
+
expect(def.description).toMatch(/SUMMARY/);
|
|
499
|
+
expect(def.description).toMatch(/cut to 25/);
|
|
500
|
+
expect(def.description).toMatch(/No patches and no file content/);
|
|
501
|
+
expect(def.description).toMatch(/include: \["patches"\]/);
|
|
502
|
+
expect(def.description).toMatch(/include: \["all-paths"\]/);
|
|
503
|
+
// `toolDef` wraps a tool's flat inputs under `body`.
|
|
504
|
+
const body = (def.inputs as { properties: { body: { properties: Record<string, { items?: { enum?: string[] }; description?: string }> } } })
|
|
505
|
+
.properties.body;
|
|
506
|
+
expect(body.properties.include.items?.enum).toEqual(['patches', 'all-paths']);
|
|
507
|
+
expect(body.properties.include.description).toMatch(/every changed path instead of the first 25/);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
254
512
|
it('create_branch forks in the BASE branch\'s workspace, never the new draft\'s', async () => {
|
|
255
513
|
const base = await start();
|
|
256
514
|
const res = await post(`${base}/api/agent/tools/create_branch`, writeTok(), {
|
|
@@ -278,9 +536,149 @@ describe('registerWorkflowTools', () => {
|
|
|
278
536
|
expect(calls.filter((c) => c[0] === 'createBranch')).toHaveLength(0);
|
|
279
537
|
});
|
|
280
538
|
|
|
539
|
+
/**
|
|
540
|
+
* These tools address their workspace by ID — `workspaceIdForBranch(branch)`
|
|
541
|
+
* straight from the argument — so they never pass the `getFilesystem` choke
|
|
542
|
+
* point that guards the file tools. A branch-less call used to commit to a
|
|
543
|
+
* workspace id that was literally the string "undefined" — and `create_branch`
|
|
544
|
+
* used to fork from one, cloning a branch of that name and answering "There is
|
|
545
|
+
* no branch named undefined." The guard sits on the mount and is keyed on
|
|
546
|
+
* whether the tool's declared inputs REQUIRE `branch`, so it covers the
|
|
547
|
+
* injected input and a tool's own alike, without any of them remembering it.
|
|
548
|
+
*/
|
|
549
|
+
describe('a branch-less call to a tool that declares `branch`', () => {
|
|
550
|
+
/**
|
|
551
|
+
* Every tool whose DECLARED inputs require `branch`, read off the mounted
|
|
552
|
+
* registry — not a list written by hand. The hand-written list is how the
|
|
553
|
+
* gap that failed Local Testing stayed invisible: it named the three tools
|
|
554
|
+
* that take the injected input and missed `create_branch`, whose `branch`
|
|
555
|
+
* is its own (the fork base) and is just as required. Anything mounted
|
|
556
|
+
* later is swept in without this file being touched.
|
|
557
|
+
*/
|
|
558
|
+
const branchTakingTools = async (): Promise<string[]> =>
|
|
559
|
+
(await registryRef!.listInternal())
|
|
560
|
+
.filter((t) => {
|
|
561
|
+
const body = (t.inputs as { properties?: { body?: { required?: string[] } } }).properties?.body;
|
|
562
|
+
return (body?.required ?? []).includes('branch');
|
|
563
|
+
})
|
|
564
|
+
.map((t) => t.name);
|
|
565
|
+
|
|
566
|
+
// The inputs each tool needs BESIDES `branch`, so the only thing wrong with
|
|
567
|
+
// every call below is the missing branch.
|
|
568
|
+
const bodyFor = (tool: string): Record<string, unknown> =>
|
|
569
|
+
({
|
|
570
|
+
commit_change: { summary: 's' },
|
|
571
|
+
save_file: { path: 'KnowledgeBase/x.md', content: 'c' },
|
|
572
|
+
create_branch: { name: 'alice/new-draft' },
|
|
573
|
+
write_file: { path: 'KnowledgeBase/x.md', content: 'c' },
|
|
574
|
+
})[tool] ?? {};
|
|
575
|
+
|
|
576
|
+
it('400s branch-required on every mounted tool that declares `branch`, and acts on nothing', async () => {
|
|
577
|
+
const base = await start();
|
|
578
|
+
const tools = await branchTakingTools();
|
|
579
|
+
// Both kinds are in here: the injected input and a tool's own required
|
|
580
|
+
// `branch`. `create_branch` is the one the reported bug survived on.
|
|
581
|
+
expect(tools).toContain('commit_change');
|
|
582
|
+
expect(tools).toContain('create_branch');
|
|
583
|
+
expect(tools).toContain('switch_branch');
|
|
584
|
+
|
|
585
|
+
for (const tool of tools) {
|
|
586
|
+
const res = await post(`${base}/api/agent/tools/${tool}`, writeTok(), bodyFor(tool));
|
|
587
|
+
|
|
588
|
+
expect(res.status, `${tool} must 400 on a branch-less call`).toBe(400);
|
|
589
|
+
expect(await res.json(), `${tool} must answer the shared refusal`).toEqual({
|
|
590
|
+
kind: 'branch-required',
|
|
591
|
+
error: '`branch` is required: pass the branch (draft) you are working on.',
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
// No workflow call was made at all — least of all one naming a workspace
|
|
595
|
+
// id built out of the missing value, or a branch forked from it.
|
|
596
|
+
expect(calls).toEqual([]);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* The exact shapes Local Testing drove over MCP against `create_branch`:
|
|
601
|
+
* a missing branch answered 404 `There is no branch named undefined.`
|
|
602
|
+
* (a clone of that "branch" was attempted), an empty one 500 'Invalid
|
|
603
|
+
* workspace ID', and `["main"]` — which `encodeURIComponent` stringifies
|
|
604
|
+
* straight back to `main` — answered 200 and really created and pushed a
|
|
605
|
+
* branch off the default. All four are one refusal now.
|
|
606
|
+
*/
|
|
607
|
+
it('refuses every absent shape on create_branch, and forks from nothing', async () => {
|
|
608
|
+
const base = await start();
|
|
609
|
+
const absent: Array<[string, Record<string, unknown>]> = [
|
|
610
|
+
['missing', { name: 'alice/new-draft' }],
|
|
611
|
+
['empty', { name: 'alice/new-draft', branch: '' }],
|
|
612
|
+
['null', { name: 'alice/new-draft', branch: null }],
|
|
613
|
+
['a number', { name: 'alice/new-draft', branch: 42 }],
|
|
614
|
+
['an array (stringifies to a real branch name)', { name: 'alice/new-draft', branch: ['main'] }],
|
|
615
|
+
['the literal "undefined"', { name: 'alice/new-draft', branch: 'undefined' }],
|
|
616
|
+
['the literal "null"', { name: 'alice/new-draft', branch: 'null' }],
|
|
617
|
+
];
|
|
618
|
+
|
|
619
|
+
for (const [label, body] of absent) {
|
|
620
|
+
const res = await post(`${base}/api/agent/tools/create_branch`, writeTok(), body);
|
|
621
|
+
expect(res.status, `branch ${label} must 400`).toBe(400);
|
|
622
|
+
const answered = await res.json();
|
|
623
|
+
expect(answered.kind, `branch ${label} must answer branch-required`).toBe('branch-required');
|
|
624
|
+
expect(JSON.stringify(answered), `branch ${label} must not echo an absent value`).not.toContain('undefined');
|
|
625
|
+
}
|
|
626
|
+
// Nothing was forked, from any workspace id.
|
|
627
|
+
expect(calls).toEqual([]);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* `create_branch` compares `branch` (the fork base) to `name` and refuses
|
|
632
|
+
* the two being equal, QUOTING the name. A call that sent neither compared
|
|
633
|
+
* undefined to undefined, matched, and answered "not 'undefined', the draft
|
|
634
|
+
* being created" — an absent value presented as a draft name. `name` is
|
|
635
|
+
* refused by name instead, so no such sentence can be produced.
|
|
636
|
+
*/
|
|
637
|
+
it('refuses a missing `name` without quoting it back', async () => {
|
|
638
|
+
const base = await start();
|
|
639
|
+
|
|
640
|
+
const res = await post(`${base}/api/agent/tools/create_branch`, writeTok(), { branch: 'main' });
|
|
641
|
+
|
|
642
|
+
expect(res.status).toBe(400);
|
|
643
|
+
const answered = await res.json();
|
|
644
|
+
expect(answered).toEqual({ kind: 'name-required', error: '`name` is required: pass the name of the draft to create.' });
|
|
645
|
+
expect(JSON.stringify(answered)).not.toContain('undefined');
|
|
646
|
+
expect(calls).toEqual([]);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it('still forks from a named base once both inputs are given', async () => {
|
|
650
|
+
const base = await start();
|
|
651
|
+
// The guards refuse absent inputs, not the tool.
|
|
652
|
+
const res = await post(`${base}/api/agent/tools/create_branch`, writeTok(), { name: 'alice/new-draft', branch: 'main' });
|
|
653
|
+
|
|
654
|
+
expect(res.status).toBe(200);
|
|
655
|
+
expect(calls).toContainEqual(['createBranch', 'main', 'alice/new-draft']);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('refuses the stringified absent values the same way', async () => {
|
|
659
|
+
const base = await start();
|
|
660
|
+
for (const literal of ['undefined', 'null']) {
|
|
661
|
+
const res = await post(`${base}/api/agent/tools/commit_change`, writeTok(), { summary: 's', branch: literal });
|
|
662
|
+
expect(res.status, `branch "${literal}" must 400`).toBe(400);
|
|
663
|
+
expect((await res.json()).kind).toBe('branch-required');
|
|
664
|
+
}
|
|
665
|
+
expect(calls).toEqual([]);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it('leaves a tool that declares no `branch` alone', async () => {
|
|
669
|
+
const base = await start();
|
|
670
|
+
// `list_branches` is repo-global and deliberately takes no branch: the
|
|
671
|
+
// guard must not start demanding one from the tools that opted out.
|
|
672
|
+
expect((await post(`${base}/api/agent/tools/list_branches`, writeTok(), {})).status).toBe(200);
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
|
|
281
676
|
it('works under a connection key too (both surface)', async () => {
|
|
282
677
|
const base = await start();
|
|
283
|
-
|
|
678
|
+
// `branch` is named because every caller must name it — this test is about
|
|
679
|
+
// the connection-key SURFACE, and an external key is precisely the caller
|
|
680
|
+
// that carries no focused branch to fall back on.
|
|
681
|
+
expect((await post(`${base}/api/agent/tools/commit_change`, 'bevel_key', { summary: 's', branch: WS })).status).toBe(200);
|
|
284
682
|
});
|
|
285
683
|
|
|
286
684
|
it('switch_branch validates + pre-warms + emits a user-scoped event (internal-only)', async () => {
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChangeRequestDetail,
|
|
3
|
+
ChangeRequestState,
|
|
4
|
+
ChangedFile,
|
|
5
|
+
FileApproval,
|
|
6
|
+
PrFileStatus,
|
|
7
|
+
} from '@bevel-software/platform-shared';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* What `open_change_request` answers, and why it is not the detail the app's
|
|
11
|
+
* dialog reads.
|
|
12
|
+
*
|
|
13
|
+
* The dialog renders every file's diff, so its detail carries every patch. An
|
|
14
|
+
* agent that opens a change request needs none of that: it needs the link to
|
|
15
|
+
* hand the user, and enough of the request to confirm it carries the files it
|
|
16
|
+
* meant to propose and to say what the request is now waiting on. Handing it
|
|
17
|
+
* the dialog's detail cost ~445,000 characters for 27 files of 15 KB — past
|
|
18
|
+
* every tool-result limit, so the one field the agent actually needed (the
|
|
19
|
+
* link) could not be read at all.
|
|
20
|
+
*
|
|
21
|
+
* So this is a purpose-made answer: the link first, the request's identity,
|
|
22
|
+
* who must approve each path, what blocks the merge verbatim from the detail,
|
|
23
|
+
* and the changed paths with their kind of change — no patch, no file content.
|
|
24
|
+
* An agent that wants more asks for it by name through `include`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** What `include` accepts. */
|
|
28
|
+
export type ChangeRequestInclude = 'patches' | 'all-paths';
|
|
29
|
+
|
|
30
|
+
/** The two `include` values, for the tool's input schema and its validation. */
|
|
31
|
+
export const CHANGE_REQUEST_INCLUDES: readonly ChangeRequestInclude[] = ['patches', 'all-paths'];
|
|
32
|
+
|
|
33
|
+
/** How many paths the answer lists before it cuts; `all-paths` lifts the cut. */
|
|
34
|
+
export const CHANGE_REQUEST_PATH_LIMIT = 25;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The kind of change to one path, in the four words the answer speaks. Git's
|
|
38
|
+
* own statuses are finer (`modified` vs `changed`, `renamed` vs `copied`) in
|
|
39
|
+
* ways nothing downstream acts on.
|
|
40
|
+
*/
|
|
41
|
+
export type ChangeKind = 'added' | 'changed' | 'deleted' | 'moved';
|
|
42
|
+
|
|
43
|
+
/** One changed path. `previousPath` is where a `moved` one came from. */
|
|
44
|
+
export interface ChangeRequestSummaryFile {
|
|
45
|
+
path: string;
|
|
46
|
+
change: ChangeKind;
|
|
47
|
+
previousPath?: string;
|
|
48
|
+
/** Unified diff. Only with `include: ["patches"]`, and absent for a binary or oversized file. */
|
|
49
|
+
patch?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Who must approve one path, at the same per-path granularity the detail
|
|
54
|
+
* carries (the access tree resolves a role per file or per folder; this says
|
|
55
|
+
* what it resolved to for this file).
|
|
56
|
+
*
|
|
57
|
+
* The detail's own entry is the dialog's: it also carries each approver's
|
|
58
|
+
* email, the viewer's own eligibility, and every approval's timestamp and
|
|
59
|
+
* staleness. Repeated over 25 files that is ~4,500 characters an agent cannot
|
|
60
|
+
* act on — it may not approve anything — and it is what would push the
|
|
61
|
+
* 27-file answer back over the limit this ticket exists to get under. So the
|
|
62
|
+
* answer keeps what "who must approve" means and nothing else.
|
|
63
|
+
*/
|
|
64
|
+
export interface ChangeRequestApprovers {
|
|
65
|
+
path: string;
|
|
66
|
+
/** Roles that confer approval here. `everyone` means any signed-in approver. */
|
|
67
|
+
roles: string[];
|
|
68
|
+
/** Approvers named directly, by display name. */
|
|
69
|
+
users: string[];
|
|
70
|
+
/** True once an eligible approver has approved at the request's current head. */
|
|
71
|
+
approved: boolean;
|
|
72
|
+
/** False when the merge gate does not bind this path — then nobody has to approve it. */
|
|
73
|
+
inMergeGate: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Present when the access tree could not be resolved for this path — which
|
|
76
|
+
* includes a detail that carries no verdict at all. Then empty
|
|
77
|
+
* `roles`/`users` means "not known", NOT "nobody has to approve".
|
|
78
|
+
*/
|
|
79
|
+
approversUnknown?: true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ChangeRequestSummary {
|
|
83
|
+
/** First field on purpose: it is the one an agent must hand the user. */
|
|
84
|
+
url: string;
|
|
85
|
+
urlNote?: string;
|
|
86
|
+
number: number;
|
|
87
|
+
title: string;
|
|
88
|
+
state: ChangeRequestState;
|
|
89
|
+
sourceBranch: string;
|
|
90
|
+
targetBranch: string;
|
|
91
|
+
/** Who must approve, one entry per listed path. */
|
|
92
|
+
approvals: ChangeRequestApprovers[];
|
|
93
|
+
/** What blocks the merge — verbatim from the detail. Empty when nothing does. */
|
|
94
|
+
mergeBlockedReasons: string[];
|
|
95
|
+
/** The changed paths, cut to {@link CHANGE_REQUEST_PATH_LIMIT} unless `all-paths` was asked for. */
|
|
96
|
+
files: ChangeRequestSummaryFile[];
|
|
97
|
+
/** How many files the request changes in total, cut or not. */
|
|
98
|
+
totalFiles: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A copy and a rename both carry `previousPath`, and the one thing a reader
|
|
103
|
+
* does with either is look at where the text came from — so both read `moved`.
|
|
104
|
+
* `unchanged` is a file the request lists without changing (git reports it on
|
|
105
|
+
* a mode-only change); `changed` is the honest word for it among these four.
|
|
106
|
+
*/
|
|
107
|
+
function changeKindOf(status: PrFileStatus | undefined): ChangeKind {
|
|
108
|
+
switch (status) {
|
|
109
|
+
case 'added':
|
|
110
|
+
return 'added';
|
|
111
|
+
case 'removed':
|
|
112
|
+
return 'deleted';
|
|
113
|
+
case 'renamed':
|
|
114
|
+
case 'copied':
|
|
115
|
+
return 'moved';
|
|
116
|
+
default:
|
|
117
|
+
return 'changed';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function approversOf(approval: FileApproval): ChangeRequestApprovers {
|
|
122
|
+
const eligible = approval.eligibleApprovers ?? { roles: [], users: [] };
|
|
123
|
+
return {
|
|
124
|
+
path: approval.path,
|
|
125
|
+
roles: eligible.roles ?? [],
|
|
126
|
+
users: (eligible.users ?? []).map((u) => u.name),
|
|
127
|
+
approved: approval.isApproved,
|
|
128
|
+
inMergeGate: approval.inMergeGate,
|
|
129
|
+
// Anything but an explicit `true` is unresolved — a detail built before
|
|
130
|
+
// the flag existed carries no flag at all, and reading that as "resolved"
|
|
131
|
+
// would turn an empty approver set into "nobody must approve". Same
|
|
132
|
+
// fail-closed reading as `computeViewerCanUpdate`.
|
|
133
|
+
...(approval.eligibilityResolved !== true ? { approversUnknown: true as const } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function summaryFileOf(file: ChangedFile, withPatch: boolean): ChangeRequestSummaryFile {
|
|
138
|
+
return {
|
|
139
|
+
path: file.path,
|
|
140
|
+
change: changeKindOf(file.status),
|
|
141
|
+
...(file.previousPath ? { previousPath: file.previousPath } : {}),
|
|
142
|
+
...(withPatch && file.patch ? { patch: file.patch } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Shape the tool's answer out of the detail the service already returns. Pure:
|
|
148
|
+
* the service and the app's routes are untouched, and the detail they serve is
|
|
149
|
+
* exactly what it was.
|
|
150
|
+
*
|
|
151
|
+
* `approvals` is narrowed to the paths the answer lists, because the detail's
|
|
152
|
+
* contract is one entry per listed file. Nothing about the merge gate is lost
|
|
153
|
+
* by that: `mergeBlockedReasons` names every missing approval, cut or not.
|
|
154
|
+
*/
|
|
155
|
+
export function summarizeChangeRequest(
|
|
156
|
+
detail: ChangeRequestDetail,
|
|
157
|
+
include: readonly ChangeRequestInclude[] = [],
|
|
158
|
+
): ChangeRequestSummary {
|
|
159
|
+
const all = detail.files ?? [];
|
|
160
|
+
const withPatch = include.includes('patches');
|
|
161
|
+
const listed = include.includes('all-paths') ? all : all.slice(0, CHANGE_REQUEST_PATH_LIMIT);
|
|
162
|
+
const listedPaths = new Set(listed.map((f) => f.path));
|
|
163
|
+
return {
|
|
164
|
+
url: detail.url,
|
|
165
|
+
...(detail.urlNote ? { urlNote: detail.urlNote } : {}),
|
|
166
|
+
number: detail.number,
|
|
167
|
+
title: detail.title,
|
|
168
|
+
state: detail.state,
|
|
169
|
+
sourceBranch: detail.branch,
|
|
170
|
+
targetBranch: detail.base,
|
|
171
|
+
approvals: (detail.approvals ?? []).filter((a) => listedPaths.has(a.path)).map(approversOf),
|
|
172
|
+
mergeBlockedReasons: detail.mergeBlockedReasons ?? [],
|
|
173
|
+
files: listed.map((f) => summaryFileOf(f, withPatch)),
|
|
174
|
+
totalFiles: all.length,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The `include` values off a tool call's arguments; anything else is ignored. */
|
|
179
|
+
export function parseInclude(raw: unknown): ChangeRequestInclude[] {
|
|
180
|
+
if (!Array.isArray(raw)) return [];
|
|
181
|
+
return CHANGE_REQUEST_INCLUDES.filter((v) => raw.includes(v));
|
|
182
|
+
}
|