@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
@@ -343,6 +343,7 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
343
343
  .values({
344
344
  prNumber,
345
345
  authorEmail: canonicalEmail(user.email),
346
+ authorEmailBidx: user.email,
346
347
  authorName: user.name,
347
348
  path: input.path ?? null,
348
349
  line: input.line ?? null,
@@ -600,6 +601,7 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
600
601
  prNumber,
601
602
  path,
602
603
  approverEmail: callerEmail,
604
+ approverEmailBidx: callerEmail,
603
605
  approverName: user.name,
604
606
  headSha,
605
607
  })
@@ -625,10 +627,13 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
625
627
  erased: { email: string; name: string },
626
628
  ): Promise<void> {
627
629
  await takeEveryApprovalLock(tx);
630
+ // Matched via the blind index — the email column is randomized
631
+ // ciphertext — and the index is rewritten to the placeholder's too, so no
632
+ // value keyed to the erased address survives.
628
633
  await tx
629
634
  .update(prFileApprovals)
630
- .set({ approverEmail: erased.email, approverName: erased.name })
631
- .where(eq(prFileApprovals.approverEmail, email));
635
+ .set({ approverEmail: erased.email, approverEmailBidx: erased.email, approverName: erased.name })
636
+ .where(eq(prFileApprovals.approverEmailBidx, email));
632
637
  }
633
638
 
634
639
  /**
@@ -714,6 +719,7 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
714
719
  .values({
715
720
  prNumber,
716
721
  triggeredByEmail,
722
+ triggeredByEmailBidx: triggeredByEmail,
717
723
  triggeredByName: user.name,
718
724
  headShaAtMerge: headSha,
719
725
  mergeMethod: MERGE_METHOD,
@@ -930,6 +936,9 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
930
936
  prNumber,
931
937
  path: r.path,
932
938
  approverEmail: r.approverEmail,
939
+ // The address, not the stored index read back beside it: an index
940
+ // column is written with what it is the index of.
941
+ approverEmailBidx: r.approverEmail,
933
942
  approverName: r.approverName,
934
943
  headSha: toHeadSha,
935
944
  approvedAt: r.approvedAt,
@@ -1013,7 +1022,7 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
1013
1022
  and(
1014
1023
  eq(prFileApprovals.prNumber, prNumber),
1015
1024
  eq(prFileApprovals.path, path),
1016
- eq(prFileApprovals.approverEmail, callerEmail),
1025
+ eq(prFileApprovals.approverEmailBidx, callerEmail),
1017
1026
  ),
1018
1027
  );
1019
1028
  });
@@ -1,14 +1,13 @@
1
- import type { ValidationReport } from '@bevel-software/platform-shared';
1
+ import type { AuthUser, ValidationReport } from '@bevel-software/platform-shared';
2
2
 
3
3
  /**
4
4
  * Workflow lifecycle hooks — the generic seam that replaced the
5
- * constructor-injected KB validator (and the core-owned ontology write block).
6
- * Core code invokes the hooks at fixed lifecycle points; the modules that OWN
7
- * the behavior (the enterprise kb/kb-graph modules today) register handlers in
8
- * the composition root, right after `createCoreServices` returns. Core
9
- * registers none, so a core-only deployment runs every hook point as a no-op.
5
+ * constructor-injected KB validator. Core code invokes the hooks at fixed
6
+ * lifecycle points; the modules that OWN the behavior register handlers in the
7
+ * composition root, right after `createCoreServices` returns. Core registers
8
+ * none, so a core-only deployment runs every hook point as a no-op.
10
9
  *
11
- * Two hook kinds with deliberately different failure semantics:
10
+ * Three hook kinds, with deliberately different failure semantics:
12
11
  *
13
12
  * - `commitValidation` — ADVISORY. Runs at commit time in `GitService`
14
13
  * (`commit` / `commitFile`), exactly where the injected `IKbValidator`
@@ -18,12 +17,18 @@ import type { ValidationReport } from '@bevel-software/platform-shared';
18
17
  * `runValidation(workspaceId)` shape so `KbValidatorService` registers
19
18
  * with a one-line adapter.
20
19
  *
21
- * - `preWrite` — BLOCKING. Runs before a gated agent write (file-write
22
- * tools) or a write-capable shell command; a hook that throws REJECTS the
23
- * operation (the error propagates to the tool caller). This is where the
24
- * enterprise ontology-session block registers; core registers nothing, so
25
- * core never blocks (touch TRACKING stays core — see
26
- * `session-ontology.gate.ts`).
20
+ * - `agentRead` — BLOCKING. Runs before a gated agent READ of a
21
+ * knowledge-base path (reads, `delete_file`, `delete_folder`, and each
22
+ * file a `grep` walk opens). A hook that throws REFUSES the read.
23
+ *
24
+ * - `preWrite` — BLOCKING. Runs before a gated agent WRITE (the file-write
25
+ * tools, once per path) or a write-capable shell command, which carries no
26
+ * path. A hook that throws REFUSES the operation, and the caller sees the
27
+ * hook's own message and status.
28
+ *
29
+ * The two blocking hooks are the seam a deployment builds a per-conversation
30
+ * rule on: they carry the session id the call named, and core itself neither
31
+ * decides nor records anything (see `workspace/agent-access.gate.ts`).
27
32
  */
