@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
@@ -0,0 +1,402 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { testKbContext } from '../../../__tests__/kb-context.js';
3
+ import { NodeFs } from '../../kb-fs/node-fs.js';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import { WorkspaceService } from '../workspace.service.js';
10
+ import { NodeGitRunner } from '../../workflow/git/node-git-runner.js';
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ /**
15
+ * A working copy left on disk by a deployment that has since been pointed at
16
+ * a DIFFERENT repository.
17
+ *
18
+ * The workspace service adopts whatever clone it finds on disk, and a clone
19
+ * fetches through the address stored in its own `remote.origin.url` — so an
20
+ * adopted one of the repository that was replaced answers "repository not
21
+ * found" on every fetch from then on, with nothing to re-clone it. It is set
22
+ * aside and cloned fresh instead.
23
+ *
24
+ * SET ASIDE, never deleted — the same rule, and the same destination, as the
25
+ * KB startup phase's sweep. This path has reached no remote of its own: it
26
+ * runs wherever the address is stored but unproven (a gated boot, a
27
+ * break-glass start, a save that changed the address before the phase got to
28
+ * it), so a mistyped host name must not cost anyone commits that exist
29
+ * nowhere else.
30
+ */
31
+
32
+ const BRANCH = 'main';
33
+
34
+ async function git(cwd: string, args: string[]): Promise<string> {
35
+ const { stdout } = await execFileAsync('git', args, {
36
+ cwd,
37
+ env: {
38
+ ...process.env,
39
+ GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 't@x.com',
40
+ GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 't@x.com',
41
+ },
42
+ });
43
+ return stdout.toString();
44
+ }
45
+
46
+ let root: string;
47
+ let workspacesRoot: string;
48
+
49
+ beforeEach(async () => {
50
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'ws-replaced-'));
51
+ workspacesRoot = path.join(root, 'workspaces');
52
+ await fs.mkdir(workspacesRoot, { recursive: true });
53
+ });
54
+
55
+ afterEach(async () => {
56
+ await fs.rm(root, { recursive: true, force: true });
57
+ });
58
+
59
+ /** A bare repository with one commit on `main`, whose file says `marker`. */
60
+ async function upstream(name: string, marker: string): Promise<string> {
61
+ const bare = path.join(root, `${name}.git`);
62
+ await git(root, ['init', '--bare', '-b', BRANCH, bare]);
63
+ const seed = path.join(root, `.seed-${name}`);
64
+ await fs.mkdir(seed);
65
+ await git(seed, ['init', '-b', BRANCH]);
66
+ await fs.writeFile(path.join(seed, 'marker.txt'), marker, 'utf8');
67
+ await git(seed, ['add', '-A']);
68
+ await git(seed, ['commit', '-m', 'init']);
69
+ await git(seed, ['remote', 'add', 'origin', bare]);
70
+ await git(seed, ['push', 'origin', BRANCH]);
71
+ return bare;
72
+ }
73
+
74
+ function service(
75
+ kbRepoUrl: () => string,
76
+ setAsideRoot?: string,
77
+ aroundSetAside?: (workspaceId: string, move: () => Promise<void>) => Promise<void>,
78
+ gitRunner: NodeGitRunner = new NodeGitRunner(),
79
+ ) {
80
+ return new WorkspaceService(
81
+ workspacesRoot,
82
+ kbRepoUrl,
83
+ testKbContext({ branchModel: { defaultBranch: BRANCH, protectedBranches: [BRANCH] } }),
84
+ new NodeFs(),
85
+ gitRunner,
86
+ setAsideRoot,
87
+ aroundSetAside,
88
+ );
89
+ }
90
+
91
+ /**
92
+ * A git runner that puts two callers' reads of a working copy's address in ONE
93
+ * order, the one that matters: both read the copy that is there, and the
94
+ * second is handed its answer only once `afterTheFirstMove` has come.
95
+ *
96
+ * Two holds, because holding the second answer back is only half of it. The
97
+ * FIRST answer is held until the second read has happened, so the first caller
98
+ * cannot move the copy before the second has looked at it — otherwise the
99
+ * second would read whatever replaced it, and the stale answer this is about
100
+ * would never exist. Then the second answer is held until the first move has
101
+ * ended. Neither can wait on the other forever: the first caller is held in its
102
+ * read, so no move is in flight for the second to wait on, and it always reads.
103
+ * Everything else runs as it is.
104
+ */
105
+ function withTheSecondAddressReadHeldPastTheMove(afterTheFirstMove: Promise<void>): NodeGitRunner {
106
+ let reads = 0;
107
+ let secondHasRead!: () => void;
108
+ const onceTheSecondHasRead = new Promise<void>((resolve) => {
109
+ secondHasRead = resolve;
110
+ });
111
+ return new Proxy(new NodeGitRunner(), {
112
+ get(target, prop, receiver) {
113
+ if (prop !== 'run') return Reflect.get(target, prop, receiver);
114
+ return async (cwd: string, args: string[], opts?: never) => {
115
+ const answer = await target.run(cwd, args, opts);
116
+ const readsTheAddress = args[0] === 'config' && args.includes('--get') && args.includes('remote.origin.url');
117
+ if (!readsTheAddress) return answer;
118
+ const mine = ++reads;
119
+ if (mine === 1) await onceTheSecondHasRead;
120
+ if (mine === 2) {
121
+ secondHasRead();
122
+ await afterTheFirstMove;
123
+ }
124
+ return answer;
125
+ };
126
+ },
127
+ });
128
+ }
129
+
130
+ const cloneDir = () => path.join(workspacesRoot, encodeURIComponent(BRANCH), 'knowledge-base');
131
+
132
+ /** Every working copy under a set-aside root, as repo directories. */
133
+ async function setAside(setAsideRoot = path.join(root, 'replaced-working-copies')): Promise<string[]> {
134
+ const stamps = await fs.readdir(setAsideRoot).catch(() => [] as string[]);
135
+ const kept: string[] = [];
136
+ for (const stamp of stamps) {
137
+ for (const id of await fs.readdir(path.join(setAsideRoot, stamp))) {
138
+ kept.push(path.join(setAsideRoot, stamp, id));
139
+ }
140
+ }
141
+ return kept;
142
+ }
143
+
144
+ describe('a working copy of a repository that was replaced', () => {
145
+ it('is set aside and cloned fresh from the configured address', async () => {
146
+ const old = await upstream('old', 'old repository');
147
+ const replacement = await upstream('replacement', 'new repository');
148
+ // A clone of the OLD repository, sitting on disk from before the change.
149
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
150
+ await git(root, ['clone', '-b', BRANCH, old, cloneDir()]);
151
+
152
+ await service(() => replacement).getOrCreateForBranch(BRANCH);
153
+
154
+ expect((await git(cloneDir(), ['config', '--get', 'remote.origin.url'])).trim()).toBe(replacement);
155
+ expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('new repository');
156
+ });
157
+
158
+ it('keeps the work that was only ever committed there, in the set-aside folder', async () => {
159
+ const old = await upstream('old', 'old repository');
160
+ const replacement = await upstream('replacement', 'new repository');
161
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
162
+ await git(root, ['clone', '-b', BRANCH, old, cloneDir()]);
163
+ // Committed here and pushed nowhere. The address may yet turn out to be a
164
+ // typo, so this is the last copy of it in the world.
165
+ await fs.writeFile(path.join(cloneDir(), 'unpushed.md'), 'local work', 'utf8');
166
+ await git(cloneDir(), ['add', '-A']);
167
+ await git(cloneDir(), ['commit', '-m', 'local work']);
168
+ const head = (await git(cloneDir(), ['rev-parse', 'HEAD'])).trim();
169
+
170
+ await service(() => replacement).getOrCreateForBranch(BRANCH);
171
+
172
+ const kept = await setAside();
173
+ expect(kept).toHaveLength(1);
174
+ // Still a repository, still holding the commit, still reachable by hand:
175
+ // `git log` there shows it and `git push <address> <branch>` sends it on.
176
+ expect((await git(kept[0]!, ['rev-parse', 'HEAD'])).trim()).toBe(head);
177
+ expect(await fs.readFile(path.join(kept[0]!, 'unpushed.md'), 'utf8')).toBe('local work');
178
+ // And the fresh clone really is of the new repository.
179
+ expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('new repository');
180
+ });
181
+
182
+ it('leaves a clone of the configured repository exactly where it is', async () => {
183
+ const configured = await upstream('configured', 'configured');
184
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
185
+ await git(root, ['clone', '-b', BRANCH, configured, cloneDir()]);
186
+ // Committed here and never pushed: a needless re-clone would lose it.
187
+ await fs.writeFile(path.join(cloneDir(), 'unpushed.md'), 'local work', 'utf8');
188
+ await git(cloneDir(), ['add', '-A']);
189
+ await git(cloneDir(), ['commit', '-m', 'local work']);
190
+ const head = (await git(cloneDir(), ['rev-parse', 'HEAD'])).trim();
191
+
192
+ await service(() => configured).getOrCreateForBranch(BRANCH);
193
+
194
+ expect((await git(cloneDir(), ['rev-parse', 'HEAD'])).trim()).toBe(head);
195
+ expect(await setAside()).toEqual([]);
196
+ });
197
+
198
+ it('keeps a clone whose address differs only in spelling', async () => {
199
+ const configured = await upstream('configured', 'configured');
200
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
201
+ await git(root, ['clone', '-b', BRANCH, configured, cloneDir()]);
202
+ await fs.writeFile(path.join(cloneDir(), 'unpushed.md'), 'local work', 'utf8');
203
+ await git(cloneDir(), ['add', '-A']);
204
+ await git(cloneDir(), ['commit', '-m', 'local work']);
205
+ const head = (await git(cloneDir(), ['rev-parse', 'HEAD'])).trim();
206
+
207
+ // The same path with a trailing slash — the same repository.
208
+ await service(() => `${configured}/`).getOrCreateForBranch(BRANCH);
209
+
210
+ expect((await git(cloneDir(), ['rev-parse', 'HEAD'])).trim()).toBe(head);
211
+ expect(await setAside()).toEqual([]);
212
+ });
213
+
214
+ it('keeps a clone when the process does not know where the repository is', async () => {
215
+ const configured = await upstream('configured', 'configured');
216
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
217
+ await git(root, ['clone', '-b', BRANCH, configured, cloneDir()]);
218
+
219
+ // Nothing configured yet: "could not tell" is no reason to move work.
220
+ await service(() => '').getOrCreateForBranch(BRANCH);
221
+
222
+ expect((await git(cloneDir(), ['config', '--get', 'remote.origin.url'])).trim()).toBe(configured);
223
+ expect(await setAside()).toEqual([]);
224
+ });
225
+ });
226
+
227
+ /**
228
+ * WHERE it is set aside is not a detail. A deployment runs from an image whose
229
+ * filesystem is replaced on every recreate, and only the mounted volumes
230
+ * survive that — so a working copy kept beside the workspaces root was kept
231
+ * until the next `docker compose up` and no longer, while the screen had
232
+ * promised the admin it was there to recover from. The composition root roots
233
+ * this service and the KB startup phase at ONE directory under the backups
234
+ * volume; this is the half of that the service owes.
235
+ */
236
+ describe('where a working copy is set aside', () => {
237
+ it('goes to the root it was given, not beside the workspaces root', async () => {
238
+ const old = await upstream('old', 'old repository');
239
+ const replacement = await upstream('replacement', 'new repository');
240
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
241
+ await git(root, ['clone', '-b', BRANCH, old, cloneDir()]);
242
+ await fs.writeFile(path.join(cloneDir(), 'unpushed.md'), 'local work', 'utf8');
243
+ await git(cloneDir(), ['add', '-A']);
244
+ await git(cloneDir(), ['commit', '-m', 'local work']);
245
+ // Somewhere the workspaces root cannot be reached from by walking up:
246
+ // the backups volume, as the deployment mounts it.
247
+ const backups = path.join(root, 'backups', 'replaced-working-copies');
248
+
249
+ await service(() => replacement, backups).getOrCreateForBranch(BRANCH);
250
+
251
+ const kept = await setAside(backups);
252
+ expect(kept).toHaveLength(1);
253
+ expect(await fs.readFile(path.join(kept[0]!, 'unpushed.md'), 'utf8')).toBe('local work');
254
+ // And nothing was left in the place that does not outlive the container.
255
+ expect(await setAside()).toEqual([]);
256
+ });
257
+
258
+ it('falls back beside the workspaces root when no root is given', async () => {
259
+ const old = await upstream('old', 'old repository');
260
+ const replacement = await upstream('replacement', 'new repository');
261
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
262
+ await git(root, ['clone', '-b', BRANCH, old, cloneDir()]);
263
+
264
+ await service(() => replacement).getOrCreateForBranch(BRANCH);
265
+
266
+ expect(await setAside()).toHaveLength(1);
267
+ });
268
+ });
269
+
270
+ /**
271
+ * A branch opened onto a clone of another repository sets it aside on the
272
+ * spot, with nobody having stopped anything for it. What the deployment does
273
+ * around the startup phase's set-asides (the commit worker held, the copy's
274
+ * queued commits held back, its locks dropped) it is handed here to do around
275
+ * this one.
276
+ */
277
+ describe('what surrounds the setting aside of one working copy', () => {
278
+ async function onTheOldRepository() {
279
+ const old = await upstream('old', 'old repository');
280
+ const replacement = await upstream('replacement', 'new repository');
281
+ await fs.mkdir(path.dirname(cloneDir()), { recursive: true });
282
+ await git(root, ['clone', '-b', BRANCH, old, cloneDir()]);
283
+ return { old, replacement };
284
+ }
285
+
286
+ it('is given the working copy and the move, and the copy is where it was until the move is run', async () => {
287
+ const { old, replacement } = await onTheOldRepository();
288
+ const seen: string[] = [];
289
+ const svc = service(() => replacement, undefined, async (workspaceId, move) => {
290
+ seen.push(`before ${workspaceId}: ${(await git(cloneDir(), ['config', '--get', 'remote.origin.url'])).trim()}`);
291
+ await move();
292
+ seen.push(`after: ${await fs.access(cloneDir()).then(() => 'still there', () => 'gone')}`);
293
+ });
294
+
295
+ await svc.getOrCreateForBranch(BRANCH);
296
+
297
+ expect(seen).toEqual([`before ${encodeURIComponent(BRANCH)}: ${old}`, 'after: gone']);
298
+ expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('new repository');
299
+ });
300
+
301
+ it('refuses the branch, and leaves the copy where it was, when what must happen first fails', async () => {
302
+ const { old, replacement } = await onTheOldRepository();
303
+ const svc = service(() => replacement, undefined, async () => {
304
+ throw new Error('the queue could not be held back');
305
+ });
306
+
307
+ await expect(svc.getOrCreateForBranch(BRANCH)).rejects.toThrow('the queue could not be held back');
308
+
309
+ expect((await git(cloneDir(), ['config', '--get', 'remote.origin.url'])).trim()).toBe(old);
310
+ expect(await setAside()).toEqual([]);
311
+ });
312
+
313
+ /**
314
+ * What surrounds the move can wait: for the commit in flight, or for the
315
+ * startup phase, which may set this very copy aside in the meantime.
316
+ */
317
+ it('moves nothing, and still opens the branch, when the startup phase took the copy while it waited', async () => {
318
+ const { replacement } = await onTheOldRepository();
319
+ const phaseKept = path.join(root, 'kept-by-the-phase');
320
+ const svc: WorkspaceService = service(() => replacement, undefined, async (workspaceId, move) => {
321
+ // The phase, while this call waited: the copy moved out, and said so.
322
+ await fs.rename(cloneDir(), phaseKept);
323
+ svc.forgetClone(workspaceId);
324
+ await move();
325
+ });
326
+
327
+ await svc.getOrCreateForBranch(BRANCH);
328
+
329
+ expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('new repository');
330
+ // Nothing of this call's own was set aside: the phase's copy is the only one.
331
+ expect(await setAside()).toEqual([]);
332
+ expect(await fs.readFile(path.join(phaseKept, 'marker.txt'), 'utf8')).toBe('old repository');
333
+ });
334
+
335
+ it('moves it once for two callers opening the branch together, and serves both', async () => {
336
+ const { replacement } = await onTheOldRepository();
337
+ let moves = 0;
338
+ const svc = service(() => replacement, undefined, async (_workspaceId, move) => {
339
+ moves += 1;
340
+ await move();
341
+ });
342
+
343
+ const [first, second] = await Promise.all([svc.getOrCreateForBranch(BRANCH), svc.getOrCreateForBranch(BRANCH)]);
344
+
345
+ expect(moves).toBe(1);
346
+ expect(first.repoDir).toBe(second.repoDir);
347
+ expect(await setAside()).toHaveLength(1);
348
+ expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('new repository');
349
+ });
350
+
351
+ /**
352
+ * The same two callers, in the order the test above only sometimes got. The
353
+ * second reads the old copy's address, and is handed the answer after the
354
+ * first has finished moving that copy — when nothing says a move is in
355
+ * flight any more, and what sits at the path is the fresh clone the first is
356
+ * making. It moved that too: a second set-aside, of a working copy of the
357
+ * RIGHT repository, taken from under the clone that was writing it.
358
+ */
359
+ it('moves it once when the second caller comes back with the old address after the move has ended', async () => {
360
+ const { replacement } = await onTheOldRepository();
361
+ let moves = 0;
362
+ let firstMoveEnded!: () => void;
363
+ const afterTheFirstMove = new Promise<void>((resolve) => {
364
+ firstMoveEnded = resolve;
365
+ });
366
+ const svc = service(
367
+ () => replacement,
368
+ undefined,
369
+ async (_workspaceId, move) => {
370
+ moves += 1;
371
+ await move();
372
+ firstMoveEnded();
373
+ },
374
+ withTheSecondAddressReadHeldPastTheMove(afterTheFirstMove),
375
+ );
376
+
377
+ const [first, second] = await Promise.all([svc.getOrCreateForBranch(BRANCH), svc.getOrCreateForBranch(BRANCH)]);
378
+
379
+ expect(moves).toBe(1);
380
+ expect(first.repoDir).toBe(second.repoDir);
381
+ expect(await setAside()).toHaveLength(1);
382
+ expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('new repository');
383
+ // And the clone both were served is whole: its address is the new one.
384
+ expect((await git(cloneDir(), ['config', '--get', 'remote.origin.url'])).trim()).toBe(replacement);
385
+ });
386
+ });
387
+
388
+ describe('forgetClone', () => {
389
+ it('drops the cached directory, so the next open clones again', async () => {
390
+ const configured = await upstream('configured', 'configured');
391
+ const svc = service(() => configured);
392
+ await svc.getOrCreateForBranch(BRANCH);
393
+ // What the KB startup phase does to a clone of another repository: moves
394
+ // it out of the workspaces root and says so.
395
+ await fs.rm(cloneDir(), { recursive: true, force: true });
396
+ svc.forgetClone(encodeURIComponent(BRANCH));
397
+
398
+ await svc.getOrCreateForBranch(BRANCH);
399
+
400
+ expect(await fs.readFile(path.join(cloneDir(), 'marker.txt'), 'utf8')).toBe('configured');
401
+ });
402
+ });
@@ -1122,7 +1122,7 @@ describe('WorkspaceService.readAllKbFiles', () => {
1122
1122
  });
