@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,170 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { testKbContext } from '../../../../__tests__/kb-context.js';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import type { WorkspaceService } from '../../../workspace/workspace.service.js';
9
+ import { WorkflowHooks } from '../../workflow-hooks.js';
10
+ import { GitService } from '../git.service.js';
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ /**
15
+ * What the log says when a change request's branches cannot be fetched.
16
+ *
17
+ * The fetch is best-effort: when it fails, the refs are resolved from what the
18
+ * working copy already has. A working copy that could not reach the remote at
19
+ * all then reported `unknown branch` for every branch it had never seen, and
20
+ * nothing said the fetch had been refused — the log named a missing branch
21
+ * that was on the remote the whole time.
22
+ */
23
+
24
+ const TRUNK = 'current-company-state';
25
+ const WORKSPACE = 'ws-pr-fetch';
26
+
27
+ async function runGit(cwd: string, args: string[]): Promise<void> {
28
+ await execFileAsync('git', args, {
29
+ cwd,
30
+ env: {
31
+ ...process.env,
32
+ GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 't@x.com',
33
+ GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 't@x.com',
34
+ },
35
+ });
36
+ }
37
+
38
+ let root: string;
39
+ let repo: string;
40
+ let upstream: string;
41
+
42
+ beforeEach(async () => {
43
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'git-pr-fetch-'));
44
+ upstream = path.join(root, 'upstream.git');
45
+ await runGit(root, ['init', '--bare', '-b', TRUNK, upstream]);
46
+ const seed = path.join(root, '.seed');
47
+ await fs.mkdir(seed);
48
+ await runGit(seed, ['init', '-b', TRUNK]);
49
+ await runGit(seed, ['remote', 'add', 'origin', upstream]);
50
+ await fs.writeFile(path.join(seed, 'base.md'), 'base\n');
51
+ await runGit(seed, ['add', '-A']);
52
+ await runGit(seed, ['commit', '-m', 'init']);
53
+ await runGit(seed, ['push', 'origin', TRUNK]);
54
+ // A draft the working copy below has never seen: pushed after it was cloned.
55
+ repo = path.join(root, WORKSPACE, 'knowledge-base');
56
+ await fs.mkdir(path.dirname(repo), { recursive: true });
57
+ await runGit(root, ['clone', upstream, repo]);
58
+ await runGit(seed, ['checkout', '-b', 'alice/feature']);
59
+ await fs.writeFile(path.join(seed, 'new.md'), 'new\n');
60
+ await runGit(seed, ['add', '-A']);
61
+ await runGit(seed, ['commit', '-m', 'feature']);
62
+ await runGit(seed, ['push', 'origin', 'alice/feature']);
63
+ });
64
+
65
+ afterEach(async () => {
66
+ vi.restoreAllMocks();
67
+ await fs.rm(root, { recursive: true, force: true });
68
+ });
69
+
70
+ function service(): GitService {
71
+ const workspaces = {
72
+ getWorkspacePath: async () => path.dirname(repo),
73
+ } as unknown as WorkspaceService;
74
+ return new GitService(workspaces, new WorkflowHooks(), testKbContext());
75
+ }
76
+
77
+ /** Everything the service logged at warn level, as one string per line. */
78
+ function warnings(spy: ReturnType<typeof vi.spyOn>): string[] {
79
+ return spy.mock.calls.map((call) => call.map((part) => (typeof part === 'string' ? part : JSON.stringify(part))).join(' '));
80
+ }
81
+
82
+ describe('a change request whose branches cannot be fetched', () => {
83
+ it('says the fetch failed, and where, when the working copy cannot reach the remote', async () => {
84
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
85
+ // The remote this working copy fetches from is gone from under it.
86
+ await runGit(repo, ['remote', 'set-url', 'origin', path.join(root, 'no-such-remote.git')]);
87
+
88
+ await expect(service().changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature')).rejects.toThrow(
89
+ 'unknown branch: alice/feature',
90
+ );
91
+
92
+ const said = warnings(warn).filter((line) => line.includes('could not fetch change-request branches'));
93
+ expect(said).toHaveLength(1);
94
+ expect(said[0]).toContain(`"${WORKSPACE}"`);
95
+ expect(said[0]).toContain('though it may be on the remote');
96
+ });
97
+
98
+ it('says it once for a list that asks about many requests in one working copy', async () => {
99
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
100
+ await runGit(repo, ['remote', 'set-url', 'origin', path.join(root, 'no-such-remote.git')]);
101
+ const git = service();
102
+
103
+ for (const branch of ['alice/feature', 'bob/other', 'carol/third']) {
104
+ await git.changedPathsForPr(WORKSPACE, TRUNK, branch).catch(() => undefined);
105
+ }
106
+
107
+ expect(warnings(warn).filter((line) => line.includes('could not fetch change-request branches'))).toHaveLength(1);
108
+ });
109
+
110
+ it('says it again once a minute has passed, and not before', async () => {
111
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
112
+ await runGit(repo, ['remote', 'set-url', 'origin', path.join(root, 'no-such-remote.git')]);
113
+ const git = service();
114
+ const said = () => warnings(warn).filter((line) => line.includes('could not fetch change-request branches'));
115
+ // Only the clock the service reads is moved: git itself runs for real.
116
+ const realNow = Date.now.bind(Date);
117
+ let ahead = 0;
118
+ vi.spyOn(Date, 'now').mockImplementation(() => realNow() + ahead);
119
+
120
+ await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature').catch(() => undefined);
121
+ expect(said()).toHaveLength(1);
122
+
123
+ ahead = 30_000;
124
+ await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature').catch(() => undefined);
125
+ expect(said()).toHaveLength(1);
126
+
127
+ ahead = 61_000;
128
+ await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature').catch(() => undefined);
129
+ expect(said()).toHaveLength(2);
130
+ });
131
+
132
+ it('says it again at once for a working copy that reached the remote in between', async () => {
133
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
134
+ const git = service();
135
+ const said = () => warnings(warn).filter((line) => line.includes('could not fetch change-request branches'));
136
+ const broken = path.join(root, 'no-such-remote.git');
137
+
138
+ await runGit(repo, ['remote', 'set-url', 'origin', broken]);
139
+ await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature').catch(() => undefined);
140
+ expect(said()).toHaveLength(1);
141
+
142
+ // It reaches the remote again: what was said about it is over, and nothing
143
+ // of it is kept.
144
+ await runGit(repo, ['remote', 'set-url', 'origin', upstream]);
145
+ expect(await git.changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature')).toEqual(['new.md']);
146
+
147
+ // So a failure right after is a new one, said without waiting out a minute.
148
+ await runGit(repo, ['remote', 'set-url', 'origin', broken]);
149
+ await git.changedPathsForPr(WORKSPACE, TRUNK, 'bob/other').catch(() => undefined);
150
+ expect(said()).toHaveLength(2);
151
+ });
152
+
153
+ it('says nothing when the branch is simply gone from the remote: that is what a retired branch looks like', async () => {
154
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
155
+
156
+ await expect(service().changedPathsForPr(WORKSPACE, TRUNK, 'dave/retired')).rejects.toThrow(
157
+ 'unknown branch: dave/retired',
158
+ );
159
+
160
+ expect(warnings(warn).filter((line) => line.includes('could not fetch change-request branches'))).toEqual([]);
161
+ });
162
+
163
+ it('says nothing when the fetch works, and finds the branch the working copy had never seen', async () => {
164
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
165
+
166
+ expect(await service().changedPathsForPr(WORKSPACE, TRUNK, 'alice/feature')).toEqual(['new.md']);
167
+
168
+ expect(warnings(warn).filter((line) => line.includes('could not fetch change-request branches'))).toEqual([]);
169
+ });
170
+ });
@@ -35,10 +35,12 @@ import {
35
35
  ProtectedBranchError,
36
36
  PullRebaseConflictError,
37
37
  RemoteBranchGoneError,
38
+ VersionNotOnBranchError,
38
39
  isMissingRemoteBranchFailure,
39
40
  } from '../../../shared/domain-errors.js';
40
41
  import {
41
42
  GitRunError,
43
+ assertOneSpecPerLine,
42
44
  isGitTimeout,
43
45
  redactGitToken,
44
46
  type GitCredentials,
@@ -2331,6 +2333,10 @@ export class GitService implements IGitService {
2331
2333
  const repoRelativePath = this.stripRepoPrefix(relativePath);
2332
2334
  return this.mutex.run(workspaceId, async () => {
2333
2335
  const cwd = await this.repoDir(workspaceId);
2336
+ // Branch-scoped, like the bytes route: a patch is a past version too,
2337
+ // and leaving this route open would let a sha refused there be read
2338
+ // here instead.
2339
+ await this.assertOnBranchHistory(cwd, sha);
2334
2340
  // BOTH sides: at a commit that DELETES a directory the path is absent
2335
2341
  // at `sha` but a tree at `sha^` — and the diff of that deletion is
2336
2342
  // every child's content, exactly what the per-file gate never checked.
@@ -2382,6 +2388,10 @@ export class GitService implements IGitService {
2382
2388
  const repoRelativePath = this.stripRepoPrefix(relativePath);
2383
2389
  return this.mutex.run(workspaceId, async () => {
2384
2390
  const cwd = await this.repoDir(workspaceId);
2391
+ // Same branch rule as the patch and the bytes: this route serves the
2392
+ // FULL text of both sides, so it is the most generous of the three and
2393
+ // the least safe one to leave ungated.
2394
+ await this.assertOnBranchHistory(cwd, sha);
2385
2395
  await this.assertNotTreeAtRef(cwd, sha, repoRelativePath, relativePath);
2386
2396
  await this.assertNotTreeAtRef(cwd, `${sha}^`, repoRelativePath, relativePath);
2387
2397
  const current = await this.readFileAtRef(workspaceId, sha, repoRelativePath);
@@ -2949,7 +2959,53 @@ export class GitService implements IGitService {
2949
2959
  'fetch', '--no-write-fetch-head', 'origin',
2950
2960
  `+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`,
2951
2961
  `+refs/heads/${headBranch}:refs/remotes/origin/${headBranch}`,
2952
- ]).catch(() => undefined);
2962
+ ]).then(
2963
+ () => void this.prFetchFailureLoggedAt.delete(cwd),
2964
+ (err: unknown) => this.notePrFetchFailure(cwd, err),
2965
+ );
2966
+ }
2967
+
2968
+ /**
2969
+ * When each working copy's change-request fetch failure was last logged.
2970
+ * Holds only the working copies that are failing NOW: an entry goes the
2971
+ * moment that copy reaches the remote again, so the table never outgrows the
2972
+ * clones that cannot fetch, however many come and go over a process's life.
2973
+ */
2974
+ private readonly prFetchFailureLoggedAt = new Map<string, number>();
2975
+
2976
+ /**
2977
+ * Say WHY a change request's branches could not be fetched, when the reason
2978
+ * is not the one that is expected.
2979
+ *
2980
+ * The fetch stays best-effort, so the caller goes on to resolve the refs
2981
+ * from whatever the clone already has. One failure is ordinary and says
2982
+ * nothing: a branch that is gone from the remote, which is what a retired
2983
+ * source branch looks like. Every OTHER failure is the working copy being
2984
+ * unable to reach the remote at all — a credential it does not have, a host
2985
+ * that does not answer — and swallowed, it resurfaced one step later as
2986
+ * `unknown branch` for a branch that was sitting on the remote the whole
2987
+ * time. A deployment logged that for every open change request on every
2988
+ * list, and nothing in the log said the fetch had been refused.
2989
+ *
2990
+ * Logged once a minute per working copy at most: the list asks once per open
2991
+ * request, and the reason is the same for all of them.
2992
+ */
2993
+ private notePrFetchFailure(cwd: string, err: unknown): void {
2994
+ const message = err instanceof Error ? err.message : String(err);
2995
+ if (isMissingRemoteBranchFailure(message)) {
2996
+ // The remote answered, and said the branch is not there: this working
2997
+ // copy reaches it, so whatever was logged about it before is over.
2998
+ this.prFetchFailureLoggedAt.delete(cwd);
2999
+ return;
3000
+ }
3001
+ const now = Date.now();
3002
+ if (now - (this.prFetchFailureLoggedAt.get(cwd) ?? 0) < 60_000) return;
3003
+ this.prFetchFailureLoggedAt.set(cwd, now);
3004
+ log.warn(
3005
+ `could not fetch change-request branches in the working copy "${path.basename(path.dirname(cwd))}": ` +
3006
+ 'a branch it has not seen will be reported as unknown, though it may be on the remote.',
3007
+ { detail: redactGitToken(message, this.credentials.token()) },
3008
+ );
2953
3009
  }
2954
3010
 
2955
3011
  /** Unified-diff patch for one file across the CR range (rename-aware). */
@@ -3269,6 +3325,143 @@ export class GitService implements IGitService {
3269
3325
  }
3270
3326
  }
3271
3327
 
3328
+ /**
3329
+ * A repo-relative file's BYTES at an arbitrary git ref — the binary sibling
3330
+ * of {@link readFileAtRef}, with the blob's object id alongside them.
3331
+ *
3332
+ * `readFileAtRef` decodes stdout as UTF-8, which for a PNG or a PDF is not a
3333
+ * lossless round trip: every byte sequence that is not valid UTF-8 comes
3334
+ * back as U+FFFD and the file is ruined. A past version has to be delivered
3335
+ * byte for byte, so this reads through `cat-file --batch`, whose framing is
3336
+ * in BYTES (`<oid> <type> <size>\n<size bytes>\n`) rather than characters.
3337
+ *
3338
+ * One invocation, and absence is a PROTOCOL answer (`<spec> missing`) rather
3339
+ * than a parsed error message — which also covers `<sha>^` of a root commit,
3340
+ * where there is no "before" side at all. `null` therefore means exactly
3341
+ * "nothing is at that path on that side".
3342
+ *
3343
+ * The `oid` is the content's own hash, so it is what the HTTP route uses as
3344
+ * an ETag: cheaper than hashing the body again, and stable for a version
3345
+ * that by definition can never change.
3346
+ */
3347
+ private async blobAtRef(
3348
+ cwd: string,
3349
+ ref: string,
3350
+ repoRelativePath: string,
3351
+ ): Promise<{ bytes: Buffer; blobId: string } | null> {
3352
+ // The spec has to be ONE line: the path is refused a control character
3353
+ // twice over before it gets here (at the route's normaliser and again at
3354
+ // `assertValidRelativePath`), and this is the fence at the protocol —
3355
+ // `--batch` would otherwise read `<sha>:Docs/x.md\nzzz` as two object
3356
+ // names and answer about the first, under the gates of the second.
3357
+ const spec = `${ref}:${repoRelativePath}`;
3358
+ assertOneSpecPerLine([spec]);
3359
+ const { stdout } = await this.gitBytes(cwd, ['cat-file', '--batch'], {
3360
+ input: `${spec}\n`,
3361
+ });
3362
+ const nl = stdout.indexOf(0x0a);
3363
+ if (nl < 0) throw new Error('unexpected end of git cat-file output');
3364
+ // The spec may itself contain spaces (a KB file routinely does), hence the
3365
+ // endsWith checks rather than a split — same protocol reading as the
3366
+ // access resolver's `catFileBatch`.
3367
+ const header = stdout.subarray(0, nl).toString('utf-8');
3368
+ if (header.endsWith(' missing') || header.endsWith(' ambiguous')) return null;
3369
+ const [blobId, type, rawSize] = header.split(' ');
3370
+ // A tree here would be the per-file gate bypassed, and `assertNotTreeAtRef`
3371
+ // is what refuses it before this runs; anything else non-blob (a tag, a
3372
+ // commit) is not a file either. Nothing to serve in either case.
3373
+ if (type !== 'blob' || blobId === undefined) return null;
3374
+ const size = Number(rawSize);
3375
+ if (!Number.isSafeInteger(size) || size < 0) {
3376
+ throw new Error(`unexpected git cat-file header: ${header}`);
3377
+ }
3378
+ return { bytes: stdout.subarray(nl + 1, nl + 1 + size), blobId };
3379
+ }
3380
+
3381
+ /**
3382
+ * Refuse a sha that is not in the history of the branch this workspace has
3383
+ * checked out.
3384
+ *
3385
+ * A file's past is served per BRANCH: the history panel lists what `git log`
3386
+ * on this workspace's own branch touched, and every read of a past save has
3387
+ * to stay inside that list. Without this, a sha lifted from another branch —
3388
+ * or from a change request nobody approved — would be readable through the
3389
+ * workspace of a branch whose history never contained it, which is the
3390
+ * access rules of the viewed branch bypassed by a query parameter.
3391
+ *
3392
+ * `merge-base --is-ancestor` is the exact question, and a commit is its own
3393
+ * ancestor, so the branch head passes. Exit 1 is git's answer for "resolved,
3394
+ * and not an ancestor". An unresolvable object name — a made-up id — gets
3395
+ * the SAME refusal on purpose (see {@link VersionNotOnBranchError}), but only
3396
+ * when git's complaint NAMES that id; every other failure propagates, a
3397
+ * broken repository included. A guard that fails open on its own errors is
3398
+ * not a guard, and one that reports them as a refusal is worse than no
3399
+ * message at all.
3400
+ */
3401
+ private async assertOnBranchHistory(cwd: string, sha: string): Promise<void> {
3402
+ try {
3403
+ await this.git(cwd, ['merge-base', '--is-ancestor', sha, 'HEAD']);
3404
+ } catch (err) {
3405
+ if (err instanceof GitRunError && !err.timedOut) {
3406
+ if (err.exitCode === 1) throw new VersionNotOnBranchError();
3407
+ const stderr = err.stderr ?? err.message;
3408
+ if (
3409
+ // The complaint has to be about THE SHA WE ASKED FOR. `HEAD` is the
3410
+ // other argument, and git spells its own failures the same way: a
3411
+ // repository with no commits answers "ambiguous argument 'HEAD'",
3412
+ // which is a broken workspace, not a version off the branch.
3413
+ // Refusing it as "not in this file's history" would hide an
3414
+ // infrastructure failure behind a sentence about access.
3415
+ stderr.toLowerCase().includes(sha.toLowerCase()) &&
3416
+ // git's wording for "I cannot resolve that" varies by subcommand and
3417
+ // by version — `merge-base` says "Not a valid commit name", others
3418
+ // "not a valid object name" / "unknown revision" — so every spelling
3419
+ // that means the same thing is listed rather than one guessed at.
3420
+ /not a valid (object|commit) name|no such commit|bad object|unknown revision|malformed object name|ambiguous argument/i.test(
3421
+ stderr,
3422
+ )
3423
+ ) {
3424
+ throw new VersionNotOnBranchError();
3425
+ }
3426
+ }
3427
+ throw err;
3428
+ }
3429
+ }
3430
+
3431
+ /**
3432
+ * One file's BYTES at a save, exactly as they were — the `after` side of
3433
+ * `sha`, or the `before` side (`<sha>^`) for the save that deleted it.
3434
+ *
3435
+ * Every guard the text history surfaces apply, in the same order: the sha is
3436
+ * a sha, the save is on this branch, and the pathspec is not a directory at
3437
+ * that ref (a per-file gate upstream authorized one path, and a tree read
3438
+ * would hand over every child). `null` means the file was not there on the
3439
+ * side asked for.
3440
+ *
3441
+ * Runs inside the workspace's git turn — one turn for the whole read, so the
3442
+ * ancestry check and the bytes it authorizes cannot be separated by another
3443
+ * caller's checkout.
3444
+ */
3445
+ async fileBytesAtCommit(
3446
+ workspaceId: string,
3447
+ relativePath: string,
3448
+ sha: string,
3449
+ side: 'after' | 'before',
3450
+ ): Promise<{ bytes: Buffer; blobId: string } | null> {
3451
+ assertValidRelativePath(relativePath);
3452
+ if (!/^[a-f0-9]{7,40}$/i.test(sha)) {
3453
+ throw new WorkflowValidationError('invalid commit sha');
3454
+ }
3455
+ const repoRelativePath = this.stripRepoPrefix(relativePath);
3456
+ return this.mutex.run(workspaceId, async () => {
3457
+ const cwd = await this.repoDir(workspaceId);
3458
+ await this.assertOnBranchHistory(cwd, sha);
3459
+ const ref = side === 'before' ? `${sha}^` : sha;
3460
+ await this.assertNotTreeAtRef(cwd, ref, repoRelativePath, relativePath);
3461
+ return this.blobAtRef(cwd, ref, repoRelativePath);
3462
+ });
3463
+ }
3464
+
3272
3465
  /**
3273
3466
  * The commits on `ref` that touched `repoRelativePath`, newest first, at
3274
3467
  * most `limit` of them. What a skill's `version` is looked up through: each
@@ -3377,6 +3570,27 @@ export class GitService implements IGitService {
3377
3570
  env: { GIT_LITERAL_PATHSPECS: '1', ...opts?.env },
3378
3571
  });
3379
3572
  }
3573
+
3574
+ /**
3575
+ * {@link git}, with stdout as raw BYTES.
3576
+ *
3577
+ * A sibling rather than an `encoding` option on `git()` itself: every other
3578
+ * caller in this service reads text, and the two return types (`string` vs
3579
+ * `Buffer`) are not interchangeable at any call site. Same
3580
+ * `GIT_LITERAL_PATHSPECS` and same runner deadline — the only difference is
3581
+ * that the bytes are not decoded.
3582
+ */
3583
+ private gitBytes(
3584
+ cwd: string,
3585
+ args: string[],
3586
+ opts?: Omit<GitRunOptions, 'encoding'>,
3587
+ ): Promise<GitRunResult<Buffer>> {
3588
+ return this.gitRunner.run(cwd, args, {
3589
+ ...opts,
3590
+ encoding: 'buffer',
3591
+ env: { GIT_LITERAL_PATHSPECS: '1', ...opts?.env },
3592
+ });
3593
+ }
3380
3594
  }
3381
3595
 
3382
3596
  /** Map a git status letter (`git diff --name-status`) to the DTO's `PrFileStatus`. */
@@ -34,7 +34,7 @@
34
34
  * (status='needs_attention')
35
35
  */
36
36
 
37
- import { and, eq, inArray, isNull, min, or, sql } from 'drizzle-orm';
37
+ import { and, eq, inArray, isNull, min, or, sql, type SQL } from 'drizzle-orm';
38
38
  import { logger } from '../../shared/logging.js';
39
39
 
40
40
  const log = logger('pending-commits');
@@ -195,6 +195,7 @@ export class PendingCommitsService {
195
195
  .set({
196
196
  queuedAt: new Date(),
197
197
  authorEmail: email,
198
+ authorEmailBidx: email,
198
199
  authorName: input.authorName,
199
200
  // Reset transient counters — this is effectively a fresh enqueue.
200
201
  attempts: 0,
@@ -216,6 +217,7 @@ export class PendingCommitsService {
216
217
  branch: input.branch,
217
218
  path: input.path,
218
219
  authorEmail: email,
220
+ authorEmailBidx: email,
219
221
  authorName: input.authorName,
220
222
  });
221
223
  }
@@ -249,11 +251,13 @@ export class PendingCommitsService {
249
251
  ),
250
252
  );
251
253
  if ((rows[0]?.count ?? 0) > 0) return false;
254
+ const email = canonicalEmail(input.authorEmail);
252
255
  await this.db.insert(pendingCommits).values({
253
256
  workspaceId,
254
257
  branch: input.branch,
255
258
  path: input.path,
256
- authorEmail: canonicalEmail(input.authorEmail),
259
+ authorEmail: email,
260
+ authorEmailBidx: email,
257
261
  authorName: input.authorName,
258
262
  });
259
263
  return true;
@@ -384,6 +388,64 @@ export class PendingCommitsService {
384
388
  .where(eq(pendingCommits.id, id));
385
389
  }
386
390
 
391
+ /**
392
+ * Take every commit still queued for `branch` out of the worker's reach and
393
+ * onto the admin surface, and answer how many moved.
394
+ *
395
+ * The one caller is the knowledge-base repository being REPLACED. A release
396
+ * ENQUEUES the bytes and only then drops its lock, so the moment the locks
397
+ * on a branch are dropped there may be rows already waiting — bytes that
398
+ * were written against a working copy of the repository that is gone. Left
399
+ * `pending`, the worker would pick them up after the re-clone and commit
400
+ * them onto a same-named branch of a DIFFERENT repository: someone else's
401
+ * file, overwritten by a change nobody made to it.
402
+ *
403
+ * `needs_attention`, not deleted — the same rule as everything else about a
404
+ * replacement. The bytes are still in the row, the reason is on it, and they
405
+ * show up on the pending-commit admin surface for a person to decide about.
406
+ * `running` rows go too: the worker checks the row back in when it finishes,
407
+ * and a row it is mid-flight with is exactly one whose destination just
408
+ * changed under it.
409
+ */
410
+ async markNeedsAttentionOnBranch(branch: string, error: string): Promise<number> {
411
+ return this.escalateQueued(eq(pendingCommits.branch, branch), error);
412
+ }
413
+
414
+ /** Every commit still queued that `which` selects goes to a person, with the reason. Answers how many. */
415
+ private async escalateQueued(which: SQL, error: string): Promise<number> {
416
+ const moved = await this.db
417
+ .update(pendingCommits)
418
+ .set({
419
+ status: 'needs_attention',
420
+ lastError: error,
421
+ lastAttemptedAt: new Date(),
422
+ })
423
+ .where(and(which, inArray(pendingCommits.status, ['pending', 'running'])))
424
+ .returning({ id: pendingCommits.id });
425
+ return moved.length;
426
+ }
427
+
428
+ /**
429
+ * The same, for every commit still queued against ONE WORKING COPY, and
430
+ * answer how many moved.
431
+ *
432
+ * The caller is the startup phase setting a working copy aside: a clone of
433
+ * a repository the configured one shares no history with. Whatever was
434
+ * queued for it was written against that clone, on any branch, the default
435
+ * one included, and the directory the worker would write to is now a fresh
436
+ * clone of another repository. Closing change requests covers the branches
437
+ * of the requests that were closed and nothing else, which is why this is
438
+ * keyed on the working copy: it is the thing that was replaced.
439
+ *
440
+ * A working copy that was only pointed at a new address (the same
441
+ * repository, moved) keeps its queue, and is never passed here.
442
+ */
443
+ async markNeedsAttentionInWorkspace(workspaceId: string, error: string): Promise<number> {
444
+ // The canonical form `enqueue` stores (see `canonicalWorkspaceId`), so the
445
+ // caller's spelling of the id cannot make this match nothing.
446
+ return this.escalateQueued(eq(pendingCommits.workspaceId, canonicalWorkspaceId(workspaceId)), error);
447
+ }
448
+
387
449
  /**
388
450
  * When the oldest commit still waiting to land was queued, or null when
389
451
  * nothing waits — the readiness answer's one number. `pending` and
@@ -41,8 +41,12 @@ export const RECOVERY_BOT_NAME = 'Bevel Recovery Bot';
41
41
  export async function ensureRecoveryBotUser(db: Database): Promise<AuthUser> {
42
42
  const inserted = await db
43
43
  .insert(users)
44
- .values({ email: RECOVERY_BOT_EMAIL, name: RECOVERY_BOT_NAME })
45
- .onConflictDoNothing({ target: users.email })
44
+ .values({
45
+ email: RECOVERY_BOT_EMAIL,
46
+ emailBidx: RECOVERY_BOT_EMAIL,
47
+ name: RECOVERY_BOT_NAME,
48
+ })
49
+ .onConflictDoNothing({ target: users.emailBidx })
46
50
  .returning();
47
51
  if (inserted.length > 0) {
48
52
  const row = inserted[0];
@@ -57,7 +61,7 @@ export async function ensureRecoveryBotUser(db: Database): Promise<AuthUser> {
57
61
  const [row] = await db
58
62
  .select()
59
63
  .from(users)
60
- .where(eq(users.email, RECOVERY_BOT_EMAIL))
64
+ .where(eq(users.emailBidx, RECOVERY_BOT_EMAIL))
61
65
  .limit(1);
62
66
  if (!row) {
63
67
  // Vanishingly unlikely: the row exists for the conflict to fire but is
@@ -247,6 +247,9 @@ describe('ReviewWorkflowService.carryApprovalsForward', () => {
247
247
  prNumber: PR,
248
248
  path: 'Sales/Terms.md',
249
249
  approverEmail: 'carol@bevel.software',
250
+ // The index column of a copied row is written with the address, not
251
+ // with the stored index read back beside it.
252
+ approverEmailBidx: 'carol@bevel.software',
250
253
  approverName: 'Carol',
251
254
  headSha: NEW_HEAD,
252
255
  // The reviewer approved THEN, not now — the merge is not an approval.
@@ -256,6 +259,7 @@ describe('ReviewWorkflowService.carryApprovalsForward', () => {
256
259
  prNumber: PR,
257
260
  path: 'Sales/Pricing.md',
258
261
  approverEmail: 'bob@bevel.software',
262
+ approverEmailBidx: 'bob@bevel.software',
259
263
  approverName: 'Bob',
260
264
  headSha: NEW_HEAD,
261
265
  approvedAt: APPROVED_AT,
@@ -1,4 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
+ import type { SQL } from 'drizzle-orm';
3
+ import { PgDialect } from 'drizzle-orm/pg-core';
2
4
  import { ReviewWorkflowService } from '../review-workflow.service.js';
3
5
  import type { ApprovalTx } from '../review-workflow.interface.js';
4
6
 
@@ -34,6 +36,8 @@ function harness() {
34
36
  const locked: unknown[][] = [];
35
37
  const lockSql: string[] = [];
36
38
  const sets: unknown[] = [];
39
+ /** The condition each rewrite was limited to, as the SQL and values it sends. */
40
+ const wheres: { sql: string; params: unknown[] }[] = [];
37
41
  const tx = {
38
42
  execute: async (statement: { queryChunks?: unknown[] }) => {
39
43
  const text = statementText(statement);
@@ -48,8 +52,9 @@ function harness() {
48
52
  },
49
53
  update: (table: unknown) => ({
50
54
  set: (values: unknown) => ({
51
- where: async () => {
55
+ where: async (condition: SQL) => {
52
56
  sets.push(values);
57
+ wheres.push(new PgDialect().sqlToQuery(condition));
53
58
  order.push(`update:${tableOf(table)}`);
54
59
  },
55
60
  }),
@@ -58,7 +63,7 @@ function harness() {
58
63
  // `eraseApprover` touches nothing but the transaction it is given, so the
59
64
  // service's own collaborators are never reached.
60
65
  const svc = new ReviewWorkflowService({} as never, {} as never, {} as never, {} as never);
61
- return { svc, tx: tx as unknown as ApprovalTx, order, locked, lockSql, sets };
66
+ return { svc, tx: tx as unknown as ApprovalTx, order, locked, lockSql, sets, wheres };
62
67
  }
63
68
 
64
69
  describe('ReviewWorkflowService.eraseApprover', () => {
@@ -79,6 +84,23 @@ describe('ReviewWorkflowService.eraseApprover', () => {
79
84
  expect(h.lockSql[0]).toContain('pg_advisory_xact_lock');
80
85
  expect(h.lockSql[0]).not.toMatch(/pg_advisory_lock\b/);
81
86
  // And the rewrite is to the placeholder identity it was given.
82
- expect(h.sets).toEqual([{ approverEmail: 'deleted-1@erased.invalid', approverName: 'Deleted user' }]);
87
+ // The blind index is rewritten with the email: nothing keyed to the
88
+ // erased address may survive, and the email itself is ciphertext. The
89
+ // index column is written with the address; the handle indexes it.
90
+ expect(h.sets).toEqual([
91
+ {
92
+ approverEmail: 'deleted-1@erased.invalid',
93
+ approverEmailBidx: 'deleted-1@erased.invalid',
94
+ approverName: 'Deleted user',
95
+ },
96
+ ]);
97
+ // WHICH rows: the ones whose blind index is the erased address's. The
98
+ // address column holds randomized ciphertext, so a match on it finds
99
+ // nothing — the rewrite would report success having rewritten no row, and
100
+ // every approval of the erased person would keep their name.
101
+ expect(h.wheres).toHaveLength(1);
102
+ expect(h.wheres[0]!.sql).toMatch(/"approver_email_bidx" = \$1$/);
103
+ expect(h.wheres[0]!.sql).not.toMatch(/"approver_email" =/);
104
+ expect(JSON.stringify(h.wheres[0]!.params)).toContain('bob@bevel.software');
83
105
  });
84
106
  });