28
33
 
29
34
  /** What `GitService` knows at the advisory commit-validation point. */
@@ -47,19 +52,35 @@ export type CommitValidationHook = (
47
52
  ) => Promise<ValidationReport | void>;
48
53
 
49
54
  /**
50
- * What the pre-write gate knows when it consults the blocking hooks. The core
51
- * gate has already resolved all skip conditions (boundary flag off, neutral
52
- * path, non-agent caller, recovery bot, missing session id — see
53
- * `resolveGatedSession`), so by hook time there is always a session id.
55
+ * What an agent operation looks like to the blocking hooks. The core gate has
56
+ * already resolved the only two skips it owns (the caller is not an agent, or
57
+ * is the recovery bot), so every hook call describes a real agent operation.
58
+ *
59
+ * `sessionId` is present only when the call carried one — core does NOT fail
60
+ * closed on its absence, because whether a path needs a session is the
61
+ * registering deployment's rule, not core's.
54
62
  */
55
- export type PreWriteContext =
56
- /** A path-addressed content write (write_file/edit_file/mkdir/move/copy/unzip…). */
57
- | { kind: 'file-write'; sessionId: string; wsPath: string }
58
- /** A write-capable operation with no resolvable target path (execute_command). */
59
- | { kind: 'shell'; sessionId: string };
63
+ export interface AgentOperationContext {
64
+ /** The conversation the call belongs to, when it named one. */
65
+ sessionId?: string;
66
+ /**
67
+ * The workspace-relative path the operation targets. Absent for a
68
+ * write-capable operation with no resolvable target (`execute_command`).
69
+ */
70
+ wsPath?: string;
71
+ /** The branch (draft) whose workspace the operation acts on. */
72
+ branch: string;
73
+ /** The calling user. */
74
+ user: AuthUser;
75
+ /** Whether the caller is the in-app agent (`internal`) or an external one. */
76
+ source: 'internal' | 'external';
77
+ }
60
78
 
61
- /** Blocking pre-write hook: throw to reject the write; return to allow. */
62
- export type PreWriteHook = (ctx: PreWriteContext) => Promise<void>;
79
+ /** Blocking pre-read hook: throw to refuse the read; return to allow. */
80
+ export type AgentReadHook = (ctx: AgentOperationContext) => Promise<void>;
81
+
82
+ /** Blocking pre-write hook: throw to refuse the write; return to allow. */
83
+ export type PreWriteHook = (ctx: AgentOperationContext) => Promise<void>;
63
84
 
64
85
  /**
65
86
  * The registry instance. ONE per composition (created in
@@ -68,6 +89,7 @@ export type PreWriteHook = (ctx: PreWriteContext) => Promise<void>;
68
89
  */
