@bevel-software/platform-core-backend 0.22.0 → 0.24.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 (466) 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 +46 -13
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +12 -2
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +137 -24
  11. package/dist/core/create-core-services.js.map +1 -1
  12. package/dist/core/lifecycle.d.ts +46 -1
  13. package/dist/core/lifecycle.d.ts.map +1 -1
  14. package/dist/core/lifecycle.js +99 -13
  15. package/dist/core/lifecycle.js.map +1 -1
  16. package/dist/core-config.d.ts +16 -12
  17. package/dist/core-config.d.ts.map +1 -1
  18. package/dist/core-config.js +28 -13
  19. package/dist/core-config.js.map +1 -1
  20. package/dist/index.d.ts +6 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +12 -3
  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.routes.d.ts.map +1 -1
  31. package/dist/modules/access/access.routes.js +17 -16
  32. package/dist/modules/access/access.routes.js.map +1 -1
  33. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
  34. package/dist/modules/access/directory-sync-bot.js +7 -3
  35. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  36. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +8 -1
  37. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
  38. package/dist/modules/agent-instructions/agent-instructions.routes.js +8 -2
  39. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
  40. package/dist/modules/agent-instructions/compose.d.ts +38 -6
  41. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  42. package/dist/modules/agent-instructions/compose.js +39 -6
  43. package/dist/modules/agent-instructions/compose.js.map +1 -1
  44. package/dist/modules/agent-instructions/index.d.ts +2 -1
  45. package/dist/modules/agent-instructions/index.d.ts.map +1 -1
  46. package/dist/modules/agent-instructions/index.js +2 -1
  47. package/dist/modules/agent-instructions/index.js.map +1 -1
  48. package/dist/modules/agent-instructions/shared-file-rules.d.ts +115 -0
  49. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -0
  50. package/dist/modules/agent-instructions/shared-file-rules.js +272 -0
  51. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -0
  52. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  53. package/dist/modules/audit/agent-audit.service.js +4 -2
  54. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  55. package/dist/modules/auth/account-admission.d.ts +58 -7
  56. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  57. package/dist/modules/auth/account-admission.js +44 -1
  58. package/dist/modules/auth/account-admission.js.map +1 -1
  59. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
  60. package/dist/modules/auth/account-erasure.service.js +57 -16
  61. package/dist/modules/auth/account-erasure.service.js.map +1 -1
  62. package/dist/modules/auth/account.routes.d.ts +1 -1
  63. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  64. package/dist/modules/auth/account.routes.js +61 -5
  65. package/dist/modules/auth/account.routes.js.map +1 -1
  66. package/dist/modules/auth/auth.middleware.d.ts +1 -1
  67. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  68. package/dist/modules/auth/auth.middleware.js +18 -7
  69. package/dist/modules/auth/auth.middleware.js.map +1 -1
  70. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  71. package/dist/modules/auth/auth.routes.js +8 -0
  72. package/dist/modules/auth/auth.routes.js.map +1 -1
  73. package/dist/modules/auth/auth.service.d.ts +70 -2
  74. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  75. package/dist/modules/auth/auth.service.js +197 -18
  76. package/dist/modules/auth/auth.service.js.map +1 -1
  77. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  78. package/dist/modules/auth/oidc-auth-provider.js +7 -2
  79. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  80. package/dist/modules/code-mode/code-mode.tool.d.ts +20 -2
  81. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  82. package/dist/modules/code-mode/code-mode.tool.js +66 -35
  83. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  84. package/dist/modules/database/connection.d.ts +16 -0
  85. package/dist/modules/database/connection.d.ts.map +1 -1
  86. package/dist/modules/database/connection.js +117 -0
  87. package/dist/modules/database/connection.js.map +1 -1
  88. package/dist/modules/database/core-schema.d.ts +337 -120
  89. package/dist/modules/database/core-schema.d.ts.map +1 -1
  90. package/dist/modules/database/core-schema.js +116 -58
  91. package/dist/modules/database/core-schema.js.map +1 -1
  92. package/dist/modules/database/migrate.d.ts +8 -0
  93. package/dist/modules/database/migrate.d.ts.map +1 -1
  94. package/dist/modules/database/migrate.js +271 -1
  95. package/dist/modules/database/migrate.js.map +1 -1
  96. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  97. package/dist/modules/kb-fs/branch-name.js +12 -2
  98. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  99. package/dist/modules/kb-fs/repo-path.d.ts +11 -16
  100. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  101. package/dist/modules/kb-fs/repo-path.js +79 -0
  102. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  103. package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
  104. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  105. package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
  106. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  107. package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
  108. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
  109. package/dist/modules/kb-sync/sync-auth.js +1 -1
  110. package/dist/modules/kb-sync/sync-auth.js.map +1 -1
  111. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  112. package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
  113. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  114. package/dist/modules/mcp/mcp.service.d.ts +8 -0
  115. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  116. package/dist/modules/mcp/mcp.service.js +38 -8
  117. package/dist/modules/mcp/mcp.service.js.map +1 -1
  118. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  119. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
  120. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  121. package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -1
  122. package/dist/modules/plugins/join-request-records.store.js +7 -4
  123. package/dist/modules/plugins/join-request-records.store.js.map +1 -1
  124. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  125. package/dist/modules/settings/deployment-settings.service.js +8 -3
  126. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  127. package/dist/modules/settings/setup.routes.d.ts +52 -1
  128. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  129. package/dist/modules/settings/setup.routes.js +207 -19
  130. package/dist/modules/settings/setup.routes.js.map +1 -1
  131. package/dist/modules/skills/skills.contract.d.ts +90 -18
  132. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  133. package/dist/modules/skills/skills.contract.js +4 -1
  134. package/dist/modules/skills/skills.contract.js.map +1 -1
  135. package/dist/modules/skills/skills.service.d.ts +98 -9
  136. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  137. package/dist/modules/skills/skills.service.js +239 -37
  138. package/dist/modules/skills/skills.service.js.map +1 -1
  139. package/dist/modules/skills/skills.tools.d.ts +7 -0
  140. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  141. package/dist/modules/skills/skills.tools.js +71 -7
  142. package/dist/modules/skills/skills.tools.js.map +1 -1
  143. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  144. package/dist/modules/tool-auth/external-api-key.service.js +12 -4
  145. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  146. package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
  147. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
  148. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  149. package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
  150. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  151. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
  152. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  153. package/dist/modules/tool-helpers/tool-handler.js +25 -4
  154. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  155. package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
  156. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  157. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  158. package/dist/modules/tool-helpers/validate-token.js +1 -1
  159. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  160. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  161. package/dist/modules/tool-manuals/tool-manuals.tools.js +42 -31
  162. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  163. package/dist/modules/tool-registry/description-length.d.ts +80 -0
  164. package/dist/modules/tool-registry/description-length.d.ts.map +1 -0
  165. package/dist/modules/tool-registry/description-length.js +108 -0
  166. package/dist/modules/tool-registry/description-length.js.map +1 -0
  167. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
  168. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
  169. package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
  170. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
  171. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  172. package/dist/modules/workflow/agent-tools/workflow.tools.js +75 -35
  173. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  174. package/dist/modules/workflow/file-lock.service.d.ts +24 -0
  175. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  176. package/dist/modules/workflow/file-lock.service.js +30 -0
  177. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  178. package/dist/modules/workflow/git/git.service.d.ts +94 -0
  179. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  180. package/dist/modules/workflow/git/git.service.js +193 -3
  181. package/dist/modules/workflow/git/git.service.js.map +1 -1
  182. package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
  183. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  184. package/dist/modules/workflow/pending-commits.service.js +60 -1
  185. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  186. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
  187. package/dist/modules/workflow/recovery-bot.js +7 -3
  188. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  189. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  190. package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
  191. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  192. package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
  193. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
  194. package/dist/modules/workflow/workflow-hooks.js +16 -1
  195. package/dist/modules/workflow/workflow-hooks.js.map +1 -1
  196. package/dist/modules/workflow/workflow.service.d.ts +60 -6
  197. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  198. package/dist/modules/workflow/workflow.service.js +115 -5
  199. package/dist/modules/workflow/workflow.service.js.map +1 -1
  200. package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
  201. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
  202. package/dist/modules/workspace/agent-access.gate.js +123 -0
  203. package/dist/modules/workspace/agent-access.gate.js.map +1 -0
  204. package/dist/modules/workspace/agent-upload.routes.d.ts +77 -0
  205. package/dist/modules/workspace/agent-upload.routes.d.ts.map +1 -0
  206. package/dist/modules/workspace/agent-upload.routes.js +210 -0
  207. package/dist/modules/workspace/agent-upload.routes.js.map +1 -0
  208. package/dist/modules/workspace/agent-upload.store.d.ts +284 -0
  209. package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -0
  210. package/dist/modules/workspace/agent-upload.store.js +553 -0
  211. package/dist/modules/workspace/agent-upload.store.js.map +1 -0
  212. package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
  213. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  214. package/dist/modules/workspace/routine-write-policy.js +5 -6
  215. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  216. package/dist/modules/workspace/session-sink.d.ts +5 -5
  217. package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
  218. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
  219. package/dist/modules/workspace/set-aside-clone.js +92 -0
  220. package/dist/modules/workspace/set-aside-clone.js.map +1 -0
  221. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +59 -9
  222. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  223. package/dist/modules/workspace/startup/kb-startup-runner.js +65 -24
  224. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  225. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  226. package/dist/modules/workspace/startup/steps/seed-tree.js +3 -3
  227. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  228. package/dist/modules/workspace/startup/steps/template-source.d.ts +40 -0
  229. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  230. package/dist/modules/workspace/startup/steps/template-source.js +46 -4
  231. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  232. package/dist/modules/workspace/upload-limits.d.ts +13 -0
  233. package/dist/modules/workspace/upload-limits.d.ts.map +1 -0
  234. package/dist/modules/workspace/upload-limits.js +13 -0
  235. package/dist/modules/workspace/upload-limits.js.map +1 -0
  236. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  237. package/dist/modules/workspace/workspace.routes.js +93 -4
  238. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  239. package/dist/modules/workspace/workspace.service.d.ts +128 -5
  240. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  241. package/dist/modules/workspace/workspace.service.js +316 -58
  242. package/dist/modules/workspace/workspace.service.js.map +1 -1
  243. package/dist/modules/workspace/workspace.tools.d.ts +13 -11
  244. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  245. package/dist/modules/workspace/workspace.tools.js +791 -198
  246. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  247. package/dist/modules/workspace/write-denial.d.ts +0 -6
  248. package/dist/modules/workspace/write-denial.d.ts.map +1 -1
  249. package/dist/modules/workspace/write-denial.js +0 -6
  250. package/dist/modules/workspace/write-denial.js.map +1 -1
  251. package/dist/modules/workspace/zip-entry-rules.d.ts +114 -0
  252. package/dist/modules/workspace/zip-entry-rules.d.ts.map +1 -0
  253. package/dist/modules/workspace/zip-entry-rules.js +154 -0
  254. package/dist/modules/workspace/zip-entry-rules.js.map +1 -0
  255. package/dist/modules/write-access/write-access.d.ts +60 -0
  256. package/dist/modules/write-access/write-access.d.ts.map +1 -0
  257. package/dist/modules/write-access/write-access.js +129 -0
  258. package/dist/modules/write-access/write-access.js.map +1 -0
  259. package/dist/shared/column-crypto.d.ts +194 -0
  260. package/dist/shared/column-crypto.d.ts.map +1 -0
  261. package/dist/shared/column-crypto.js +144 -0
  262. package/dist/shared/column-crypto.js.map +1 -0
  263. package/dist/shared/domain-errors.d.ts +25 -0
  264. package/dist/shared/domain-errors.d.ts.map +1 -1
  265. package/dist/shared/domain-errors.js +28 -0
  266. package/dist/shared/domain-errors.js.map +1 -1
  267. package/dist/shared/git.contract.d.ts +20 -0
  268. package/dist/shared/git.contract.d.ts.map +1 -1
  269. package/dist/shared/git.contract.js +26 -0
  270. package/dist/shared/git.contract.js.map +1 -1
  271. package/dist/shared/token-crypto.d.ts.map +1 -1
  272. package/dist/shared/token-crypto.js +25 -1
  273. package/dist/shared/token-crypto.js.map +1 -1
  274. package/dist/tenancy/static-tenant-source.d.ts +0 -1
  275. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  276. package/dist/tenancy/static-tenant-source.js +1 -2
  277. package/dist/tenancy/static-tenant-source.js.map +1 -1
  278. package/dist/tenancy/tenant-secrets.d.ts +5 -1
  279. package/dist/tenancy/tenant-secrets.d.ts.map +1 -1
  280. package/dist/tenancy/tenant-secrets.js +4 -0
  281. package/dist/tenancy/tenant-secrets.js.map +1 -1
  282. package/kb-template/AGENTS.md +2 -0
  283. package/migrations/0014_change_request_closed_reason.sql +1 -0
  284. package/migrations/0015_account_deactivation.sql +3 -0
  285. package/migrations/0016_pii_encryption.sql +20 -0
  286. package/migrations/meta/0014_snapshot.json +2265 -0
  287. package/migrations/meta/0015_snapshot.json +2271 -0
  288. package/migrations/meta/0016_snapshot.json +2327 -0
  289. package/migrations/meta/_journal.json +21 -0
  290. package/package.json +3 -3
  291. package/src/__tests__/kb-layout-config.test.ts +0 -2
  292. package/src/__tests__/retired-settings.test.ts +96 -0
  293. package/src/core/__tests__/gated-boot.test.ts +132 -0
  294. package/src/core/__tests__/lifecycle.test.ts +241 -5
  295. package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
  296. package/src/core/core-ports.ts +11 -1
  297. package/src/core/create-core-server.ts +52 -16
  298. package/src/core/create-core-services.ts +156 -24
  299. package/src/core/lifecycle.ts +117 -13
  300. package/src/core-config.ts +31 -14
  301. package/src/index.ts +30 -1
  302. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
  303. package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
  304. package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
  305. package/src/modules/access/__tests__/users-db-double.ts +21 -12
  306. package/src/modules/access/access-control.interface.ts +43 -12
  307. package/src/modules/access/access-control.service.ts +234 -36
  308. package/src/modules/access/access.routes.ts +17 -16
  309. package/src/modules/access/directory-sync-bot.ts +7 -3
  310. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +11 -5
  311. package/src/modules/agent-instructions/__tests__/compose.test.ts +19 -10
  312. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +238 -0
  313. package/src/modules/agent-instructions/agent-instructions.routes.ts +12 -2
  314. package/src/modules/agent-instructions/compose.ts +50 -7
  315. package/src/modules/agent-instructions/index.ts +12 -0
  316. package/src/modules/agent-instructions/shared-file-rules.ts +314 -0
  317. package/src/modules/audit/agent-audit.service.ts +4 -2
  318. package/src/modules/auth/__tests__/account-deactivation.test.ts +253 -0
  319. package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +6 -2
  320. package/src/modules/auth/__tests__/account.routes.test.ts +87 -6
  321. package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
  322. package/src/modules/auth/__tests__/auth.service.test.ts +4 -2
  323. package/src/modules/auth/account-admission.ts +78 -9
  324. package/src/modules/auth/account-erasure.service.ts +69 -18
  325. package/src/modules/auth/account.routes.ts +63 -7
  326. package/src/modules/auth/auth.middleware.ts +19 -8
  327. package/src/modules/auth/auth.routes.ts +8 -0
  328. package/src/modules/auth/auth.service.ts +206 -23
  329. package/src/modules/auth/oidc-auth-provider.ts +7 -2
  330. package/src/modules/code-mode/__tests__/chain-runtime.e2e.test.ts +335 -0
  331. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +46 -2
  332. package/src/modules/code-mode/code-mode.tool.ts +80 -34
  333. package/src/modules/database/__tests__/connection.test.ts +12 -0
  334. package/src/modules/database/__tests__/pii-backfill.pg.test.ts +561 -0
  335. package/src/modules/database/connection.ts +117 -0
  336. package/src/modules/database/core-schema.ts +116 -58
  337. package/src/modules/database/migrate.ts +353 -1
  338. package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
  339. package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
  340. package/src/modules/kb-fs/branch-name.ts +14 -1
  341. package/src/modules/kb-fs/repo-path.ts +85 -0
  342. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
  343. package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
  344. package/src/modules/kb-sync/sync-auth.ts +2 -2
  345. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
  346. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +4 -3
  347. package/src/modules/mcp/__tests__/mcp.service.test.ts +115 -13
  348. package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
  349. package/src/modules/mcp/mcp.service.ts +46 -7
  350. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
  351. package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
  352. package/src/modules/plugins/join-request-records.store.ts +7 -4
  353. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
  354. package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +91 -58
  355. package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +25 -8
  356. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +14 -11
  357. package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
  358. package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
  359. package/src/modules/settings/deployment-settings.service.ts +8 -3
  360. package/src/modules/settings/setup.routes.ts +263 -19
  361. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
  362. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
  363. package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
  364. package/src/modules/skills/skills.contract.ts +91 -18
  365. package/src/modules/skills/skills.service.ts +278 -41
  366. package/src/modules/skills/skills.tools.ts +80 -8
  367. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
  368. package/src/modules/tool-auth/external-api-key.service.ts +12 -4
  369. package/src/modules/tool-auth/internal-token.service.ts +3 -3
  370. package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
  371. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
  372. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
  373. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
  374. package/src/modules/tool-helpers/tool-handler.ts +24 -4
  375. package/src/modules/tool-helpers/tool.contract.ts +3 -2
  376. package/src/modules/tool-helpers/validate-token.ts +1 -1
  377. package/src/modules/tool-manuals/tool-manuals.tools.ts +44 -31
  378. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +378 -0
  379. package/src/modules/tool-registry/description-length.ts +111 -0
  380. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
  381. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
  382. package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
  383. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +264 -6
  384. package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
  385. package/src/modules/workflow/agent-tools/workflow.tools.ts +86 -35
  386. package/src/modules/workflow/file-lock.service.ts +31 -0
  387. package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
  388. package/src/modules/workflow/git/__tests__/git.service.prFetchFailure.test.ts +170 -0
  389. package/src/modules/workflow/git/git.service.ts +215 -1
  390. package/src/modules/workflow/pending-commits.service.ts +64 -2
  391. package/src/modules/workflow/recovery-bot.ts +7 -3
  392. package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +4 -0
  393. package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +25 -3
  394. package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
  395. package/src/modules/workflow/workflow-hooks.ts +64 -26
  396. package/src/modules/workflow/workflow.service.ts +125 -5
  397. package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
  398. package/src/modules/workspace/__tests__/agent-uploads.test.ts +1604 -0
  399. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +394 -0
  400. package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
  401. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
  402. package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
  403. package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
  404. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
  405. package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +156 -0
  406. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
  407. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +402 -0
  408. package/src/modules/workspace/__tests__/workspace.service.test.ts +77 -9
  409. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +41 -39
  410. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
  411. package/src/modules/workspace/__tests__/workspace.tools.test.ts +766 -56
  412. package/src/modules/workspace/agent-access.gate.ts +164 -0
  413. package/src/modules/workspace/agent-upload.routes.ts +214 -0
  414. package/src/modules/workspace/agent-upload.store.ts +668 -0
  415. package/src/modules/workspace/routine-write-policy.ts +5 -6
  416. package/src/modules/workspace/session-sink.ts +5 -5
  417. package/src/modules/workspace/set-aside-clone.ts +96 -0
  418. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +86 -0
  419. package/src/modules/workspace/startup/kb-startup-runner.ts +115 -34
  420. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +9 -9
  421. package/src/modules/workspace/startup/steps/seed-tree.ts +3 -6
  422. package/src/modules/workspace/startup/steps/template-source.ts +53 -5
  423. package/src/modules/workspace/upload-limits.ts +12 -0
  424. package/src/modules/workspace/workspace.routes.ts +96 -5
  425. package/src/modules/workspace/workspace.service.ts +319 -61
  426. package/src/modules/workspace/workspace.tools.ts +894 -214
  427. package/src/modules/workspace/write-denial.ts +0 -8
  428. package/src/modules/workspace/zip-entry-rules.ts +173 -0
  429. package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
  430. package/src/modules/write-access/write-access.ts +153 -0
  431. package/src/shared/__tests__/column-crypto.test.ts +217 -0
  432. package/src/shared/column-crypto.ts +218 -0
  433. package/src/shared/domain-errors.ts +31 -0
  434. package/src/shared/git.contract.ts +27 -0
  435. package/src/shared/token-crypto.ts +28 -1
  436. package/src/tenancy/__tests__/static-tenant-source.test.ts +4 -1
  437. package/src/tenancy/static-tenant-source.ts +1 -3
  438. package/src/tenancy/tenant-secrets.ts +5 -1
  439. package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
  440. package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
  441. package/dist/modules/workflow/session-ontology.policy.js +0 -62
  442. package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
  443. package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
  444. package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
  445. package/dist/modules/workflow/session-ontology.service.js +0 -147
  446. package/dist/modules/workflow/session-ontology.service.js.map +0 -1
  447. package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
  448. package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
  449. package/dist/modules/workspace/session-ontology.gate.js +0 -161
  450. package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
  451. package/dist/shared/kb-layout.d.ts +0 -39
  452. package/dist/shared/kb-layout.d.ts.map +0 -1
  453. package/dist/shared/kb-layout.js +0 -103
  454. package/dist/shared/kb-layout.js.map +0 -1
  455. package/dist/shared/kb-layout.test.d.ts +0 -2
  456. package/dist/shared/kb-layout.test.d.ts.map +0 -1
  457. package/dist/shared/kb-layout.test.js +0 -75
  458. package/dist/shared/kb-layout.test.js.map +0 -1
  459. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
  460. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
  461. package/src/modules/workflow/session-ontology.policy.ts +0 -70
  462. package/src/modules/workflow/session-ontology.service.ts +0 -183
  463. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
  464. package/src/modules/workspace/session-ontology.gate.ts +0 -191
  465. package/src/shared/kb-layout.test.ts +0 -98
  466. package/src/shared/kb-layout.ts +0 -102