1123
1123
  });
1124
1124
 
1125
- describe('WorkspaceService.unzipFile — ontology-session write guard', () => {
1125
+ describe('WorkspaceService.unzipFile — per-entry write guard', () => {
1126
1126
  let root: string;
1127
1127
  let svc: WorkspaceService;
1128
1128
  let workspaceDir: string;
@@ -1154,12 +1154,12 @@ describe('WorkspaceService.unzipFile — ontology-session write guard', () => {
1154
1154
  it('skips entries the write guard rejects and never writes them to disk', async () => {
1155
1155
  await writeZip('a.zip', { 'keep.md': 'ok', 'blocked.md': 'no' });
1156
1156
  const res = await svc.unzipFile(workspaceId, 'a.zip', 'out', async (wsPath) => {
1157
- if (wsPath.endsWith('blocked.md')) throw new Error('Blocked by the ontology-session boundary');
1157
+ if (wsPath.endsWith('blocked.md')) throw new Error('Not in this conversation.');
1158
1158
  });
1159
1159
  // An unprefixed destination is the repository's own `out/`, and the
1160
1160
  // extracted paths are reported workspace-relative, so prefixed.
1161
1161
  expect(res.extracted).toEqual(['knowledge-base/out/keep.md']);
1162
- expect(res.skipped).toContainEqual({ path: 'blocked.md', reason: 'Blocked by the ontology-session boundary' });
1162
+ expect(res.skipped).toContainEqual({ path: 'blocked.md', reason: 'Not in this conversation.' });
1163
1163
  expect((await fs.readFile(path.join(repoDir, 'out', 'keep.md'))).toString()).toBe('ok');
1164
1164
  await expect(fs.readFile(path.join(repoDir, 'out', 'blocked.md'))).rejects.toThrow();
1165
1165
  await expect(fs.stat(path.join(workspaceDir, 'out'))).rejects.toMatchObject({ code: 'ENOENT' });
@@ -1173,18 +1173,75 @@ describe('WorkspaceService.unzipFile — ontology-session write guard', () => {
1173
1173
  await fs.writeFile(path.join(repoDir, 'c.zip'), zip.toBuffer());
1174
1174
 
1175
1175
  const res = await svc.unzipFile(workspaceId, 'c.zip', 'out', async (wsPath) => {
1176
- if (wsPath.includes('blocked-dir')) throw new Error('Blocked by the ontology-session boundary');
1176
+ if (wsPath.includes('blocked-dir')) throw new Error('Not in this conversation.');
1177
1177
  });
1178
1178
 
1179
1179
  expect(res.extracted).toEqual(['knowledge-base/out/keep.md']);
1180
- expect(res.skipped).toContainEqual({ path: 'blocked-dir/', reason: 'Blocked by the ontology-session boundary' });
1180
+ expect(res.skipped).toContainEqual({ path: 'blocked-dir/', reason: 'Not in this conversation.' });
1181
1181
  await expect(fs.stat(path.join(repoDir, 'out', 'blocked-dir'))).rejects.toThrow();
1182
1182
  });
1183
1183
 
1184
+ /**
1185
+ * An archive reader caps the inflation at the size the header DECLARES, but
1186
+ * only when that size is above zero: an entry declaring zero is inflated with
1187
+ * no cap. So "check the declared size, then read" let a few kilobytes expand
1188
+ * to whatever memory there was. Both headers are patched to say "empty" here,
1189
+ * over four megabytes that deflate to four kilobytes.
1190
+ */
1191
+ it('refuses an entry whose header says it is empty and whose stream is not, and extracts the rest', async () => {
1192
+ const { default: AdmZip } = await import('adm-zip');
1193
+ const zip = new AdmZip();
1194
+ zip.addFile('bomb.md', Buffer.alloc(4 * 1024 * 1024, 0x61));
1195
+ const bomb = zip.toBuffer();
1196
+ // The uncompressed-size field: 22 bytes into the local header, 24 into the
1197
+ // central-directory one.
1198
+ bomb.writeUInt32LE(0, bomb.indexOf(Buffer.from([0x50, 0x4b, 0x03, 0x04])) + 22);
1199
+ bomb.writeUInt32LE(0, bomb.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02])) + 24);
1200
+ await fs.writeFile(path.join(repoDir, 'bomb.zip'), bomb);
1201
+
1202
+ const res = await svc.unzipFile(workspaceId, 'bomb.zip', 'out');
1203
+
1204
+ expect(res.extracted).toEqual([]);
1205
+ expect(res.skipped).toHaveLength(1);
1206
+ expect(res.skipped[0]).toMatchObject({ path: 'bomb.md' });
1207
+ expect(res.skipped[0].reason).toContain('declares an empty file');
1208
+ await expect(fs.stat(path.join(repoDir, 'out', 'bomb.md'))).rejects.toThrow();
1209
+ });
1210
+
1211
+ it('extracts the other entries of an archive in which one fails its checksum', async () => {
1212
+ const { default: AdmZip } = await import('adm-zip');
1213
+ const zip = new AdmZip();
1214
+ zip.addFile('bad.md', Buffer.from('this entry will not match its checksum'));
1215
+ zip.addFile('fine.md', Buffer.from('fine'));
1216
+ const archive = zip.toBuffer();
1217
+ // The CRC field of the FIRST entry, in both of its headers: 14 bytes into
1218
+ // the local one, 16 into the central-directory one. A reader checks one or
1219
+ // the other depending on the entry's flags.
1220
+ const firstLocal = archive.indexOf(Buffer.from([0x50, 0x4b, 0x03, 0x04]));
1221
+ const firstCentral = archive.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02]));
1222
+ archive.writeUInt32LE((archive.readUInt32LE(firstLocal + 14) ^ 0xffffffff) >>> 0, firstLocal + 14);
1223
+ archive.writeUInt32LE((archive.readUInt32LE(firstCentral + 16) ^ 0xffffffff) >>> 0, firstCentral + 16);
1224
+ await fs.writeFile(path.join(repoDir, 'crc.zip'), archive);
1225
+
1226
+ const res = await svc.unzipFile(workspaceId, 'crc.zip', 'out');
1227
+
1228
+ expect(res.extracted).toEqual(['knowledge-base/out/fine.md']);
1229
+ expect(res.skipped).toHaveLength(1);
1230
+ expect(res.skipped[0].path).toBe('bad.md');
1231
+ expect(res.skipped[0].reason).toContain('could not be read');
1232
+ });
1233
+
1234
+ it('still extracts a file that really is empty', async () => {
1235
+ await writeZip('empty.zip', { 'empty.md': '', 'full.md': 'x' });
1236
+ const res = await svc.unzipFile(workspaceId, 'empty.zip', 'out');
1237
+ expect(res.extracted.sort()).toEqual(['knowledge-base/out/empty.md', 'knowledge-base/out/full.md']);
1238
+ expect((await fs.readFile(path.join(repoDir, 'out', 'empty.md'))).byteLength).toBe(0);
1239
+ });
1240
+
1184
1241
  it('does not create the destination directory when every entry is blocked', async () => {
1185
1242
  await writeZip('d.zip', { 'a.md': '1', 'b.md': '2' });
1186
1243
  const res = await svc.unzipFile(workspaceId, 'd.zip', 'out', async () => {
1187
- throw new Error('Blocked by the ontology-session boundary');
1244
+ throw new Error('Not in this conversation.');
1188
1245
  });
1189
1246
  expect(res.extracted).toEqual([]);
1190
1247
  expect(res.skipped).toHaveLength(2);
@@ -1213,11 +1270,22 @@ describe('WorkspaceService.unzipFile — ontology-session write guard', () => {
1213
1270
  'There is no file or directory at "knowledge-base/nope.zip" in this workspace. Check the path with list_files.',
1214
1271
  });
1215
1272
  // A name carrying a line break cannot forge a second line of the answer,
1216
- // in the message or in the `path` a JSON consumer reads.
1217
- const forged = 'a\nb\u2028c.zip';
1273
+ // in the message or in the `path` a JSON consumer reads. A LINE BREAK no
1274
+ // longer reaches this refusal at all — the normaliser now refuses a
1275
+ // control character outright, because git reads one in a path as the end
1276
+ // of an object name (see `repo-path.ts`) — so the forged name here
1277
+ // carries U+2028, which is a line break to a renderer and nothing to git:
1278
+ // still accepted as a file name, still escaped on the way out.
1279
+ const forged = 'a\u2028c.zip';
1218
1280
  await expect(svc.unzipFile(workspaceId, forged)).rejects.toMatchObject({
1219
1281
  status: 404,
1220
- payload: { kind: 'not_found', path: 'knowledge-base/a\\nb\\u2028c.zip' },
1282
+ payload: { kind: 'not_found', path: 'knowledge-base/a\\u2028c.zip' },
1283
+ });
1284
+ // And the same invariant at the gate that now owns the line break: the
1285
+ // refusal names the path with the break escaped, never raw.
1286
+ await expect(svc.unzipFile(workspaceId, 'a\nb.zip')).rejects.toMatchObject({
1287
+ status: 400,
1288
+ payload: { kind: 'path-control-character', path: 'a\\nb.zip' },
1221
1289
  });
1222
1290
  });
1223
1291