69
90
  export class WorkflowHooks {
70
91
  private readonly commitValidation: CommitValidationHook[] = [];
92
+ private readonly agentRead: AgentReadHook[] = [];
71
93
  private readonly preWrite: PreWriteHook[] = [];
72
94
 
73
95
  /** Register an advisory commit-time validation hook. */
@@ -75,6 +97,11 @@ export class WorkflowHooks {
75
97
  this.commitValidation.push(hook);
76
98
  }
77
99
 
100
+ /** Register a blocking pre-read hook. */
101
+ onAgentRead(hook: AgentReadHook): void {
102
+ this.agentRead.push(hook);
103
+ }
104
+
78
105
  /** Register a blocking pre-write hook. */
79
106
  onPreWrite(hook: PreWriteHook): void {
80
107
  this.preWrite.push(hook);
@@ -88,12 +115,23 @@ export class WorkflowHooks {
88
115
  return this.commitValidation;
89
116
  }
90
117
 
118
+ /**
119
+ * Run every blocking pre-read hook in registration order. The first throw
120
+ * propagates and refuses the read; with no hooks registered (core-only)
121
+ * this resolves immediately.
122
+ */
123
+ async runAgentRead(ctx: AgentOperationContext): Promise<void> {
124
+ for (const hook of this.agentRead) {
125
+ await hook(ctx);
126
+ }
127
+ }
128
+
91
129
  /**
92
130
  * Run every blocking pre-write hook in registration order. The first throw
93
- * propagates and rejects the write; with no hooks registered (core-only)
131
+ * propagates and refuses the write; with no hooks registered (core-only)
94
132
  * this resolves immediately.
95
133
  */
96
- async runPreWrite(ctx: PreWriteContext): Promise<void> {
134
+ async runPreWrite(ctx: AgentOperationContext): Promise<void> {
97
135
  for (const hook of this.preWrite) {
98
136
  await hook(ctx);
99
137
  }
@@ -63,7 +63,7 @@ import {
63
63
  isPlatformRestoreShape,
64
64
  } from '@bevel-software/platform-shared';
65
65
  import type { KbContext } from '../../shared/kb-context.js';
66
- import { and, desc, eq, inArray, isNotNull, isNull, lt, ne, or } from 'drizzle-orm';
66
+ import { and, desc, eq, inArray, isNotNull, isNull, lt, ne, or, sql } from 'drizzle-orm';
67
67
  import type { Database } from '../database/connection.js';
68
68
  import { changeRequests } from '../database/schema.js';
69
69
  import type { GitService } from './git/git.service.js';
@@ -324,9 +324,9 @@ export class WorkflowService implements IWorkflowService {
324
324
  * the composition root registers module-owned behavior against it
325
325
  * (`core.workflowService.hooks.onCommitValidation(…)` /
326
326
  * `.onPreWrite(…)`) after `createCoreServices` returns. The composition
327
- * root passes the SAME instance `GitService` and the session-ontology
328
- * gate consult; the default keeps test constructions (which don't
329
- * exercise hooks) unchanged.
327
+ * root passes the SAME instance `GitService` and the agent-access gate
328
+ * consult; the default keeps test constructions (which don't exercise
329
+ * hooks) unchanged.
330
330
  */
331
331
  public readonly hooks: WorkflowHooks = new WorkflowHooks(),
332
332
  ) {
@@ -910,6 +910,22 @@ export class WorkflowService implements IWorkflowService {
910
910
  return this.git.fileContentsAtCommit(workspaceId, path, sha);
911
911
  }
912
912
 
913
+ /**
914
+ * The BYTES of one file at a change — what the file page's byte-reading
915
+ * viewers (pdf, image, docx, pptx, xlsx, email) need to show a past save,
916
+ * and what "Download this version" hands over. `fileAtChange` above cannot
917
+ * serve them: it decodes to UTF-8, which ruins every format that is not
918
+ * text.
919
+ */
920
+ fileBytesAtChange(
921
+ workspaceId: string,
922
+ path: string,
923
+ sha: string,
924
+ side: 'after' | 'before',
925
+ ): Promise<{ bytes: Buffer; blobId: string } | null> {
926
+ return this.git.fileBytesAtCommit(workspaceId, path, sha, side);
927
+ }
928
+
913
929
  /**
914
930
  * Resolve write permission for `targetPath` against the access tree at
915
931
  * `HEAD` of `branch`. Throws `AccessDeniedError` on denial with the
@@ -1754,7 +1770,7 @@ export class WorkflowService implements IWorkflowService {
1754
1770
  .from(changeRequests)
1755
1771
  .where(
1756
1772
  and(
1757
- eq(changeRequests.authorEmail, email),
1773
+ eq(changeRequests.authorEmailBidx, email),
1758
1774
  eq(changeRequests.sourceBranch, sourceBranch),
1759
1775
  ne(changeRequests.state, 'open'),
1760
1776
  ),
@@ -1894,6 +1910,7 @@ export class WorkflowService implements IWorkflowService {
1894
1910
  title: input.title.trim(),
1895
1911
  body,
1896
1912
  authorEmail: canonicalEmail(user.email),
1913
+ authorEmailBidx: user.email,
1897
1914
  authorName: user.name,
1898
1915
  })
1899
1916
  .returning({ number: changeRequests.number });
@@ -3024,6 +3041,107 @@ export class WorkflowService implements IWorkflowService {
3024
3041
  return closed;
3025
3042
  }
3026
3043
 
3044
+ /**
3045
+ * How many change requests are open right now.
3046
+ *
3047
+ * Read by the setup screen's repository-change confirmation, which has to
3048
+ * say how many requests the admin is deciding about BEFORE the address is
3049
+ * saved. A plain count — nothing here reaches the repository, so it answers
3050
+ * just as well for a repository that is already gone.
3051
+ */
3052
+ async countOpenChangeRequests(): Promise<number> {
3053
+ const [row] = await this.db
3054
+ .select({ open: sql<number>`count(*)::int` })
3055
+ .from(changeRequests)
3056
+ .where(eq(changeRequests.state, 'open'));
3057
+ return row?.open ?? 0;
3058
+ }
3059
+
3060
+ /**
3061
+ * Close every open change request because the knowledge-base REPOSITORY was
3062
+ * replaced, and answer how many were closed.
3063
+ *
3064
+ * The admin's own choice on the setup screen: a repository that merely
3065
+ * MOVED keeps its requests (the branches came along), while a repository
3066
+ * that was REPLACED has none of those branches, so every request points at
3067
+ * something this deployment can no longer fetch. Nothing is deleted — the
3068
+ * rows stay, closed, carrying `closed_reason = 'repository-replaced'` so
3069
+ * the reason survives the person who chose it.
3070
+ *
3071
+ * Locks held on those branches go with them. Their holder cannot publish
3072
+ * the bytes (the branch is not in the new repository) and cannot release
3073
+ * the lock by finishing (the request is closed), so the row would sit there
3074
+ * refusing the path until its TTL ran out — and a still-connected client's
3075
+ * heartbeat keeps pushing that out.
3076
+ *
3077
+ * The close is ONE statement per pass — `update … where state = 'open'
3078
+ * returning` — not a count followed by a row-at-a-time loop. Read first and
3079
+ * then written, every request opened while the loop ran would be missed and
3080
+ * left open against a branch the new repository does not have. One
3081
+ * set-shaped statement cannot miss a row that exists when it runs, and the
3082
+ * guard is the statement's own `where`, so a merge or a withdrawal racing it
3083
+ * wins and its request is simply not in the returned set.
3084
+ *
3085
+ * Repeated while a pass still finds something, because a request can be
3086
+ * opened BETWEEN passes: the caller has not saved the new address yet, so
3087
+ * the deployment is still live and `POST /change-requests` is still being
3088
+ * served. Bounded, so a client opening requests in a loop cannot hold the
3089
+ * save open forever — what survives the last pass is a request opened after
3090
+ * the repository was replaced, which the deleted-branch sweep closes on its
3091
+ * own terms.
3092
+ */
3093
+ async closeOpenChangeRequestsAsRepositoryReplaced(): Promise<number> {
3094
+ let closed = 0;
3095
+ for (let pass = 0; pass < 5; pass++) {
3096
+ const now = new Date();
3097
+ const justClosed = await this.db
3098
+ .update(changeRequests)
3099
+ .set({ state: 'closed', closedAt: now, updatedAt: now, closedReason: 'repository-replaced' })
3100
+ .where(eq(changeRequests.state, 'open'))
3101
+ .returning({ number: changeRequests.number, sourceBranch: changeRequests.sourceBranch });
3102
+ if (justClosed.length === 0) break;
3103
+ for (const cr of justClosed) {
3104
+ // Best effort, per branch: a lock table that will not answer must not
3105
+ // leave the requests half closed — the close is the decision the admin
3106
+ // made, and the locks expire on their own within the minute.
3107
+ try {
3108
+ const released = await this.fileLocks.releaseAllOnBranch(cr.sourceBranch);
3109
+ if (released > 0) {
3110
+ crLog.info(`released ${released} file lock(s) held on "${cr.sourceBranch}"`);
3111
+ }
3112
+ } catch (err) {
3113
+ crLog.warn(`could not release the file locks on "${cr.sourceBranch}":`, { err });
3114
+ }
3115
+ // The bytes a release already enqueued belong to the repository that
3116
+ // is gone — see `markNeedsAttentionOnBranch`. Taken out of the
3117
+ // worker's reach in the same breath as the locks, because dropping a
3118
+ // lock is precisely what leaves a row behind.
3119
+ try {
3120
+ const quarantined = await this.pendingCommits.markNeedsAttentionOnBranch(
3121
+ cr.sourceBranch,
3122
+ 'The knowledge-base repository was replaced while this commit was still queued, so it was never ' +
3123
+ 'written. The bytes are kept here: the branch it was meant for belongs to the previous repository.',
3124
+ );
3125
+ if (quarantined > 0) {
3126
+ crLog.warn(
3127
+ `${quarantined} queued commit(s) on "${cr.sourceBranch}" need attention: the repository was replaced ` +
3128
+ 'before they landed, so they were not written to the new one.',
3129
+ );
3130
+ }
3131
+ } catch (err) {
3132
+ crLog.warn(`could not set aside the queued commits on "${cr.sourceBranch}":`, { err });
3133
+ }
3134
+ this.prs.invalidateDetailCache(cr.number);
3135
+ this.events?.emit({ kind: 'change-request-rejected', number: cr.number });
3136
+ closed++;
3137
+ }
3138
+ }
3139
+ if (closed > 0) {
3140
+ crLog.info(`closed ${closed} change request(s): the knowledge-base repository was replaced`);
3141
+ }
3142
+ return closed;
3143
+ }
3144
+
3027
3145
  /**
3028
3146
  * The sweep above, on demand. Boot runs it once (`create-core-server`),
3029
3147
  * which clears requests stranded BEFORE the process started; a branch that
@@ -3460,6 +3578,7 @@ export class WorkflowService implements IWorkflowService {
3460
3578
  applyFailureKind: failure.kind,
3461
3579
  applyFailedAt: at,
3462
3580
  applyFailedByName: user.name,
3581
+ applyFailedByEmailBidx: user.email,
3463
3582
  })
3464
3583
  // The write itself refuses to go backwards: the attempt map above is an
3465
3584
  // early exit within this process, but an older UPDATE still in flight (or
@@ -3520,6 +3639,7 @@ export class WorkflowService implements IWorkflowService {
3520
3639
  applyFailureConflicts: null,
3521
3640
  applyFailedAt: null,
3522
3641
  applyFailedByName: null,
3642
+ applyFailedByEmailBidx: null,
3523
3643
  applyFailureKind: null,
3524
3644
  })
3525
3645
  .where(
@@ -0,0 +1,208 @@
1
+ import { describe, expect, it, beforeEach } from 'vitest';
2
+ import {
3
+ notifyAgentRead,
4
+ assertAgentWriteAllowed,
5
+ SESSION_ID_DESCRIPTION,
6
+ SESSION_ID_INPUT,
7
+ ToolDescriptionNotes,
8
+ type AgentAccessGate,
9
+ } from '../agent-access.gate.js';
10
+ import { WorkflowHooks, type AgentOperationContext } from '../../workflow/workflow-hooks.js';
11
+ import { ToolError, type ToolContext } from '../../tool-helpers/tool.contract.js';
12
+
13
+ const RECOVERY_EMAIL = 'recovery-bot@bevel.local';
14
+ const P_FILE = 'knowledge-base/KnowledgeBase/Product/Knowledge/Roadmap.md';
15
+
16
+ function ctx(over: Partial<ToolContext> = {}): ToolContext {
17
+ return {
18
+ user: { id: 'u1', email: 'a@b.c', name: 'A' },
19
+ scope: 'write',
20
+ source: 'external',
21
+ sessionId: 's1',
22
+ abortSignal: new AbortController().signal,
23
+ workspaceService: {} as never,
24
+ workflowService: {} as never,
25
+ events: {} as never,
26
+ getFilesystem: async () => ({}) as never,
27
+ ...over,
28
+ } as ToolContext;
29
+ }
30
+
31
+ /** A gate with a fresh hook registry, plus the two lists of what the hooks saw. */
32
+ function makeGate(): { gate: AgentAccessGate; reads: AgentOperationContext[]; writes: AgentOperationContext[] } {
33
+ const hooks = new WorkflowHooks();
34
+ const reads: AgentOperationContext[] = [];
35
+ const writes: AgentOperationContext[] = [];
36
+ hooks.onAgentRead(async (op) => {
37
+ reads.push(op);
38
+ });
39
+ hooks.onPreWrite(async (op) => {
40
+ writes.push(op);
41
+ });
42
+ return { gate: { recoveryBotEmail: RECOVERY_EMAIL, hooks, notes: new ToolDescriptionNotes() }, reads, writes };
43
+ }
44
+
45
+ describe('notifyAgentRead', () => {
46
+ it('calls the read hook once with the session, path, branch, user and source', async () => {
47
+ const { gate, reads, writes } = makeGate();
48
+ await notifyAgentRead(gate, ctx(), 'draft-1', P_FILE);
49
+ expect(reads).toEqual([
50
+ {
51
+ sessionId: 's1',
52
+ wsPath: P_FILE,
53
+ branch: 'draft-1',
54
+ user: { id: 'u1', email: 'a@b.c', name: 'A' },
55
+ source: 'external',
56
+ },
57
+ ]);
58
+ expect(writes).toEqual([]);
59
+ });
60
+
61
+ it('carries the in-app agent as source `internal`', async () => {
62
+ const { gate, reads } = makeGate();
63
+ await notifyAgentRead(gate, ctx({ source: 'internal' }), 'main', P_FILE);
64
+ expect(reads[0].source).toBe('internal');
65
+ });
66
+
67
+ it('calls the hook with no session id when the call carried none — the hook decides', async () => {
68
+ const { gate, reads } = makeGate();
69
+ await notifyAgentRead(gate, ctx({ sessionId: undefined }), 'main', P_FILE);
70
+ expect(reads).toHaveLength(1);
71
+ expect(reads[0].sessionId).toBeUndefined();
72
+ });
73
+
74
+ it('never reaches the hooks for a caller that is not an agent', async () => {
75
+ const { gate, reads } = makeGate();
76
+ await notifyAgentRead(gate, ctx({ source: 'session' }), 'main', P_FILE);
77
+ expect(reads).toEqual([]);
78
+ });
79
+
80
+ it('never reaches the hooks for the recovery bot, whatever the case of its address', async () => {
81
+ const { gate, reads } = makeGate();
82
+ await notifyAgentRead(gate, ctx({ user: { id: 'bot', email: 'Recovery-Bot@Bevel.local', name: 'bot' } }), 'main', P_FILE);
83
+ expect(reads).toEqual([]);
84
+ });
85
+
86
+ it('refuses the read with the hook\'s own message and status', async () => {
87
+ const hooks = new WorkflowHooks();
88
+ hooks.onAgentRead(async () => {
89
+ throw new ToolError('Not in this conversation.', 403);
90
+ });
91
+ const gate: AgentAccessGate = { recoveryBotEmail: RECOVERY_EMAIL, hooks, notes: new ToolDescriptionNotes() };
92
+ await expect(notifyAgentRead(gate, ctx(), 'main', P_FILE)).rejects.toMatchObject({
93
+ message: 'Not in this conversation.',
94
+ status: 403,
95
+ });
96
+ });
97
+
98
+ it('is a no-op with no hook registered', async () => {
99
+ const gate: AgentAccessGate = {
100
+ recoveryBotEmail: RECOVERY_EMAIL,
101
+ hooks: new WorkflowHooks(),
102
+ notes: new ToolDescriptionNotes(),
103
+ };
104
+ await expect(notifyAgentRead(gate, ctx({ sessionId: undefined }), 'main', P_FILE)).resolves.toBeUndefined();
105
+ });
106
+ });
107
+
108
+ describe('assertAgentWriteAllowed', () => {
109
+ it('calls the write hook with the path, and never the read hook', async () => {
110
+ const { gate, reads, writes } = makeGate();
111
+ await assertAgentWriteAllowed(gate, ctx(), 'draft-1', P_FILE);
112
+ expect(reads).toEqual([]);
113
+ expect(writes).toHaveLength(1);
114
+ expect(writes[0]).toMatchObject({ sessionId: 's1', wsPath: P_FILE, branch: 'draft-1', source: 'external' });
115
+ });
116
+
117
+ it('calls the write hook with NO path for a write-capable call that has none', async () => {
118
+ const { gate, writes } = makeGate();
119
+ await assertAgentWriteAllowed(gate, ctx(), 'main');
120
+ expect(writes).toHaveLength(1);
121
+ expect(writes[0].wsPath).toBeUndefined();
122
+ expect(writes[0].branch).toBe('main');
123
+ });
124
+
125
+ it('refuses the write with the hook\'s own message and status', async () => {
126
+ const hooks = new WorkflowHooks();
127
+ hooks.onPreWrite(async () => {
128
+ throw new ToolError('Not in this conversation.', 403);
129
+ });
130
+ const gate: AgentAccessGate = { recoveryBotEmail: RECOVERY_EMAIL, hooks, notes: new ToolDescriptionNotes() };
131
+ await expect(assertAgentWriteAllowed(gate, ctx(), 'main', P_FILE)).rejects.toMatchObject({
132
+ message: 'Not in this conversation.',
133
+ status: 403,
134
+ });
135
+ });
136
+
137
+ it('runs every registered write hook, in registration order, until one throws', async () => {
138
+ const hooks = new WorkflowHooks();
139
+ const seen: string[] = [];
140
+ hooks.onPreWrite(async () => {
141
+ seen.push('first');
142
+ });
143
+ hooks.onPreWrite(async () => {
144
+ seen.push('second');
145
+ throw new ToolError('no', 403);
146
+ });
147
+ hooks.onPreWrite(async () => {
148
+ seen.push('third');
149
+ });
150
+ const gate: AgentAccessGate = { recoveryBotEmail: RECOVERY_EMAIL, hooks, notes: new ToolDescriptionNotes() };
151
+ await expect(assertAgentWriteAllowed(gate, ctx(), 'main', P_FILE)).rejects.toThrow('no');
152
+ expect(seen).toEqual(['first', 'second']);
153
+ });
154
+
155
+ it('never reaches the hooks for a person in the app or for the recovery bot', async () => {
156
+ const { gate, writes } = makeGate();
157
+ await assertAgentWriteAllowed(gate, ctx({ source: 'session' }), 'main', P_FILE);
158
+ await assertAgentWriteAllowed(gate, ctx({ user: { id: 'bot', email: RECOVERY_EMAIL, name: 'bot' } }), 'main', P_FILE);
159
+ expect(writes).toEqual([]);
160
+ });
161
+
162
+ it('is a no-op with no hook registered, with or without a session id', async () => {
163
+ const gate: AgentAccessGate = {
164
+ recoveryBotEmail: RECOVERY_EMAIL,
165
+ hooks: new WorkflowHooks(),
166
+ notes: new ToolDescriptionNotes(),
167
+ };
168
+ await expect(assertAgentWriteAllowed(gate, ctx(), 'main', P_FILE)).resolves.toBeUndefined();
169
+ await expect(assertAgentWriteAllowed(gate, ctx({ sessionId: undefined }), 'main', P_FILE)).resolves.toBeUndefined();
170
+ });
171
+ });
172
+
173
+ describe('ToolDescriptionNotes', () => {
174
+ let notes: ToolDescriptionNotes;
175
+ beforeEach(() => {
176
+ notes = new ToolDescriptionNotes();
177
+ });
178
+
179
+ it('has no note registered by default, which is every Hexis-only deployment', () => {
180
+ expect(notes.gatedToolNote()).toBe('');
181
+ expect(notes.sessionIdDescription()).toBe(SESSION_ID_DESCRIPTION);
182
+ });
183
+
184
+ it('appends a registered gated-tool note and a registered sessionId note', () => {
185
+ notes.registerGatedToolNote(' Stay put.');
186
+ notes.registerSessionIdNote(' It also pins the boundary.');
187
+ expect(notes.gatedToolNote()).toBe(' Stay put.');
188
+ expect(notes.sessionIdDescription()).toBe(`${SESSION_ID_DESCRIPTION} It also pins the boundary.`);
189
+ });
190
+
191
+ it('tells its subscribers on every registration, so mounted tools re-describe', () => {
192
+ let calls = 0;
193
+ notes.onChange(() => {
194
+ calls++;
195
+ });
196
+ notes.registerGatedToolNote(' a');
197
+ notes.registerSessionIdNote(' b');
198
+ expect(calls).toBe(2);
199
+ });
200
+
201
+ it('the default sessionId input says what the id is, and mentions no ontology', () => {
202
+ expect(SESSION_ID_INPUT.description).toBe(SESSION_ID_DESCRIPTION);
203
+ expect(SESSION_ID_DESCRIPTION).toMatch(/conversation/i);
204
+ expect(SESSION_ID_DESCRIPTION).toMatch(/start_session/);
205
+ expect(SESSION_ID_DESCRIPTION).toMatch(/ask/);
206
+ expect(SESSION_ID_DESCRIPTION.toLowerCase()).not.toContain('ontolog');
207
+ });
208
+ });