@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.
Files changed (434) hide show
  1. package/dist/core/core-ports.d.ts +11 -1
  2. package/dist/core/core-ports.d.ts.map +1 -1
  3. package/dist/core/core-ports.js.map +1 -1
  4. package/dist/core/create-core-server.d.ts +3 -3
  5. package/dist/core/create-core-server.d.ts.map +1 -1
  6. package/dist/core/create-core-server.js +62 -13
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +22 -3
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +158 -23
  11. package/dist/core/create-core-services.js.map +1 -1
  12. package/dist/core/lifecycle.d.ts +34 -1
  13. package/dist/core/lifecycle.d.ts.map +1 -1
  14. package/dist/core/lifecycle.js +89 -13
  15. package/dist/core/lifecycle.js.map +1 -1
  16. package/dist/core-config.d.ts +0 -12
  17. package/dist/core-config.d.ts.map +1 -1
  18. package/dist/core-config.js +11 -13
  19. package/dist/core-config.js.map +1 -1
  20. package/dist/index.d.ts +3 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/modules/access/access-control.interface.d.ts +42 -12
  25. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  26. package/dist/modules/access/access-control.service.d.ts +67 -10
  27. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  28. package/dist/modules/access/access-control.service.js +214 -35
  29. package/dist/modules/access/access-control.service.js.map +1 -1
  30. package/dist/modules/access/access-requests.contract.d.ts +75 -0
  31. package/dist/modules/access/access-requests.contract.d.ts.map +1 -0
  32. package/dist/modules/access/access-requests.contract.js +20 -0
  33. package/dist/modules/access/access-requests.contract.js.map +1 -0
  34. package/dist/modules/access/access-requests.routes.d.ts +35 -0
  35. package/dist/modules/access/access-requests.routes.d.ts.map +1 -0
  36. package/dist/modules/access/access-requests.routes.js +237 -0
  37. package/dist/modules/access/access-requests.routes.js.map +1 -0
  38. package/dist/modules/access/access-requests.service.d.ts +123 -0
  39. package/dist/modules/access/access-requests.service.d.ts.map +1 -0
  40. package/dist/modules/access/access-requests.service.js +337 -0
  41. package/dist/modules/access/access-requests.service.js.map +1 -0
  42. package/dist/modules/access/access.routes.d.ts.map +1 -1
  43. package/dist/modules/access/access.routes.js +13 -15
  44. package/dist/modules/access/access.routes.js.map +1 -1
  45. package/dist/modules/access-model/access-grammar.d.ts +12 -0
  46. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  47. package/dist/modules/access-model/access-grammar.js +24 -0
  48. package/dist/modules/access-model/access-grammar.js.map +1 -1
  49. package/dist/modules/auth/account-admission.d.ts +58 -7
  50. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  51. package/dist/modules/auth/account-admission.js +44 -1
  52. package/dist/modules/auth/account-admission.js.map +1 -1
  53. package/dist/modules/auth/account.routes.d.ts +1 -1
  54. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  55. package/dist/modules/auth/account.routes.js +61 -5
  56. package/dist/modules/auth/account.routes.js.map +1 -1
  57. package/dist/modules/auth/auth.middleware.d.ts +1 -1
  58. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  59. package/dist/modules/auth/auth.middleware.js +18 -7
  60. package/dist/modules/auth/auth.middleware.js.map +1 -1
  61. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  62. package/dist/modules/auth/auth.routes.js +8 -0
  63. package/dist/modules/auth/auth.routes.js.map +1 -1
  64. package/dist/modules/auth/auth.service.d.ts +96 -3
  65. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  66. package/dist/modules/auth/auth.service.js +187 -9
  67. package/dist/modules/auth/auth.service.js.map +1 -1
  68. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  69. package/dist/modules/auth/oidc-auth-provider.js +7 -2
  70. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  71. package/dist/modules/database/core-schema.d.ts +41 -24
  72. package/dist/modules/database/core-schema.d.ts.map +1 -1
  73. package/dist/modules/database/core-schema.js +35 -27
  74. package/dist/modules/database/core-schema.js.map +1 -1
  75. package/dist/modules/github-app/github-app.client.d.ts +101 -0
  76. package/dist/modules/github-app/github-app.client.d.ts.map +1 -0
  77. package/dist/modules/github-app/github-app.client.js +222 -0
  78. package/dist/modules/github-app/github-app.client.js.map +1 -0
  79. package/dist/modules/github-app/github-app.connection.d.ts +102 -0
  80. package/dist/modules/github-app/github-app.connection.d.ts.map +1 -0
  81. package/dist/modules/github-app/github-app.connection.js +185 -0
  82. package/dist/modules/github-app/github-app.connection.js.map +1 -0
  83. package/dist/modules/github-app/github-app.routes.d.ts +59 -0
  84. package/dist/modules/github-app/github-app.routes.d.ts.map +1 -0
  85. package/dist/modules/github-app/github-app.routes.js +323 -0
  86. package/dist/modules/github-app/github-app.routes.js.map +1 -0
  87. package/dist/modules/github-app/index.d.ts +4 -0
  88. package/dist/modules/github-app/index.d.ts.map +1 -0
  89. package/dist/modules/github-app/index.js +4 -0
  90. package/dist/modules/github-app/index.js.map +1 -0
  91. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  92. package/dist/modules/kb-fs/branch-name.js +12 -2
  93. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  94. package/dist/modules/kb-fs/remote-url.d.ts +22 -0
  95. package/dist/modules/kb-fs/remote-url.d.ts.map +1 -0
  96. package/dist/modules/kb-fs/remote-url.js +35 -0
  97. package/dist/modules/kb-fs/remote-url.js.map +1 -0
  98. package/dist/modules/kb-fs/repo-path.d.ts +11 -16
  99. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  100. package/dist/modules/kb-fs/repo-path.js +79 -0
  101. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  102. package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
  103. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  104. package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
  105. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  106. package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
  107. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
  108. package/dist/modules/kb-sync/sync-auth.js +1 -1
  109. package/dist/modules/kb-sync/sync-auth.js.map +1 -1
  110. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  111. package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
  112. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  113. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  114. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
  115. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  116. package/dist/modules/plugins/join-proposals.d.ts +17 -5
  117. package/dist/modules/plugins/join-proposals.d.ts.map +1 -1
  118. package/dist/modules/plugins/join-proposals.js +76 -22
  119. package/dist/modules/plugins/join-proposals.js.map +1 -1
  120. package/dist/modules/plugins/join-requests.service.d.ts +111 -18
  121. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  122. package/dist/modules/plugins/join-requests.service.js +173 -29
  123. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  124. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  125. package/dist/modules/plugins/plugins.routes.js +3 -2
  126. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  127. package/dist/modules/settings/deployment-settings.service.d.ts +22 -1
  128. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  129. package/dist/modules/settings/deployment-settings.service.js +101 -7
  130. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  131. package/dist/modules/settings/managed-repository.d.ts +43 -0
  132. package/dist/modules/settings/managed-repository.d.ts.map +1 -0
  133. package/dist/modules/settings/managed-repository.js +60 -0
  134. package/dist/modules/settings/managed-repository.js.map +1 -0
  135. package/dist/modules/settings/repository-source.d.ts +128 -0
  136. package/dist/modules/settings/repository-source.d.ts.map +1 -0
  137. package/dist/modules/settings/repository-source.js +150 -0
  138. package/dist/modules/settings/repository-source.js.map +1 -0
  139. package/dist/modules/settings/setup.routes.d.ts +78 -4
  140. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  141. package/dist/modules/settings/setup.routes.js +379 -17
  142. package/dist/modules/settings/setup.routes.js.map +1 -1
  143. package/dist/modules/skills/skill-access-requests.routes.d.ts +6 -8
  144. package/dist/modules/skills/skill-access-requests.routes.d.ts.map +1 -1
  145. package/dist/modules/skills/skill-access-requests.routes.js +24 -63
  146. package/dist/modules/skills/skill-access-requests.routes.js.map +1 -1
  147. package/dist/modules/skills/skills.contract.d.ts +90 -18
  148. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  149. package/dist/modules/skills/skills.contract.js +4 -1
  150. package/dist/modules/skills/skills.contract.js.map +1 -1
  151. package/dist/modules/skills/skills.service.d.ts +98 -9
  152. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  153. package/dist/modules/skills/skills.service.js +239 -37
  154. package/dist/modules/skills/skills.service.js.map +1 -1
  155. package/dist/modules/skills/skills.tools.d.ts +7 -0
  156. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  157. package/dist/modules/skills/skills.tools.js +71 -7
  158. package/dist/modules/skills/skills.tools.js.map +1 -1
  159. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  160. package/dist/modules/tool-auth/external-api-key.service.js +4 -2
  161. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  162. package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
  163. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
  164. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  165. package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
  166. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  167. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  168. package/dist/modules/tool-helpers/tool-context.js +15 -0
  169. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  170. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
  171. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  172. package/dist/modules/tool-helpers/tool-handler.js +25 -4
  173. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  174. package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
  175. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  176. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  177. package/dist/modules/tool-helpers/validate-token.js +1 -1
  178. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  179. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
  180. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
  181. package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
  182. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
  183. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  184. package/dist/modules/workflow/agent-tools/workflow.tools.js +113 -37
  185. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  186. package/dist/modules/workflow/file-lock.service.d.ts +24 -0
  187. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  188. package/dist/modules/workflow/file-lock.service.js +30 -0
  189. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  190. package/dist/modules/workflow/git/git.service.d.ts +72 -1
  191. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  192. package/dist/modules/workflow/git/git.service.js +170 -6
  193. package/dist/modules/workflow/git/git.service.js.map +1 -1
  194. package/dist/modules/workflow/git/node-git-runner.d.ts.map +1 -1
  195. package/dist/modules/workflow/git/node-git-runner.js +5 -0
  196. package/dist/modules/workflow/git/node-git-runner.js.map +1 -1
  197. package/dist/modules/workflow/git/pull-request.service.d.ts +45 -0
  198. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  199. package/dist/modules/workflow/git/pull-request.service.js +99 -0
  200. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  201. package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
  202. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  203. package/dist/modules/workflow/pending-commits.service.js +55 -0
  204. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  205. package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
  206. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
  207. package/dist/modules/workflow/workflow-hooks.js +16 -1
  208. package/dist/modules/workflow/workflow-hooks.js.map +1 -1
  209. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  210. package/dist/modules/workflow/workflow.routes.js +3 -1
  211. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  212. package/dist/modules/workflow/workflow.service.d.ts +94 -13
  213. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  214. package/dist/modules/workflow/workflow.service.js +258 -51
  215. package/dist/modules/workflow/workflow.service.js.map +1 -1
  216. package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
  217. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
  218. package/dist/modules/workspace/agent-access.gate.js +123 -0
  219. package/dist/modules/workspace/agent-access.gate.js.map +1 -0
  220. package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
  221. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  222. package/dist/modules/workspace/routine-write-policy.js +5 -6
  223. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  224. package/dist/modules/workspace/session-sink.d.ts +5 -5
  225. package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
  226. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
  227. package/dist/modules/workspace/set-aside-clone.js +92 -0
  228. package/dist/modules/workspace/set-aside-clone.js.map +1 -0
  229. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +117 -1
  230. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  231. package/dist/modules/workspace/startup/kb-startup-runner.js +190 -1
  232. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  233. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  234. package/dist/modules/workspace/workspace.routes.js +92 -3
  235. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  236. package/dist/modules/workspace/workspace.service.d.ts +115 -5
  237. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  238. package/dist/modules/workspace/workspace.service.js +272 -26
  239. package/dist/modules/workspace/workspace.service.js.map +1 -1
  240. package/dist/modules/workspace/workspace.tools.d.ts +2 -2
  241. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  242. package/dist/modules/workspace/workspace.tools.js +337 -117
  243. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  244. package/dist/modules/write-access/write-access.d.ts +60 -0
  245. package/dist/modules/write-access/write-access.d.ts.map +1 -0
  246. package/dist/modules/write-access/write-access.js +129 -0
  247. package/dist/modules/write-access/write-access.js.map +1 -0
  248. package/dist/shared/domain-errors.d.ts +64 -0
  249. package/dist/shared/domain-errors.d.ts.map +1 -1
  250. package/dist/shared/domain-errors.js +83 -0
  251. package/dist/shared/domain-errors.js.map +1 -1
  252. package/dist/shared/git.contract.d.ts +30 -0
  253. package/dist/shared/git.contract.d.ts.map +1 -1
  254. package/dist/shared/git.contract.js +26 -0
  255. package/dist/shared/git.contract.js.map +1 -1
  256. package/dist/tenancy/static-tenant-source.d.ts +0 -1
  257. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  258. package/dist/tenancy/static-tenant-source.js +0 -2
  259. package/dist/tenancy/static-tenant-source.js.map +1 -1
  260. package/migrations/0014_change_request_closed_reason.sql +1 -0
  261. package/migrations/0015_account_deactivation.sql +3 -0
  262. package/migrations/meta/0014_snapshot.json +2265 -0
  263. package/migrations/meta/0015_snapshot.json +2271 -0
  264. package/migrations/meta/_journal.json +14 -0
  265. package/package.json +3 -3
  266. package/src/__tests__/kb-layout-config.test.ts +0 -2
  267. package/src/__tests__/retired-settings.test.ts +96 -0
  268. package/src/core/__tests__/gated-boot.test.ts +132 -0
  269. package/src/core/__tests__/lifecycle.test.ts +170 -1
  270. package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
  271. package/src/core/core-ports.ts +11 -1
  272. package/src/core/create-core-server.ts +69 -15
  273. package/src/core/create-core-services.ts +190 -27
  274. package/src/core/lifecycle.ts +98 -13
  275. package/src/core-config.ts +13 -14
  276. package/src/index.ts +12 -1
  277. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
  278. package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
  279. package/src/modules/access/__tests__/access-requests.recut.test.ts +134 -0
  280. package/src/modules/access/__tests__/access-requests.routes.test.ts +610 -0
  281. package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
  282. package/src/modules/access/access-control.interface.ts +43 -12
  283. package/src/modules/access/access-control.service.ts +234 -36
  284. package/src/modules/access/access-requests.contract.ts +93 -0
  285. package/src/modules/access/access-requests.routes.ts +286 -0
  286. package/src/modules/access/access-requests.service.ts +420 -0
  287. package/src/modules/access/access.routes.ts +13 -15
  288. package/src/modules/access-model/access-grammar.ts +22 -0
  289. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +3 -1
  290. package/src/modules/auth/__tests__/account-deactivation.test.ts +252 -0
  291. package/src/modules/auth/__tests__/account.routes.test.ts +81 -3
  292. package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
  293. package/src/modules/auth/__tests__/auth.service.test.ts +39 -1
  294. package/src/modules/auth/account-admission.ts +78 -9
  295. package/src/modules/auth/account.routes.ts +63 -7
  296. package/src/modules/auth/auth.middleware.ts +19 -8
  297. package/src/modules/auth/auth.routes.ts +8 -0
  298. package/src/modules/auth/auth.service.ts +207 -7
  299. package/src/modules/auth/oidc-auth-provider.ts +7 -2
  300. package/src/modules/database/core-schema.ts +35 -27
  301. package/src/modules/github-app/__tests__/github-app.test.ts +848 -0
  302. package/src/modules/github-app/github-app.client.ts +268 -0
  303. package/src/modules/github-app/github-app.connection.ts +204 -0
  304. package/src/modules/github-app/github-app.routes.ts +359 -0
  305. package/src/modules/github-app/index.ts +19 -0
  306. package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
  307. package/src/modules/kb-fs/__tests__/remote-url.test.ts +38 -0
  308. package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
  309. package/src/modules/kb-fs/branch-name.ts +14 -1
  310. package/src/modules/kb-fs/remote-url.ts +35 -0
  311. package/src/modules/kb-fs/repo-path.ts +85 -0
  312. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
  313. package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
  314. package/src/modules/kb-sync/sync-auth.ts +2 -2
  315. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
  316. package/src/modules/mcp/__tests__/mcp.service.test.ts +68 -6
  317. package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
  318. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
  319. package/src/modules/plugins/__tests__/join-proposals.test.ts +100 -19
  320. package/src/modules/plugins/__tests__/join-requests.service.test.ts +27 -11
  321. package/src/modules/plugins/__tests__/join-requests.settlement.test.ts +371 -0
  322. package/src/modules/plugins/__tests__/plugins.routes.test.ts +1 -1
  323. package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
  324. package/src/modules/plugins/join-proposals.ts +87 -19
  325. package/src/modules/plugins/join-requests.service.ts +199 -39
  326. package/src/modules/plugins/plugins.routes.ts +3 -2
  327. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
  328. package/src/modules/settings/__tests__/repository-source.test.ts +191 -0
  329. package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +386 -0
  330. package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +299 -0
  331. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +236 -0
  332. package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
  333. package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
  334. package/src/modules/settings/deployment-settings.service.ts +117 -6
  335. package/src/modules/settings/managed-repository.ts +68 -0
  336. package/src/modules/settings/repository-source.ts +197 -0
  337. package/src/modules/settings/setup.routes.ts +463 -15
  338. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
  339. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
  340. package/src/modules/skills/__tests__/skill-access-requests.routes.test.ts +5 -1
  341. package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
  342. package/src/modules/skills/skill-access-requests.routes.ts +25 -75
  343. package/src/modules/skills/skills.contract.ts +91 -18
  344. package/src/modules/skills/skills.service.ts +278 -41
  345. package/src/modules/skills/skills.tools.ts +80 -8
  346. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
  347. package/src/modules/tool-auth/external-api-key.service.ts +4 -2
  348. package/src/modules/tool-auth/internal-token.service.ts +3 -3
  349. package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
  350. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
  351. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
  352. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
  353. package/src/modules/tool-helpers/tool-context.ts +15 -0
  354. package/src/modules/tool-helpers/tool-handler.ts +24 -4
  355. package/src/modules/tool-helpers/tool.contract.ts +3 -2
  356. package/src/modules/tool-helpers/validate-token.ts +1 -1
  357. package/src/modules/workflow/__tests__/apply-failure.test.ts +9 -1
  358. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
  359. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
  360. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +159 -8
  361. package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
  362. package/src/modules/workflow/__tests__/workflow.service.update-from-target.test.ts +146 -40
  363. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +405 -7
  364. package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
  365. package/src/modules/workflow/agent-tools/workflow.tools.ts +135 -38
  366. package/src/modules/workflow/file-lock.service.ts +31 -0
  367. package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
  368. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +121 -0
  369. package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +145 -2
  370. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-delete.test.ts +166 -0
  371. package/src/modules/workflow/git/git.service.ts +193 -4
  372. package/src/modules/workflow/git/node-git-runner.ts +6 -0
  373. package/src/modules/workflow/git/pull-request.service.ts +119 -0
  374. package/src/modules/workflow/pending-commits.service.ts +59 -1
  375. package/src/modules/workflow/workflow-hooks.ts +64 -26
  376. package/src/modules/workflow/workflow.routes.ts +3 -1
  377. package/src/modules/workflow/workflow.service.ts +290 -55
  378. package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
  379. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +384 -0
  380. package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
  381. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
  382. package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
  383. package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
  384. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
  385. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
  386. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +326 -0
  387. package/src/modules/workspace/__tests__/workspace.service.test.ts +20 -9
  388. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +43 -0
  389. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -3
  390. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +235 -10
  391. package/src/modules/workspace/__tests__/workspace.tools.test.ts +720 -17
  392. package/src/modules/workspace/agent-access.gate.ts +164 -0
  393. package/src/modules/workspace/routine-write-policy.ts +5 -6
  394. package/src/modules/workspace/session-sink.ts +5 -5
  395. package/src/modules/workspace/set-aside-clone.ts +96 -0
  396. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +297 -0
  397. package/src/modules/workspace/startup/kb-startup-runner.ts +242 -2
  398. package/src/modules/workspace/workspace.routes.ts +95 -3
  399. package/src/modules/workspace/workspace.service.ts +273 -25
  400. package/src/modules/workspace/workspace.tools.ts +373 -119
  401. package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
  402. package/src/modules/write-access/write-access.ts +153 -0
  403. package/src/shared/domain-errors.ts +89 -0
  404. package/src/shared/git.contract.ts +37 -0
  405. package/src/tenancy/__tests__/static-tenant-source.test.ts +0 -1
  406. package/src/tenancy/static-tenant-source.ts +0 -3
  407. package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
  408. package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
  409. package/dist/modules/workflow/session-ontology.policy.js +0 -62
  410. package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
  411. package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
  412. package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
  413. package/dist/modules/workflow/session-ontology.service.js +0 -147
  414. package/dist/modules/workflow/session-ontology.service.js.map +0 -1
  415. package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
  416. package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
  417. package/dist/modules/workspace/session-ontology.gate.js +0 -161
  418. package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
  419. package/dist/shared/kb-layout.d.ts +0 -39
  420. package/dist/shared/kb-layout.d.ts.map +0 -1
  421. package/dist/shared/kb-layout.js +0 -103
  422. package/dist/shared/kb-layout.js.map +0 -1
  423. package/dist/shared/kb-layout.test.d.ts +0 -2
  424. package/dist/shared/kb-layout.test.d.ts.map +0 -1
  425. package/dist/shared/kb-layout.test.js +0 -75
  426. package/dist/shared/kb-layout.test.js.map +0 -1
  427. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
  428. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
  429. package/src/modules/workflow/session-ontology.policy.ts +0 -70
  430. package/src/modules/workflow/session-ontology.service.ts +0 -183
  431. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
  432. package/src/modules/workspace/session-ontology.gate.ts +0 -191
  433. package/src/shared/kb-layout.test.ts +0 -98
  434. package/src/shared/kb-layout.ts +0 -102