@@ -99,6 +99,27 @@
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
116
+ },
117
+ {
118
+ "idx": 16,
119
+ "version": "7",
120
+ "when": 1790933605784,
121
+ "tag": "0016_pii_encryption",
122
+ "breakpoints": true
102
123
  }
103
124
  ]
104
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bevel-software/platform-core-backend",
3
- "version": "0.22.0",
3
+ "version": "0.24.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.22.0",
56
- "@bevel-software/platform-shared": "0.22.0"
55
+ "@bevel-software/platform-mcp-core": "0.24.0",
56
+ "@bevel-software/platform-shared": "0.24.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', () => {
@@ -237,6 +262,14 @@ describe('createShutdown', () => {
237
262
  order.push('backgroundJobs.drain');
238
263
  },
239
264
  },
265
+ agentUploadStore: {
266
+ stopSweeping() {
267
+ order.push('agentUploadStore.stopSweeping');
268
+ },
269
+ async drainSweep() {
270
+ order.push('agentUploadStore.drainSweep');
271
+ },
272
+ },
240
273
  db: {
241
274
  $client: {
242
275
  async end() {
@@ -264,7 +297,12 @@ describe('createShutdown', () => {
264
297
  'server.close',
265
298
  'server.closeAllConnections',
266
299
  'backgroundJobs.stopSweeping',
300
+ // The upload sweep is stopped in the same breath as the other timers,
301
+ // and drained before the pool goes: a graph that is stopping must not
302
+ // leave a sweep walking the staging root its replacement will stage into.
303
+ 'agentUploadStore.stopSweeping',
267
304
  'backgroundJobs.drain',
305
+ 'agentUploadStore.drainSweep',
268
306
  'commitWorker.stop',
269
307
  'db.end',
270
308
  ]);
@@ -285,7 +323,13 @@ describe('createShutdown', () => {
285
323
  d.finishClose();
286
324
  await settle();
287
325
  // The jobs are still running: nothing after them has run.
288
- expect(d.order).toEqual(['server.close', 'server.closeAllConnections', 'backgroundJobs.stopSweeping', 'backgroundJobs.drain']);
326
+ expect(d.order).toEqual([
327
+ 'server.close',
328
+ 'server.closeAllConnections',
329
+ 'backgroundJobs.stopSweeping',
330
+ 'agentUploadStore.stopSweeping',
331
+ 'backgroundJobs.drain',
332
+ ]);
289
333
  release();
290
334
  await done;
291
335
  expect(d.order.slice(-2)).toEqual(['commitWorker.stop', 'db.end']);
@@ -314,6 +358,7 @@ describe('createShutdown', () => {
314
358
  const d = deps();
315
359
  // A stop that lands mid-boot: the services exist, the jobs do not.
316
360
  delete (d.deps as { backgroundJobs?: unknown }).backgroundJobs;
361
+ delete (d.deps as { agentUploadStore?: unknown }).agentUploadStore;
317
362
  const shutdown = createShutdown(d.deps as never);
318
363
  const done = shutdown('SIGTERM');
319
364
  await settle();
@@ -338,7 +383,12 @@ describe('createShutdown', () => {
338
383
  await settle();
339
384
  d.finishClose();
340
385
  await done;
341
- expect(d.order.slice(-3)).toEqual(['backgroundJobs.drain', 'commitWorker.stop', 'db.end']);
386
+ expect(d.order.slice(-4)).toEqual([
387
+ 'backgroundJobs.drain',
388
+ 'agentUploadStore.drainSweep',
389
+ 'commitWorker.stop',
390
+ 'db.end',
391
+ ]);
342
392
  });
343
393
 
344
394
  it('does not hang on a step that never finishes, and still runs the rest', async () => {
@@ -351,7 +401,9 @@ describe('createShutdown', () => {
351
401
  'server.close',
352
402
  'server.closeAllConnections',
353
403
  'backgroundJobs.stopSweeping',
404
+ 'agentUploadStore.stopSweeping',
354
405
  'backgroundJobs.drain',
406
+ 'agentUploadStore.drainSweep',
355
407
  'commitWorker.stop',
356
408
  'db.end',
357
409
  ]);
@@ -388,6 +440,14 @@ describe('stopCore', () => {
388
440
  order.push('backgroundJobs.drain');
389
441
  },
390
442
  },
443
+ agentUploadStore: {
444
+ stopSweeping() {
445
+ order.push('agentUploadStore.stopSweeping');
446
+ },
447
+ async drainSweep() {
448
+ order.push('agentUploadStore.drainSweep');
449
+ },
450
+ },
391
451
  startupRetry: {
392
452
  stop() {
393
453
  order.push('startupRetry.stop');
@@ -411,13 +471,31 @@ describe('stopCore', () => {
411
471
  await stopCore(g.core as never);
412
472
  expect(g.order).toEqual([
413
473
  'backgroundJobs.stopSweeping',
474
+ 'agentUploadStore.stopSweeping',
414
475
  'startupRetry.stop',
415
476
  'backgroundJobs.drain',
477
+ 'agentUploadStore.drainSweep',
416
478
  'commitWorker.stop',
417
479
  'db.end',
418
480
  ]);
419
481
  });
420
482
 
483
+ it('stops the upload sweep, so an evicted tenant cannot sweep its own replacement', async () => {
484
+ const g = graph();
485
+ await stopCore(g.core as never);
486
+ // This is the eviction path: the next activation of the same tenant builds
487
+ // a new store over the SAME staging directory with an empty record map, so
488
+ // a sweep left running from this graph would read the replacement's files
489
+ // as orphans and delete them under an apply about to read them. Both
490
+ // halves, in order — the timer cleared, then the sweep in flight awaited
491
+ // before anything else lets go.
492
+ const stopped = g.order.indexOf('agentUploadStore.stopSweeping');
493
+ const drained = g.order.indexOf('agentUploadStore.drainSweep');
494
+ expect(stopped).toBeGreaterThanOrEqual(0);
495
+ expect(stopped).toBeLessThan(drained);
496
+ expect(drained).toBeLessThan(g.order.indexOf('db.end'));
497
+ });
498
+
421
499
  it('forgets the graph\'s secrets scope, so a descriptor that outlives it resolves nothing', async () => {
422
500
  const g = graph();
423
501
  registerBevelSecretsVariableLoader(
@@ -434,13 +512,171 @@ describe('stopCore', () => {
434
512
  const g = graph();
435
513
  const core = { ...g.core, startupRetry: null, secretsScope: undefined };
436
514
  await stopCore(core as never);
437
- expect(g.order).toEqual(['backgroundJobs.stopSweeping', 'backgroundJobs.drain', 'commitWorker.stop', 'db.end']);
515
+ expect(g.order).toEqual([
516
+ 'backgroundJobs.stopSweeping',
517
+ 'agentUploadStore.stopSweeping',
518
+ 'backgroundJobs.drain',
519
+ 'agentUploadStore.drainSweep',
520
+ 'commitWorker.stop',
521
+ 'db.end',
522
+ ]);
438
523
  });
439
524
 
440
525
  it('does not wait past the budget for a worker that never stops, and still ends the pool', async () => {
441
526
  const g = graph();
442
527
  const core = { ...g.core, commitWorker: { stop: () => new Promise<void>(() => undefined) } };
443
528
  await stopCore(core as never, { deadlineMs: 50 });
444
- expect(g.order).toEqual(['backgroundJobs.stopSweeping', 'startupRetry.stop', 'backgroundJobs.drain', 'db.end']);
529
+ expect(g.order).toEqual([
530
+ 'backgroundJobs.stopSweeping',
531
+ 'agentUploadStore.stopSweeping',
532
+ 'startupRetry.stop',
533
+ 'backgroundJobs.drain',
534
+ 'agentUploadStore.drainSweep',
535
+ 'db.end',
536
+ ]);
537
+ });
538
+ });
539
+
540
+ /**
541
+ * Two independent reasons for the commit worker not to run: the lease loop
542
+ * does not want it (another process holds the queue), or something holds it
543
+ * still (the deployment is being moved to another repository). It runs
544
+ * exactly when it is wanted and nothing holds it.
545
+ */
546
+ describe('holdable', () => {
547
+ function recorded() {
548
+ const events: string[] = [];
549
+ const worker: LeasedWorker = {
550
+ start: () => void events.push('start'),
551
+ stop: async () => void events.push('stop'),
552
+ };
553
+ return { events, worker: holdable(worker) };
554
+ }
555
+
556
+ it('stops a running worker for the work, and starts it again afterwards', async () => {
557
+ const { events, worker } = recorded();
558
+ worker.start();
559
+ const answer = await worker.whileHeld(async () => {
560
+ events.push('work');
561
+ return 42;
562
+ });
563
+ expect(answer).toBe(42);
564
+ expect(events).toEqual(['start', 'stop', 'work', 'start']);
565
+ });
566
+
567
+ it('starts it again when the work fails, and hands the failure on', async () => {
568
+ const { events, worker } = recorded();
569
+ worker.start();
570
+ await expect(
571
+ worker.whileHeld(async () => {
572
+ throw new Error('the move failed');
573
+ }),
574
+ ).rejects.toThrow('the move failed');
575
+ expect(events).toEqual(['start', 'stop', 'start']);
576
+ });
577
+
578
+ it('does not start a worker the lease loop never wanted', async () => {
579
+ const { events, worker } = recorded();
580
+ await worker.whileHeld(async () => undefined);
581
+ expect(events).not.toContain('start');
582
+ });
583
+
584
+ it('holds back a start the lease loop asks for during the work, and makes it afterwards', async () => {
585
+ const { events, worker } = recorded();
586
+ await worker.whileHeld(async () => {
587
+ // The lease is taken while the move runs.
588
+ worker.start();
589
+ events.push('work');
590
+ });
591
+ expect(events).toEqual(['stop', 'work', 'start']);
592
+ });
593
+
594
+ it('leaves it stopped when the lease loop stopped it during the work', async () => {
595
+ const { events, worker } = recorded();
596
+ worker.start();
597
+ // The loop's stop waits for the hold, so it is not awaited from inside it.
598
+ let stopping: Promise<void> = Promise.resolve();
599
+ await worker.whileHeld(async () => {
600
+ stopping = worker.stop();
601
+ await settle();
602
+ });
603
+ await stopping;
604
+ expect(events.at(-1)).toBe('stop');
605
+ expect(events.filter((e) => e === 'start')).toHaveLength(1);
606
+ });
607
+
608
+ /**
609
+ * Shutdown awaits this stop before it ends the database pool. A move still
610
+ * setting copies aside needs that pool, so the stop does not come back
611
+ * while a hold is in flight.
612
+ */
613
+ it('does not finish stopping while a hold is still in flight', async () => {
614
+ const { worker } = recorded();
615
+ worker.start();
616
+ let finishMove: () => void = () => undefined;
617
+ const move = worker.whileHeld(() => new Promise<void>((resolve) => (finishMove = resolve)));
618
+ await settle();
619
+ let stopped = false;
620
+ const stopping = worker.stop().then(() => (stopped = true));
621
+ await settle();
622
+ expect(stopped).toBe(false);
623
+ finishMove();
624
+ await move;
625
+ await stopping;
626
+ expect(stopped).toBe(true);
627
+ });
628
+
629
+ it('finishes stopping when the hold in flight fails', async () => {
630
+ const { worker } = recorded();
631
+ let failMove: (err: Error) => void = () => undefined;
632
+ const move = worker.whileHeld(() => new Promise<void>((_, reject) => (failMove = reject)));
633
+ move.catch(() => undefined);
634
+ await settle();
635
+ const stopping = worker.stop();
636
+ failMove(new Error('the move failed'));
637
+ await expect(stopping).resolves.toBeUndefined();
638
+ });
639
+
640
+ /**
641
+ * A worker's stop answers a second caller at once while the first is
642
+ * still waiting for the commit in flight. A second hold given that answer
643
+ * would set a working copy aside under that commit.
644
+ */
645
+ it('makes a second hold wait for the stop the first one is still waiting on', async () => {
646
+ const events: string[] = [];
647
+ let stopped = false;
648
+ let finishStop: () => void = () => undefined;
649
+ const worker = holdable({
650
+ start: () => void events.push('start'),
651
+ // As the commit worker's: the first call waits, a later one returns.
652
+ stop: () => {
653
+ if (stopped) return Promise.resolve();
654
+ stopped = true;
655
+ return new Promise<void>((resolve) => (finishStop = resolve));
656
+ },
657
+ });
658
+ worker.start();
659
+ const first = worker.whileHeld(async () => void events.push('first work'));
660
+ const second = worker.whileHeld(async () => void events.push('second work'));
661
+ await settle();
662
+ // The commit in flight has not finished: neither hold has begun.
663
+ expect(events).toEqual(['start']);
664
+ finishStop();
665
+ await Promise.all([first, second]);
666
+ expect(events.slice(0, 3).sort()).toEqual(['first work', 'second work', 'start']);
667
+ });
668
+
669
+ it('waits for the last of two overlapping holds before it starts again', async () => {
670
+ const { events, worker } = recorded();
671
+ worker.start();
672
+ let finishFirst: () => void = () => undefined;
673
+ const first = worker.whileHeld(() => new Promise<void>((resolve) => (finishFirst = resolve)));
674
+ await settle();
675
+ await worker.whileHeld(async () => undefined);
676
+ // The second hold ended and the first is still running: not started yet.
677
+ expect(events.filter((e) => e === 'start')).toHaveLength(1);
678
+ finishFirst();
679
+ await first;
680
+ expect(events.filter((e) => e === 'start')).toHaveLength(2);
445
681
  });
446
682
  });