@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
@@ -99,6 +99,20 @@
99
99
  "when": 1790243482866,
100
100
  "tag": "0013_agent_identity_and_key_soft_delete",
101
101
  "breakpoints": true
102
+ },
103
+ {
104
+ "idx": 14,
105
+ "version": "7",
106
+ "when": 1790667040382,
107
+ "tag": "0014_change_request_closed_reason",
108
+ "breakpoints": true
109
+ },
110
+ {
111
+ "idx": 15,
112
+ "version": "7",
113
+ "when": 1790892582327,
114
+ "tag": "0015_account_deactivation",
115
+ "breakpoints": true
102
116
  }
103
117
  ]
104
118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bevel-software/platform-core-backend",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "Open-source core backend of the Bevel platform: git-backed knowledge workspace, workflow (branches/change requests/locks/SSE), skills, tools, secrets vault, access control and the remote MCP surface.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -52,8 +52,8 @@
52
52
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
53
53
  "yaml": "^2.9.0",
54
54
  "zod": "^3.24.0",
55
- "@bevel-software/platform-mcp-core": "0.21.0",
56
- "@bevel-software/platform-shared": "0.21.0"
55
+ "@bevel-software/platform-shared": "0.23.0",
56
+ "@bevel-software/platform-mcp-core": "0.23.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/adm-zip": "^0.5.8",
@@ -10,7 +10,6 @@ import {
10
10
  currentKbLayout,
11
11
  isPlatformFile,
12
12
  mentionsAgentsFile,
13
- ontologyRoots,
14
13
  platformFileNames,
15
14
  pluginOfPath,
16
15
  renderKbLayoutPlaceholders,
@@ -105,7 +104,6 @@ describe('KB layout — configuration', () => {
105
104
 
106
105
  test('the derived sets follow the configured names rather than snapshotting the defaults', () => {
107
106
  configureKbLayout({ knowledgeBaseDir: 'docs', skillsDir: 'skills', pluginsDir: 'plugins' });
108
- expect(ontologyRoots(currentKbLayout())).toEqual(['docs', 'Data']);
109
107
  expect(reservedRootDirNames(currentKbLayout()).has('plugins')).toBe(true);
110
108
  expect(reservedRootDirNames(currentKbLayout()).has('Plugins')).toBe(false);
111
109
  // Path rules read the configured name too.
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { readFile, readdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { CoreConfig } from '../core-config.js';
6
+ import { PLATFORM_HEADER, TOOL_PREFIX_LINE } from '../modules/agent-instructions/compose.js';
7
+
8
+ /**
9
+ * What a deployment is left holding after the per-conversation boundary moved
10
+ * out of these packages: a variable nobody reads, a table nobody touches, and
11
+ * no wording that names what Hexis no longer knows about.
12
+ */
13
+
14
+ const REQUIRED = {
15
+ KB_REPO_URL: 'https://example.com/org/kb.git',
16
+ ADMIN_EMAIL: 'root@example.com',
17
+ ADMIN_PASSWORD: 'sup3r-secret',
18
+ JWT_SECRET: 'test-jwt-secret',
19
+ SECRETS_ENC_KEY: 'kToAi8FXWDpDn3A6yQ/60O39bv05N7XzVOIu/0CJrFc=',
20
+ };
21
+
22
+ let saved: NodeJS.ProcessEnv;
23
+
24
+ beforeEach(() => {
25
+ saved = { ...process.env };
26
+ delete process.env.ONTOLOGY_SESSION_BLOCK;
27
+ Object.assign(process.env, REQUIRED);
28
+ });
29
+
30
+ afterEach(() => {
31
+ process.env = saved;
32
+ });
33
+
34
+ describe('ONTOLOGY_SESSION_BLOCK is retired', () => {
35
+ it('is ignored: the config builds either way, with no such setting on it', () => {
36
+ for (const value of ['false', 'true', 'nonsense']) {
37
+ process.env.ONTOLOGY_SESSION_BLOCK = value;
38
+ const config = new CoreConfig();
39
+ expect(config.kbRepoUrl).toBe(REQUIRED.KB_REPO_URL);
40
+ expect(config).not.toHaveProperty('ontologySessionBlock');
41
+ }
42
+ });
43
+
44
+ it('a config built with it set is the same as one built without it', () => {
45
+ const without = JSON.stringify(new CoreConfig());
46
+ process.env.ONTOLOGY_SESSION_BLOCK = 'false';
47
+ expect(JSON.stringify(new CoreConfig())).toBe(without);
48
+ });
49
+
50
+ // BOTH reference docs, so neither can be left promising what the other
51
+ // retired: `.env.example` is the one a deployment COPIES, so an entry
52
+ // surviving there is an inert kill-switch somebody sets and believes in.
53
+ it.each(['docs/configuration.md', '.env.example'])('%s documents no variable named for it', async (doc) => {
54
+ const here = fileURLToPath(new URL('.', import.meta.url));
55
+ const text = await readFile(join(here, '../../../../', doc), 'utf8');
56
+ expect(text).not.toContain('ONTOLOGY_SESSION_BLOCK');
57
+ expect(text.toLowerCase()).not.toContain('ontolog');
58
+ });
59
+ });
60
+
61
+ describe('what an agent or a person reads names no ontology', () => {
62
+ it('the server instructions do not', () => {
63
+ expect(PLATFORM_HEADER.toLowerCase()).not.toContain('ontolog');
64
+ expect(TOOL_PREFIX_LINE.toLowerCase()).not.toContain('ontolog');
65
+ });
66
+ });
67
+
68
+ /**
69
+ * The table stays in this release so that no deployment runs a version in
70
+ * which neither side has it — but nothing in this package may read or write it
71
+ * any more. The schema file that declares it is the one and only mention.
72
+ */
73
+ describe('session_ontology_touches is unused', () => {
74
+ it('is named by the schema that declares it and by nothing else', async () => {
75
+ const src = fileURLToPath(new URL('..', import.meta.url));
76
+ const self = fileURLToPath(import.meta.url);
77
+ const found: string[] = [];
78
+ const walk = async (dir: string): Promise<void> => {
79
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
80
+ const path = join(dir, entry.name);
81
+ if (entry.isDirectory()) {
82
+ await walk(path);
83
+ continue;
84
+ }
85
+ // This file names the table to look for it; it is not a use of it.
86
+ if (!entry.name.endsWith('.ts') || path === self) continue;
87
+ const text = await readFile(path, 'utf8');
88
+ if (text.includes('sessionOntologyTouches') || text.includes('session_ontology_touches')) {
89
+ found.push(path.slice(src.length));
90
+ }
91
+ }
92
+ };
93
+ await walk(src);
94
+ expect(found).toEqual(['modules/database/core-schema.ts']);
95
+ });
96
+ });
@@ -0,0 +1,132 @@
1
+ import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
2
+ import fsp from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { startCore, type BootableCore } from '../lifecycle.js';
6
+ import { ClassifiedFailure, gitFailure } from '../../shared/git-failure.js';
7
+ import {
8
+ KbRemoteUnreachableError,
9
+ bootMaySurvive,
10
+ } from '../../modules/workspace/startup/kb-startup-runner.js';
11
+
12
+ /**
13
+ * A boot over a knowledge-base repository the host will not serve.
14
+ *
15
+ * On 2026-09-28 a deployment's repository was deleted and replaced. The new
16
+ * address saved and its connection test passed, but a working copy still
17
+ * fetching through the OLD address answered "repository not found" inside a
18
+ * startup step — which was not the one failure a boot survived, so the
19
+ * container stopped, restarted, and stopped again, taking the setup screen
20
+ * that would have fixed it with it.
21
+ *
22
+ * The rule now: whatever the host ANSWERED about the repository or the
23
+ * credentials lets the boot come up GATED. Only a failure that says the
24
+ * knowledge base itself would be written wrong still stops it.
25
+ */
26
+
27
+ /** An empty workspaces root, so the beside-the-checkout note finds nothing to say. */
28
+ let workspacesRoot: string;
29
+
30
+ beforeEach(async () => {
31
+ workspacesRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'gated-boot-'));
32
+ });
33
+
34
+ afterEach(async () => {
35
+ await fsp.rm(workspacesRoot, { recursive: true, force: true });
36
+ });
37
+
38
+ /** A core whose startup phase fails in a way the test picks. */
39
+ function bootable(runAll: () => Promise<void>) {
40
+ const retries = { started: 0 };
41
+ const core: BootableCore = {
42
+ config: { workspacesRoot },
43
+ kbDirName: 'knowledge-base',
44
+ kbStartupRunner: {
45
+ runAll,
46
+ retryUntilMaintained: () => {
47
+ retries.started += 1;
48
+ return { stop() {} };
49
+ },
50
+ } as BootableCore['kbStartupRunner'],
51
+ workflowService: {
52
+ closeChangeRequestsWithDeletedBranches: async () => 0,
53
+ } as BootableCore['workflowService'],
54
+ pluginJoinRequestJobs: { startSweeping() {} } as BootableCore['pluginJoinRequestJobs'],
55
+ startupRetry: null,
56
+ };
57
+ return { core, retries };
58
+ }
59
+
60
+ afterEach(() => {
61
+ vi.restoreAllMocks();
62
+ });
63
+
64
+ describe('startCore over a repository the host will not serve', () => {
65
+ it('comes up gated on "repository not found" raised inside the phase — no restart loop', async () => {
66
+ vi.spyOn(console, 'error').mockImplementation(() => {});
67
+ const { core, retries } = bootable(async () => {
68
+ throw new ClassifiedFailure(
69
+ 'KB startup step "groups-to-plugins" failed: git fetch failed: repository not found',
70
+ gitFailure('not-found'),
71
+ );
72
+ });
73
+
74
+ // Resolves: the server boots. The setup routes keep it gated on the
75
+ // runner's standing failure, and the admin can sign in and fix it.
76
+ await expect(startCore(core)).resolves.toBeUndefined();
77
+ // And it keeps asking on its own: a repository that is created, or a token
78
+ // that is granted access to it, needs no settings change on this side.
79
+ expect(retries.started).toBe(1);
80
+ expect(core.startupRetry).not.toBeNull();
81
+ });
82
+
83
+ it('comes up gated when the host rejected the credentials', async () => {
84
+ vi.spyOn(console, 'error').mockImplementation(() => {});
85
+ const { core } = bootable(async () => {
86
+ throw new ClassifiedFailure('git push failed: Authentication failed', gitFailure('credentials-rejected'));
87
+ });
88
+ await expect(startCore(core)).resolves.toBeUndefined();
89
+ // The same gated contract as the sibling above: the boot hands the retry
90
+ // handle to the runner rather than leaving the deployment to a restart.
91
+ // (Handle CREATION is what this pins — the stub counts
92
+ // `retryUntilMaintained` calls, which cannot tell a handle being made from
93
+ // a retry loop actually running; that semantics is
94
+ // `kb-startup-runner.degraded.test.ts`'s.)
95
+ expect(core.startupRetry).not.toBeNull();
96
+ });
97
+
98
+ it('still STOPS the boot when the failure is about what a step would write', async () => {
99
+ const { core, retries } = bootable(async () => {
100
+ throw new ClassifiedFailure(
101
+ 'KB startup step "template-files" failed: the template is missing roles.yaml',
102
+ { kind: 'step-failed', cause: 'The startup step "template-files" failed.' },
103
+ );
104
+ });
105
+
106
+ // Booting over this would scaffold something wrong into every branch.
107
+ await expect(startCore(core)).rejects.toThrow(/template-files/);
108
+ expect(retries.started).toBe(0);
109
+ });
110
+ });
111
+
112
+ describe('bootMaySurvive', () => {
113
+ it('says yes to the remote being out of reach, whatever raised it', () => {
114
+ expect(bootMaySurvive(new KbRemoteUnreachableError('could not be reached'))).toBe(true);
115
+ expect(bootMaySurvive(new ClassifiedFailure('x', gitFailure('unreachable')))).toBe(true);
116
+ });
117
+
118
+ it('says yes to what the host answered about the repository or the credentials', () => {
119
+ for (const kind of ['not-found', 'credentials-rejected', 'write-refused'] as const) {
120
+ expect({ kind, survives: bootMaySurvive(new ClassifiedFailure('x', gitFailure(kind))) }).toEqual({
121
+ kind,
122
+ survives: true,
123
+ });
124
+ }
125
+ });
126
+
127
+ it('says no to everything else — those say the knowledge base would be written wrong', () => {
128
+ expect(bootMaySurvive(new ClassifiedFailure('x', gitFailure('push-refused-by-policy')))).toBe(false);
129
+ expect(bootMaySurvive(new ClassifiedFailure('x', gitFailure('unknown')))).toBe(false);
130
+ expect(bootMaySurvive(new Error('a step threw'))).toBe(false);
131
+ });
132
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { createShutdown, holdCommitWorkerLease, stopCore, withStartupTask, type LeasedWorker } from '../lifecycle.js';
2
+ import { createShutdown, holdCommitWorkerLease, holdable, stopCore, withStartupTask, type LeasedWorker } from '../lifecycle.js';
3
3
  import { BevelSecretsVariableLoader, registerBevelSecretsVariableLoader } from '../../modules/secrets-vault/secrets-variable-loader.js';
4
4
  import type { AdvisoryLease } from '../../modules/database/advisory-lock.js';
5
5
 
@@ -158,6 +158,31 @@ describe('holdCommitWorkerLease', () => {
158
158
  await handle.stop();
159
159
  expect(record.released).toBe(1);
160
160
  });
161
+
162
+ /**
163
+ * A process that never held the lease never ran the worker, and can still
164
+ * be in the middle of a hold on it: a repository move, using the database.
165
+ * Shutdown ends the pool after this stop, so this stop is what waits.
166
+ */
167
+ it('stop() waits for a hold in flight on a worker this process never ran', async () => {
168
+ const { lease, record } = fakeLease([false]);
169
+ const worker = holdable({ start: () => undefined, stop: async () => undefined });
170
+ const clock = manualSleep();
171
+ const handle = holdCommitWorkerLease(lease, worker, { sleep: clock.sleep, log: () => undefined });
172
+ await settle();
173
+
174
+ let finishMove: () => void = () => undefined;
175
+ const move = worker.whileHeld(() => new Promise<void>((resolve) => (finishMove = resolve)));
176
+ await settle();
177
+ const stopping = handle.stop();
178
+ await settle();
179
+ expect(record.released).toBe(0);
180
+
181
+ finishMove();
182
+ await move;
183
+ await stopping;
184
+ expect(record.released).toBe(1);
185
+ });
161
186
  });
162
187
 
163
188
  describe('withStartupTask', () => {
@@ -444,3 +469,147 @@ describe('stopCore', () => {
444
469
  expect(g.order).toEqual(['backgroundJobs.stopSweeping', 'startupRetry.stop', 'backgroundJobs.drain', 'db.end']);
445
470
  });
446
471
  });
472
+
473
+ /**
474
+ * Two independent reasons for the commit worker not to run: the lease loop
475
+ * does not want it (another process holds the queue), or something holds it
476
+ * still (the deployment is being moved to another repository). It runs
477
+ * exactly when it is wanted and nothing holds it.
478
+ */
479
+ describe('holdable', () => {
480
+ function recorded() {
481
+ const events: string[] = [];
482
+ const worker: LeasedWorker = {
483
+ start: () => void events.push('start'),
484
+ stop: async () => void events.push('stop'),
485
+ };
486
+ return { events, worker: holdable(worker) };
487
+ }
488
+
489
+ it('stops a running worker for the work, and starts it again afterwards', async () => {
490
+ const { events, worker } = recorded();
491
+ worker.start();
492
+ const answer = await worker.whileHeld(async () => {
493
+ events.push('work');
494
+ return 42;
495
+ });
496
+ expect(answer).toBe(42);
497
+ expect(events).toEqual(['start', 'stop', 'work', 'start']);
498
+ });
499
+
500
+ it('starts it again when the work fails, and hands the failure on', async () => {
501
+ const { events, worker } = recorded();
502
+ worker.start();
503
+ await expect(
504
+ worker.whileHeld(async () => {
505
+ throw new Error('the move failed');
506
+ }),
507
+ ).rejects.toThrow('the move failed');
508
+ expect(events).toEqual(['start', 'stop', 'start']);
509
+ });
510
+
511
+ it('does not start a worker the lease loop never wanted', async () => {
512
+ const { events, worker } = recorded();
513
+ await worker.whileHeld(async () => undefined);
514
+ expect(events).not.toContain('start');
515
+ });
516
+
517
+ it('holds back a start the lease loop asks for during the work, and makes it afterwards', async () => {
518
+ const { events, worker } = recorded();
519
+ await worker.whileHeld(async () => {
520
+ // The lease is taken while the move runs.
521
+ worker.start();
522
+ events.push('work');
523
+ });
524
+ expect(events).toEqual(['stop', 'work', 'start']);
525
+ });
526
+
527
+ it('leaves it stopped when the lease loop stopped it during the work', async () => {
528
+ const { events, worker } = recorded();
529
+ worker.start();
530
+ // The loop's stop waits for the hold, so it is not awaited from inside it.
531
+ let stopping: Promise<void> = Promise.resolve();
532
+ await worker.whileHeld(async () => {
533
+ stopping = worker.stop();
534
+ await settle();
535
+ });
536
+ await stopping;
537
+ expect(events.at(-1)).toBe('stop');
538
+ expect(events.filter((e) => e === 'start')).toHaveLength(1);
539
+ });
540
+
541
+ /**
542
+ * Shutdown awaits this stop before it ends the database pool. A move still
543
+ * setting copies aside needs that pool, so the stop does not come back
544
+ * while a hold is in flight.
545
+ */
546
+ it('does not finish stopping while a hold is still in flight', async () => {
547
+ const { worker } = recorded();
548
+ worker.start();
549
+ let finishMove: () => void = () => undefined;
550
+ const move = worker.whileHeld(() => new Promise<void>((resolve) => (finishMove = resolve)));
551
+ await settle();
552
+ let stopped = false;
553
+ const stopping = worker.stop().then(() => (stopped = true));
554
+ await settle();
555
+ expect(stopped).toBe(false);
556
+ finishMove();
557
+ await move;
558
+ await stopping;
559
+ expect(stopped).toBe(true);
560
+ });
561
+
562
+ it('finishes stopping when the hold in flight fails', async () => {
563
+ const { worker } = recorded();
564
+ let failMove: (err: Error) => void = () => undefined;
565
+ const move = worker.whileHeld(() => new Promise<void>((_, reject) => (failMove = reject)));
566
+ move.catch(() => undefined);
567
+ await settle();
568
+ const stopping = worker.stop();
569
+ failMove(new Error('the move failed'));
570
+ await expect(stopping).resolves.toBeUndefined();
571
+ });
572
+
573
+ /**
574
+ * A worker's stop answers a second caller at once while the first is
575
+ * still waiting for the commit in flight. A second hold given that answer
576
+ * would set a working copy aside under that commit.
577
+ */
578
+ it('makes a second hold wait for the stop the first one is still waiting on', async () => {
579
+ const events: string[] = [];
580
+ let stopped = false;
581
+ let finishStop: () => void = () => undefined;
582
+ const worker = holdable({
583
+ start: () => void events.push('start'),
584
+ // As the commit worker's: the first call waits, a later one returns.
585
+ stop: () => {
586
+ if (stopped) return Promise.resolve();
587
+ stopped = true;
588
+ return new Promise<void>((resolve) => (finishStop = resolve));
589
+ },
590
+ });
591
+ worker.start();
592
+ const first = worker.whileHeld(async () => void events.push('first work'));
593
+ const second = worker.whileHeld(async () => void events.push('second work'));
594
+ await settle();
595
+ // The commit in flight has not finished: neither hold has begun.
596
+ expect(events).toEqual(['start']);
597
+ finishStop();
598
+ await Promise.all([first, second]);
599
+ expect(events.slice(0, 3).sort()).toEqual(['first work', 'second work', 'start']);
600
+ });
601
+
602
+ it('waits for the last of two overlapping holds before it starts again', async () => {
603
+ const { events, worker } = recorded();
604
+ worker.start();
605
+ let finishFirst: () => void = () => undefined;
606
+ const first = worker.whileHeld(() => new Promise<void>((resolve) => (finishFirst = resolve)));
607
+ await settle();
608
+ await worker.whileHeld(async () => undefined);
609
+ // The second hold ended and the first is still running: not started yet.
610
+ expect(events.filter((e) => e === 'start')).toHaveLength(1);
611
+ finishFirst();
612
+ await first;
613
+ expect(events.filter((e) => e === 'start')).toHaveLength(2);
614
+ });
615
+ });
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { setAsideRootFor } from '../../modules/workspace/set-aside-clone.js';
6
+
7
+ /**
8
+ * A working copy of a repository that is no longer the configured one is SET
9
+ * ASIDE rather than deleted, and two places do it: the KB startup phase,
10
+ * sweeping the whole workspaces root, and the workspace service, refusing to
11
+ * adopt one on a branch open. They must set aside into ONE directory.
12
+ *
13
+ * They did not. The composition root gave the phase a root under the backups
14
+ * volume and the workspace service nothing at all, so that one fell back to a
15
+ * sibling of the workspaces root — which in the shipped compose file is the
16
+ * container's own filesystem, not a volume. A copy the workspace service kept
17
+ * was therefore gone at the next ordinary recreate, after the setup screen had
18
+ * told the admin it was being kept and where to find it.
19
+ *
20
+ * Read off the source, like `migrations-unqualified.test.ts` reads the
21
+ * migrations: the wiring is a single call graph that cannot be built in a unit
22
+ * test (it wants a database, a git host and a disk), and the invariant is
23
+ * about the TEXT — that the folder is named once and that one name reaches
24
+ * both. A second literal is exactly how the two came apart.
25
+ */
26
+
27
+ const compositionRoot = path.resolve(
28
+ path.dirname(fileURLToPath(import.meta.url)),
29
+ '../create-core-services.ts',
30
+ );
31
+
32
+ describe('the set-aside root the composition root wires', () => {
33
+ it('is named once, and handed to both the startup phase and the workspace service', async () => {
34
+ const source = await fs.readFile(compositionRoot, 'utf8');
35
+
36
+ // Named once. Two occurrences means two expressions, which is how a
37
+ // change to one of them silently stops applying to the other.
38
+ const literals = source.match(/'replaced-working-copies'/g) ?? [];
39
+ expect(literals).toHaveLength(1);
40
+
41
+ // And that one name is rooted at the backups volume, which the deployment
42
+ // mounts — not at the workspaces root's parent, which it does not.
43
+ expect(source).toMatch(
44
+ /const replacedWorkingCopiesRoot = path\.join\(config\.backupsRoot, 'replaced-working-copies'\)/,
45
+ );
46
+
47
+ // Both consumers read the one variable.
48
+ expect(source).toMatch(/new WorkspaceService\([\s\S]*?replacedWorkingCopiesRoot,[\s\S]*?\);/);
49
+ expect(source).toMatch(/setAsideRoot: replacedWorkingCopiesRoot,/);
50
+ });
51
+ });
52
+
53
+ describe('setAsideRootFor', () => {
54
+ it('uses the root it is given, whatever the workspaces root is', () => {
55
+ expect(setAsideRootFor('/app/apps/server/workspaces', '/app/apps/server/backups/replaced-working-copies')).toBe(
56
+ '/app/apps/server/backups/replaced-working-copies',
57
+ );
58
+ });
59
+
60
+ it('falls back beside the workspaces root — a place a container deployment must not rely on', () => {
61
+ expect(setAsideRootFor('/app/apps/server/workspaces')).toBe('/app/apps/server/replaced-working-copies');
62
+ });
63
+ });
@@ -7,6 +7,7 @@ import type { ILlmUsageMeter } from '../modules/tool-auth/llm-usage-meter.js';
7
7
  import type { AuthProviderPlugin } from '../modules/auth/auth.routes.js';
8
8
  import type { IErasureParticipant } from '../modules/auth/account-erasure.service.js';
9
9
  import type { IAccountAdmission } from '../modules/auth/account-admission.js';
10
+ import type { IWriteAccess } from '../modules/write-access/write-access.js';
10
11
  import type { OnServerStart } from '../modules/workspace/startup/on-server-start.js';
11
12
 
12
13
  /**
@@ -34,7 +35,7 @@ import type { OnServerStart } from '../modules/workspace/startup/on-server-start
34
35
  * enterprise root overwrites the `CoreServices` fields (or pushes into the
35
36
  * `authProviders` array) after construction, before the server is built.
36
37
  *
37
- * (Commit-time KB validation and the ontology write block are NOT ports:
38
+ * (Commit-time KB validation and the agent read/write hooks are NOT ports:
38
39
  * they are workflow lifecycle HOOKS the enterprise root registers on
39
40
  * `workflowService.hooks` after construction — see
40
41
  * `modules/workflow/workflow-hooks.ts`.)
@@ -117,6 +118,15 @@ export interface CorePorts {
117
118
  * admin who asked. See `modules/auth/account-admission.ts`.
118
119
  */
119
120
  accountAdmission?: IAccountAdmission;
121
+ /**
122
+ * Whether the deployment may be changed right now. Asked before every
123
+ * mutating route (bar the ones that sign people in, manage accounts or
124
+ * configure the deployment) and every write tool. Core default: always.
125
+ * A host that sells seats answers no while more accounts are on than its
126
+ * plan allows, so the workspace is read-only until an admin fixes it. See
127
+ * `modules/write-access/write-access.ts`.
128
+ */
129
+ writeAccess?: IWriteAccess;
120
130
  }
121
131
 
122
132
  /**