@@ -5,9 +5,16 @@ import { toolDef, withBranchInput } from '../../tool-helpers/tool-def.js';
5
5
  import type { ToolHandlerFactory } from '../../tool-helpers/tool-handler.js';
6
6
  import { requireInternalSource } from '../../tool-auth/tool-auth.middleware.js';
7
7
  import type { KbContext } from '../../../shared/kb-context.js';
8
+ import { assertBranchProvided } from '../../../shared/domain-errors.js';
8
9
  import { workspaceIdForBranch } from '../../../shared/workspace-id.js';
9
10
  import { assertInsideRepo, normalizePathArgs } from '../../kb-fs/repo-path.js';
10
11
  import { RETIRED_TOOL_MESSAGES } from '@bevel-software/platform-mcp-core';
12
+ import {
13
+ CHANGE_REQUEST_INCLUDES,
14
+ CHANGE_REQUEST_PATH_LIMIT,
15
+ parseInclude,
16
+ summarizeChangeRequest,
17
+ } from './change-request-summary.js';
11
18
 
12
19
  /** `knowledge-base/KnowledgeBase/x.md` → `KnowledgeBase/x.md`; anything else unchanged. */
13
20
  function stripKbDir(path: string, kbDirName: string): string {
@@ -46,20 +53,31 @@ const commitSchema: JsonSchema = {
46
53
  required: ['authorName', 'authorEmail', 'sha', 'subject', 'committedAt'],
47
54
  };
48
55
 
49
- const changeRequestFileSchema: JsonSchema = {
56
+ const changeRequestSummaryFileSchema: JsonSchema = {
50
57
  type: 'object',
51
58
  properties: {
52
59
  path: { type: 'string' },
53
- previousPath: { type: 'string', description: 'Set for renames/copies.' },
54
- status: { type: 'string', description: '`added` | `modified` | `removed` | `renamed` | `copied` | `changed` | `unchanged`.' },
55
- additions: { type: 'integer' },
56
- deletions: { type: 'integer' },
57
- patch: { type: 'string', description: 'Unified diff; absent for binary or oversized files.' },
58
- isBinary: { type: 'boolean' },
59
- sha: { type: 'string', description: 'Blob SHA at the change-request head.' },
60
- rawUrl: { type: 'string' },
60
+ change: { type: 'string', description: '`added` | `changed` | `deleted` | `moved`.' },
61
+ previousPath: { type: 'string', description: 'Where a `moved` file came from.' },
62
+ patch: {
63
+ type: 'string',
64
+ description: 'Unified diff. Only with `include: ["patches"]`, and absent for a binary or oversized file.',
65
+ },
66
+ },
67
+ required: ['path', 'change'],
68
+ };
69
+
70
+ const changeRequestApproversSchema: JsonSchema = {
71
+ type: 'object',
72
+ properties: {
73
+ path: { type: 'string' },
74
+ roles: { type: 'array', items: { type: 'string' }, description: 'Roles that confer approval here; `everyone` means any signed-in approver.' },
75
+ users: { type: 'array', items: { type: 'string' }, description: 'Approvers named directly, by display name.' },
76
+ approved: { type: 'boolean', description: 'True once an eligible approver has approved at the current head.' },
77
+ inMergeGate: { type: 'boolean', description: 'False when the merge gate does not bind this path — then nobody has to approve it.' },
78
+ approversUnknown: { type: 'boolean', description: 'Present when the access tree could not be resolved: empty `roles`/`users` means unknown, not nobody.' },
61
79
  },
62
- required: ['path', 'status', 'additions', 'deletions', 'isBinary', 'sha', 'rawUrl'],
80
+ required: ['path', 'roles', 'users', 'approved', 'inMergeGate'],
63
81
  };
64
82
 
65
83
  const commentSchema: JsonSchema = {
@@ -98,24 +116,43 @@ function linkOf(cr: { number: number; url: string; urlNote?: string }): { number
98
116
  return { number: cr.number, url: cr.url, ...(cr.urlNote ? { urlNote: cr.urlNote } : {}) };
99
117
  }
100
118
 
101
- const changeRequestDetailSchema: JsonSchema = {
119
+ /**
120
+ * What `open_change_request` answers: a summary, `url` first, with no patch and
121
+ * no file content. The app's dialog reads the full detail from its own route;
122
+ * handing that to an agent cost ~445,000 characters for 27 files of 15 KB and
123
+ * overflowed every tool-result limit. Built by `summarizeChangeRequest`.
124
+ *
125
+ * Property order is the answer's field order, and `url` leads it: the link is
126
+ * the one field the agent must hand the user, so it must survive any truncation.
127
+ */
128
+ const changeRequestSummarySchema: JsonSchema = {
102
129
  type: 'object',
103
- description: 'Full change-request detail (aliased from the underlying pull request).',
104
130
  properties: {
105
- number: { type: 'integer' },
106
131
  url: changeRequestUrlSchema,
107
132
  urlNote: changeRequestUrlNoteSchema,
133
+ number: { type: 'integer' },
108
134
  title: { type: 'string' },
109
- body: { type: 'string' },
110
- author: { type: 'object', properties: { login: { type: 'string' }, name: { type: 'string' } }, required: ['login'] },
111
- headSha: { type: 'string' },
112
- baseSha: { type: 'string' },
113
- files: { type: 'array', items: changeRequestFileSchema },
114
- comments: { type: 'array', items: commentSchema },
115
- approvals: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Per-file approval state, one entry per file in `files`.' },
135
+ state: { type: 'string', description: '`open` | `merged` | `closed`.' },
136
+ sourceBranch: { type: 'string', description: 'The draft branch carrying the changes.' },
137
+ targetBranch: { type: 'string', description: 'The branch the changes apply to.' },
138
+ approvals: {
139
+ type: 'array',
140
+ items: changeRequestApproversSchema,
141
+ description: 'Who must approve, one entry per listed file. A file nobody can approve (`inMergeGate: false`) blocks nothing.',
142
+ },
143
+ mergeBlockedReasons: {
144
+ type: 'array',
145
+ items: { type: 'string' },
146
+ description: 'What blocks the merge — every missing approval, whether its file is listed or cut. Empty when nothing blocks.',
147
+ },
148
+ files: {
149
+ type: 'array',
150
+ items: changeRequestSummaryFileSchema,
151
+ description: `The changed paths with their kind of change, cut to ${CHANGE_REQUEST_PATH_LIMIT}; \`include: ["all-paths"]\` lists them all.`,
152
+ },
153
+ totalFiles: { type: 'integer', description: 'How many files the request changes in total, cut or not.' },
116
154
  },
117
- required: ['number', 'url', 'title', 'body', 'headSha', 'baseSha', 'files', 'comments', 'approvals'],
118
- additionalProperties: true,
155
+ required: ['url', 'number', 'title', 'state', 'sourceBranch', 'targetBranch', 'approvals', 'mergeBlockedReasons', 'files', 'totalFiles'],
119
156
  };
120
157
 
121
158
  /**
@@ -144,16 +181,30 @@ export function registerWorkflowTools(
144
181
  write: boolean;
145
182
  /** Register on the internal catalog only (e.g. agent-control tools that make no sense to a remote caller). */
146
183
  internalOnly?: boolean;
147
- /** Skip the auto-injected `branch` input — for a tool that already declares its own `branch` (switch_branch). */
184
+ /**
185
+ * Skip the auto-injected `branch` input — for a tool that declares its own
186
+ * `branch` with its own meaning (`create_branch`'s fork base,
187
+ * `switch_branch`'s target draft) or none at all (`merge_branch`,
188
+ * `open_change_request`, the repo-global reads). It says NOTHING about
189
+ * whether the tool requires a branch: that is read off the declared schema
190
+ * below, because reading it off this flag is what let `create_branch` opt
191
+ * out of the check while still demanding the input.
192
+ */
148
193
  skipBranch?: boolean;
149
194
  handler: ToolHandler;
150
195
  }): void => {
151
196
  const path = `/api/agent/tools/${spec.name}`;
197
+ const inputs = spec.skipBranch ? spec.inputs : withBranchInput(spec.inputs);
198
+ // The tool's OWN declaration is the only honest answer to "must this call
199
+ // name a branch": the injected input and a hand-declared one (the fork
200
+ // base, the draft to switch to) are equally required, and a tool that adds
201
+ // or drops the input later moves this with it.
202
+ const requiresBranch = ((inputs as { required?: string[] }).required ?? []).includes('branch');
152
203
  const def = toolDef({
153
204
  name: spec.name,
154
205
  description: spec.description,
155
206
  path,
156
- inputs: spec.skipBranch ? spec.inputs : withBranchInput(spec.inputs),
207
+ inputs,
157
208
  outputs: spec.outputs,
158
209
  tags: spec.write ? ['workflow', 'write'] : ['workflow'],
159
210
  });
@@ -171,7 +222,27 @@ export function registerWorkflowTools(
171
222
  // normaliser: the root-anchored `/<kbDirName>/…` form names the same
172
223
  // workspace path, and a path with no prefix is placed under
173
224
  // `<kbDirName>/` rather than refused.
174
- toolHandler((args, ctx) => spec.handler(normalizePathArgs(args, kbDirName), ctx), { write: spec.write }),
225
+ toolHandler(
226
+ (args, ctx) => {
227
+ // These tools hand `branch` STRAIGHT to `workspaceIdForBranch` —
228
+ // they address a workspace by id rather than going through
229
+ // `getFilesystem`, so the choke point that guards the file tools
230
+ // never sees them. Without this, a branch-less `commit_change`
231
+ // commits to a workspace id literally named "undefined", and a
232
+ // branch-less `create_branch` forks from one: it clones a branch of
233
+ // that name, fails, and answers `There is no branch named undefined.`
234
+ //
235
+ // Keyed on whether the SCHEMA requires `branch`, not on whether the
236
+ // input was injected. `create_branch` and `switch_branch` declare
237
+ // their own required `branch`; a check keyed on the injection skipped
238
+ // exactly those two, which is how the reported bug survived on
239
+ // `create_branch` — the one tool whose `branch` is a branch the
240
+ // caller must already have.
241
+ if (requiresBranch) assertBranchProvided(args.branch);
242
+ return spec.handler(normalizePathArgs(args, kbDirName), ctx);
243
+ },
244
+ { write: spec.write },
245
+ ),
175
246
  );
176
247
  };
177
248
 
@@ -330,6 +401,17 @@ export function registerWorkflowTools(
330
401
  handler: async (args, ctx: ToolContext) => {
331
402
  const name = args.name as string;
332
403
  const base = args.branch as string;
404
+ // `name` is declared required too, and the same-name refusal below quotes
405
+ // it. With nothing enforcing it, a call that named neither input compared
406
+ // undefined to undefined, matched, and answered "not 'undefined', the
407
+ // draft being created" — an absent value presented as a draft name, the
408
+ // very sentence this ticket exists to stop. Refused by name instead,
409
+ // without echoing what was not sent.
410
+ if (typeof name !== 'string' || name.length === 0) {
411
+ throw new ToolError('`name` is required: pass the name of the draft to create.', 400, {
412
+ kind: 'name-required',
413
+ });
414
+ }
333
415
  // The git op runs in the base's workspace, whose clone has the base
334
416
  // checked out — the new draft forks from its HEAD. Resolving a workspace
335
417
  // lazily clones its branch from origin, so a `branch` naming the
@@ -353,6 +435,12 @@ export function registerWorkflowTools(
353
435
  'Open a change request from a draft branch into a target branch. Auto-merges the latest target ' +
354
436
  'into the source first, pushes, then creates the CR. On conflicts returns a ' +
355
437
  '`change-request-conflicts` error with affected paths. The author marker is injected server-side. ' +
438
+ 'Answers a SUMMARY, not the request\'s content: `url` first (hand it to the user), then `number`, ' +
439
+ '`title`, `state`, `sourceBranch`, `targetBranch`, `approvals` (who must approve each file), ' +
440
+ '`mergeBlockedReasons` (what blocks the merge), and `files` — the changed paths with their kind of ' +
441
+ `change (\`added\` / \`changed\` / \`deleted\` / \`moved\`), cut to ${CHANGE_REQUEST_PATH_LIMIT} of \`totalFiles\`. ` +
442
+ 'No patches and no file content: ask for the diffs with `include: ["patches"]` and for every path ' +
443
+ 'with `include: ["all-paths"]`. ' +
356
444
  'An agent proposes; a person reviews and merges the request in the app — hand the user its `url`.',
357
445
  inputs: {
358
446
  type: 'object',
@@ -361,28 +449,37 @@ export function registerWorkflowTools(
361
449
  targetBranch: { type: 'string', minLength: 1, description: `The branch to apply to (e.g. '${kb.defaultBranch}').` },
362
450
  title: { type: 'string', minLength: 1, maxLength: 256, description: 'Short imperative title (≤256 chars).' },
363
451
  description: { type: 'string', description: 'Optional markdown body shown verbatim to reviewers.' },
452
+ include: {
453
+ type: 'array',
454
+ items: { type: 'string', enum: [...CHANGE_REQUEST_INCLUDES] },
455
+ description:
456
+ 'Add to the default answer, which carries neither: `patches` gives each listed file its unified diff, ' +
457
+ `\`all-paths\` lists every changed path instead of the first ${CHANGE_REQUEST_PATH_LIMIT}. Both may be given. ` +
458
+ 'Patches of a large request run to hundreds of thousands of characters — ask for them only when you will read them.',
459
+ },
364
460
  },
365
461
  required: ['sourceBranch', 'targetBranch', 'title'],
366
462
  additionalProperties: false,
367
463
  },
368
- outputs: {
369
- type: 'object',
370
- properties: { changeRequest: changeRequestDetailSchema },
371
- required: ['changeRequest'],
372
- },
464
+ outputs: changeRequestSummarySchema,
373
465
  write: true,
374
466
  // The workspace this acts on is the SOURCE draft's — `sourceBranch` already
375
467
  // names it, so skip the auto-injected (and here redundant) `branch` input
376
468
  // rather than asking the model for both and reading the wrong one.
377
469
  skipBranch: true,
378
- handler: async (args, ctx: ToolContext) => ({
379
- changeRequest: await ctx.workflowService.openChangeRequest(workspaceIdForBranch(args.sourceBranch as string), ctx.user, {
380
- sourceBranch: args.sourceBranch as string,
381
- targetBranch: args.targetBranch as string,
382
- title: args.title as string,
383
- description: typeof args.description === 'string' ? args.description : undefined,
384
- }),
385
- }),
470
+ // The service still returns the full detail — the app's routes serve the
471
+ // same call and must keep getting it. The shaping happens here, in the
472
+ // answer, so nothing below the tool changes.
473
+ handler: async (args, ctx: ToolContext) =>
474
+ summarizeChangeRequest(
475
+ await ctx.workflowService.openChangeRequest(workspaceIdForBranch(args.sourceBranch as string), ctx.user, {
476
+ sourceBranch: args.sourceBranch as string,
477
+ targetBranch: args.targetBranch as string,
478
+ title: args.title as string,
479
+ description: typeof args.description === 'string' ? args.description : undefined,
480
+ }),
481
+ parseInclude(args.include),
482
+ ),
386
483
  });
387
484
 
388
485
  mount({
@@ -305,6 +305,37 @@ export class FileLockService {
305
305
  return rowToFileLock(row);
306
306
  }
307
307
 
308
+ /**
309
+ * Drop every lock held on `branch`, whoever holds it and whichever
310
+ * workspace it sits in, and answer how many rows went.
311
+ *
312
+ * The one caller is a change request being closed because the
313
+ * knowledge-base repository was REPLACED: the branch the lock names belongs
314
+ * to a repository this deployment no longer has, so the holder can neither
315
+ * publish their bytes nor release the lock themselves — and the row would
316
+ * otherwise sit there refusing the path to everyone until its TTL ran out
317
+ * on every heartbeat a still-connected client keeps sending.
318
+ *
319
+ * Expired rows go too: they are already nobody's lock, and leaving them for
320
+ * the lazy sweep would only make this answer harder to read.
321
+ *
322
+ * Deliberately NOT a release in the ordinary sense — nothing is committed
323
+ * and nothing is enqueued. The bytes belonged to a repository that is gone.
324
+ *
325
+ * What an ORDINARY release already enqueued is a different matter, and not
326
+ * this method's to fix: a release queues the bytes and only then drops its
327
+ * lock, so rows can be waiting for a branch whose locks this deletes. The
328
+ * caller takes those out of the worker's reach in the same breath — see
329
+ * `PendingCommitsService.markNeedsAttentionOnBranch`.
330
+ */
331
+ async releaseAllOnBranch(branch: string): Promise<number> {
332
+ const gone = await this.db
333
+ .delete(fileLocks)
334
+ .where(eq(fileLocks.branch, branch))
335
+ .returning({ path: fileLocks.path });
336
+ return gone.length;
337
+ }
338
+
308
339
  /**
309
340
  * Is ANY unexpired lock held anywhere in this workspace? Feeds the git
310
341
  * status sanity check: a held lock means a mutation is mid-flight — the
@@ -0,0 +1,260 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { testKbContext } from '../../../../__tests__/kb-context.js';
6
+ import { GitService } from '../git.service.js';
7
+ import {
8
+ VersionNotOnBranchError,
9
+ VERSION_NOT_ON_BRANCH_MESSAGE,
10
+ WorkflowValidationError,
11
+ } from '../../../../shared/domain-errors.js';
12
+ import { GitRunError, assertOneSpecPerLine } from '../../../../shared/git.contract.js';
13
+ import { runGit, gitOut, stubWorkflowHooks, stubWorkspaceService } from './git-test-helpers.js';
14
+
15
+ /**
16
+ * A past version has to come back BYTE FOR BYTE, and only for saves on the
17
+ * branch being viewed.
18
+ *
19
+ * `readFileAtRef` decodes git's stdout as UTF-8, which is why a binary could
20
+ * not be served through it at all: every byte sequence that is not valid UTF-8
21
+ * comes back as U+FFFD, so a PNG downloaded from history would not open. The
22
+ * suite pins the round trip on bytes that would not survive that (everything
23
+ * from 0x00 to 0xff, NUL included), the two sides of a save, and the branch
24
+ * rule that decides who may ask at all.
25
+ */
26
+
27
+ /** Every byte value, so any decode-and-re-encode step is visible as a failure. */
28
+ const ALL_BYTES = Buffer.from(Array.from({ length: 256 }, (_, i) => i));
29
+ const PNG_HEADER = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
30
+ const V1 = Buffer.concat([PNG_HEADER, ALL_BYTES, Buffer.from('v1')]);
31
+ const V2 = Buffer.concat([PNG_HEADER, ALL_BYTES.subarray(0, 128), Buffer.from('v2\0tail')]);
32
+
33
+ describe('GitService.fileBytesAtCommit', () => {
34
+ let root: string;
35
+ let repo: string;
36
+ let svc: GitService;
37
+ const workspaceId = 'target-company-state';
38
+ /** The save that ADDED logo.png (V1), the save that REPLACED it (V2). */
39
+ let addSha: string;
40
+ let replaceSha: string;
41
+
42
+ beforeEach(async () => {
43
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-bytes-at-ref-'));
44
+ const workspaceDir = path.join(root, workspaceId);
45
+ repo = path.join(workspaceDir, 'knowledge-base');
46
+ await fs.mkdir(repo, { recursive: true });
47
+ await runGit(root, ['init', '-b', 'target-company-state', repo]);
48
+ await runGit(repo, ['config', 'user.email', 'workspace@bevel.test']);
49
+ await runGit(repo, ['config', 'user.name', 'bevel Workspace']);
50
+
51
+ await fs.writeFile(path.join(repo, 'logo.png'), V1);
52
+ await fs.mkdir(path.join(repo, 'docs'));
53
+ await fs.writeFile(path.join(repo, 'docs', 'child.md'), 'child\n');
54
+ await runGit(repo, ['add', '.']);
55
+ await runGit(repo, ['commit', '-m', 'add logo']);
56
+ addSha = await gitOut(repo, ['rev-parse', 'HEAD']);
57
+
58
+ await fs.writeFile(path.join(repo, 'logo.png'), V2);
59
+ await runGit(repo, ['add', '.']);
60
+ await runGit(repo, ['commit', '-m', 'replace logo']);
61
+ replaceSha = await gitOut(repo, ['rev-parse', 'HEAD']);
62
+
63
+ svc = new GitService(
64
+ stubWorkspaceService({ [workspaceId]: workspaceDir }),
65
+ stubWorkflowHooks(),
66
+ testKbContext(),
67
+ );
68
+ });
69
+
70
+ afterEach(async () => {
71
+ await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
72
+ });
73
+
74
+ it('returns the exact bytes of each save, on both sides of it', async () => {
75
+ const atAdd = await svc.fileBytesAtCommit(workspaceId, 'knowledge-base/logo.png', addSha, 'after');
76
+ expect(atAdd).not.toBeNull();
77
+ expect(atAdd!.bytes.equals(V1)).toBe(true);
78
+ expect(atAdd!.blobId).toMatch(/^[0-9a-f]{40}$/);
79
+
80
+ const atReplace = await svc.fileBytesAtCommit(
81
+ workspaceId,
82
+ 'knowledge-base/logo.png',
83
+ replaceSha,
84
+ 'after',
85
+ );
86
+ expect(atReplace!.bytes.equals(V2)).toBe(true);
87
+
88
+ // The `before` side of the replacing save IS the original upload — which
89
+ // is what makes "download the version this save replaced" possible.
90
+ const before = await svc.fileBytesAtCommit(
91
+ workspaceId,
92
+ 'knowledge-base/logo.png',
93
+ replaceSha,
94
+ 'before',
95
+ );
96
+ expect(before!.bytes.equals(V1)).toBe(true);
97
+ // Different content, therefore a different ETag: the blob id IS the hash.
98
+ expect(atReplace!.blobId).not.toBe(before!.blobId);
99
+ });
100
+
101
+ it('accepts the repo-relative spelling of the path too', async () => {
102
+ const at = await svc.fileBytesAtCommit(workspaceId, 'logo.png', addSha, 'after');
103
+ expect(at!.bytes.equals(V1)).toBe(true);
104
+ });
105
+
106
+ it('serves the version just before the save that DELETED the file', async () => {
107
+ await fs.rm(path.join(repo, 'logo.png'));
108
+ await runGit(repo, ['add', '-A']);
109
+ await runGit(repo, ['commit', '-m', 'remove logo']);
110
+ const delSha = await gitOut(repo, ['rev-parse', 'HEAD']);
111
+
112
+ // Nothing at the path after the save…
113
+ expect(
114
+ await svc.fileBytesAtCommit(workspaceId, 'knowledge-base/logo.png', delSha, 'after'),
115
+ ).toBeNull();
116
+ // …and the bytes as they were just before it.
117
+ const before = await svc.fileBytesAtCommit(
118
+ workspaceId,
119
+ 'knowledge-base/logo.png',
120
+ delSha,
121
+ 'before',
122
+ );
123
+ expect(before!.bytes.equals(V2)).toBe(true);
124
+ });
125
+
126
+ it('returns null for a path the save did not have', async () => {
127
+ expect(
128
+ await svc.fileBytesAtCommit(workspaceId, 'knowledge-base/never.png', addSha, 'after'),
129
+ ).toBeNull();
130
+ // The root commit has no parent, so it has no `before` side either.
131
+ expect(
132
+ await svc.fileBytesAtCommit(workspaceId, 'knowledge-base/logo.png', addSha, 'before'),
133
+ ).toBeNull();
134
+ });
135
+
136
+ it('refuses a directory at the ref — history is served per file', async () => {
137
+ await expect(
138
+ svc.fileBytesAtCommit(workspaceId, 'knowledge-base/docs', addSha, 'after'),
139
+ ).rejects.toThrow(/history is served per file/);
140
+ });
141
+
142
+ it('refuses a path carrying a LINE BREAK, which git would read as two object names', async () => {
143
+ // `cat-file --batch` takes one `<ref>:<path>` spec per LINE. Before the
144
+ // path validator refused control characters, `docs/child.md\nzzz` reached
145
+ // it as two specs: git answered about `docs/child.md` and those bytes came
146
+ // back as this path's — under the access gates of a name that exists
147
+ // nowhere, so the file's own rules were never the ones consulted. Verified
148
+ // against real git here, not a stub: the call must fail, and nothing of
149
+ // `child.md` may come back under the longer name.
150
+ for (const injected of [
151
+ 'knowledge-base/docs/child.md\nzzz',
152
+ 'knowledge-base/docs/child.md\rzzz',
153
+ 'knowledge-base/docs/child.md\nknowledge-base/logo.png',
154
+ ]) {
155
+ // Both the TYPE and the sentence. Two different validators are called
156
+ // `assertValidRelativePath` — the shared one in
157
+ // `packages/shared/src/workspace/filename.ts`, which throws a plain
158
+ // `Error('Invalid path: …')`, and the kb-fs one in
159
+ // `modules/kb-fs/branch-name.ts`, which throws `WorkflowValidationError`
160
+ // (status 400). `GitService` imports the kb-fs one, and the 400 is what
161
+ // makes the route answer 400 rather than 500, so the type is asserted
162
+ // here deliberately and the message pins which of the two ran.
163
+ await expect(
164
+ svc.fileBytesAtCommit(workspaceId, injected, addSha, 'after'),
165
+ ).rejects.toThrow(WorkflowValidationError);
166
+ await expect(
167
+ svc.fileBytesAtCommit(workspaceId, injected, addSha, 'after'),
168
+ ).rejects.toThrow(/control character/);
169
+ await expect(
170
+ svc.fileBytesAtCommit(workspaceId, injected, addSha, 'after'),
171
+ ).rejects.toMatchObject({ status: 400 });
172
+ }
173
+ // The refusal is the PATH's, decided before the sha is resolved or the
174
+ // workspace's git turn is taken — so a caller cannot reach git with one.
175
+ await expect(
176
+ svc.fileBytesAtCommit(workspaceId, 'knowledge-base/docs/child.md\nzzz', 'HEAD', 'after'),
177
+ ).rejects.toThrow(/control character/);
178
+ });
179
+
180
+ it('fences the batch protocol itself against a spec that is not one line', () => {
181
+ // The fence under `blobAtRef` and under the access resolver's batch read,
182
+ // for a spec assembled from somewhere the path validator does not cover.
183
+ expect(() => assertOneSpecPerLine([`${addSha}:logo.png`])).not.toThrow();
184
+ expect(() => assertOneSpecPerLine([`${addSha}:docs/child.md\nzzz`])).toThrow(/line break/);
185
+ expect(() => assertOneSpecPerLine(['a:b', 'c:d\re'])).toThrow(/line break/);
186
+ });
187
+
188
+ it('refuses a sha that is not a sha', async () => {
189
+ await expect(
190
+ svc.fileBytesAtCommit(workspaceId, 'knowledge-base/logo.png', 'HEAD', 'after'),
191
+ ).rejects.toThrow(WorkflowValidationError);
192
+ });
193
+
194
+ it('refuses a save that is not in this branch\'s history, and a made-up one, alike', async () => {
195
+ // A save that exists — on another branch this workspace is not viewing.
196
+ await runGit(repo, ['checkout', '-b', 'someone-else']);
197
+ await fs.writeFile(path.join(repo, 'logo.png'), Buffer.from('elsewhere'));
198
+ await runGit(repo, ['add', '-A']);
199
+ await runGit(repo, ['commit', '-m', 'elsewhere']);
200
+ const otherSha = await gitOut(repo, ['rev-parse', 'HEAD']);
201
+ await runGit(repo, ['checkout', 'target-company-state']);
202
+
203
+ for (const sha of [otherSha, 'a'.repeat(40)]) {
204
+ const read = svc.fileBytesAtCommit(workspaceId, 'knowledge-base/logo.png', sha, 'after');
205
+ await expect(read).rejects.toThrow(VersionNotOnBranchError);
206
+ await expect(read).rejects.toThrow(VERSION_NOT_ON_BRANCH_MESSAGE);
207
+ }
208
+
209
+ // …and every save the panel LISTS is accepted, which is the other half of
210
+ // the rule: `logForFile` walks HEAD, so anything it returns is an ancestor.
211
+ // Asserted through the read itself rather than a boolean helper, so what is
212
+ // pinned is the thing the route calls.
213
+ for (const listed of await svc.logForFile(workspaceId, 'knowledge-base/logo.png')) {
214
+ await expect(
215
+ svc.fileBytesAtCommit(workspaceId, 'knowledge-base/logo.png', listed.sha, 'after'),
216
+ ).resolves.not.toBeNull();
217
+ }
218
+ });
219
+
220
+ it('propagates a repository failure rather than calling it a version off the branch', async () => {
221
+ // An UNBORN HEAD: the sha resolves perfectly well and git's complaint is
222
+ // about the other argument ('ambiguous argument HEAD'). Folded into
223
+ // `VersionNotOnBranchError` it would tell a reader the save they can see
224
+ // listed is not in their history — a broken workspace dressed up as an
225
+ // answer about access, and one 'Try again' can never get past.
226
+ await runGit(repo, ['checkout', '--orphan', 'nothing-committed-yet']);
227
+ const err: unknown = await svc
228
+ .fileBytesAtCommit(workspaceId, 'knowledge-base/logo.png', addSha, 'after')
229
+ .catch((e: unknown) => e);
230
+ // `GitRunError` specifically, not just `Error`: the route reads the error's
231
+ // TYPE to decide its status, so a future re-wrap into
232
+ // `WorkflowValidationError` would turn this into a 400 about the caller's
233
+ // request rather than the logged 500 a broken repository earns, and a bare
234
+ // `toBeInstanceOf(Error)` would not notice.
235
+ expect(err).toBeInstanceOf(GitRunError);
236
+ expect(err).not.toBeInstanceOf(VersionNotOnBranchError);
237
+ });
238
+
239
+ it('applies the same branch rule to the existing text-history reads', async () => {
240
+ await runGit(repo, ['checkout', '-b', 'someone-else']);
241
+ await fs.writeFile(path.join(repo, 'logo.png'), Buffer.from('elsewhere'));
242
+ await runGit(repo, ['add', '-A']);
243
+ await runGit(repo, ['commit', '-m', 'elsewhere']);
244
+ const otherSha = await gitOut(repo, ['rev-parse', 'HEAD']);
245
+ await runGit(repo, ['checkout', 'target-company-state']);
246
+
247
+ // Otherwise the bytes route's rule could simply be read around: the patch
248
+ // and the full before/after text are past versions too.
249
+ await expect(
250
+ svc.diffFileAtCommit(workspaceId, 'knowledge-base/logo.png', otherSha),
251
+ ).rejects.toThrow(VERSION_NOT_ON_BRANCH_MESSAGE);
252
+ await expect(
253
+ svc.fileContentsAtCommit(workspaceId, 'knowledge-base/logo.png', otherSha),
254
+ ).rejects.toThrow(VERSION_NOT_ON_BRANCH_MESSAGE);
255
+ // The on-branch saves still work.
256
+ expect(await svc.diffFileAtCommit(workspaceId, 'knowledge-base/logo.png', replaceSha)).toContain(
257
+ 'logo.png',
258
+ );
259
+ });
260
+ });