@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
@@ -2,6 +2,7 @@ import { sql } from 'drizzle-orm';
2
2
  import { drizzle } from 'drizzle-orm/node-postgres';
3
3
  import pg from 'pg';
4
4
  import * as schema from './schema.js';
5
+ import { PII_CIPHERTEXT_PREFIX, PiiParam, derivePiiKeys, type PiiKeys } from '../../shared/column-crypto.js';
5
6
 
6
7
  /**
7
8
  * One database, one schema per knowledge base.
@@ -74,6 +75,83 @@ export interface DbOptions {
74
75
  max?: number;
75
76
  /** How long an idle connection is kept before the pool closes it. Default: `pg`'s. */
76
77
  idleTimeoutMillis?: number;
78
+ /**
79
+ * The key this handle seals personal data with: the `SECRETS_ENC_KEY` of
80
+ * the knowledge base whose rows it holds (its own derived one, for a
81
+ * tenant). Without it the handle reads sealed values as their blobs and
82
+ * refuses to write one — right for a handle that only touches tables with
83
+ * no personal data, and a loud failure for one that was meant to have it.
84
+ */
85
+ piiKey?: string;
86
+ }
87
+
88
+ type QueryFn = (this: pg.Client, config?: unknown, values?: unknown, callback?: unknown) => unknown;
89
+ type ResultCallback = (err: unknown, result: unknown) => void;
90
+
91
+ /**
92
+ * The `pg` client of a handle that holds `keys`: the one place personal data
93
+ * changes form. Going in, every value a personal-data column marked (see
94
+ * `column-crypto.ts`) is replaced by its ciphertext or its blind index.
95
+ * Coming out, every sealed value of a result is opened — by its prefix, not
96
+ * by its column, so raw SQL through the handle reads plaintext too; a
97
+ * statement that needs what is STORED asks for it as bytes
98
+ * (`convert_to(col, 'UTF8')`).
99
+ *
100
+ * At the client, because that is what the handle owns: the pool builds every
101
+ * connection from this class, a transaction's included, so there is no path
102
+ * from this handle to the database that goes round it. Results that are
103
+ * streamed to a `Submittable` (a cursor) are not opened; nothing here uses one.
104
+ */
105
+ function keyedClient(keys: PiiKeys | null): typeof pg.Client {
106
+ const stored = (value: unknown): unknown => {
107
+ if (!(value instanceof PiiParam)) return value;
108
+ return keys ? value.stored(keys) : value.toPostgres();
109
+ };
110
+ const openCell = (value: unknown): unknown =>
111
+ typeof value === 'string' && value.startsWith(PII_CIPHERTEXT_PREFIX) ? keys!.read(value) : value;
112
+ const openOne = (result: unknown): void => {
113
+ const rows = (result as { rows?: unknown } | null)?.rows;
114
+ if (!Array.isArray(rows)) return;
115
+ for (const row of rows) {
116
+ if (Array.isArray(row)) {
117
+ for (let i = 0; i < row.length; i++) row[i] = openCell(row[i]);
118
+ } else if (row && typeof row === 'object') {
119
+ const record = row as Record<string, unknown>;
120
+ for (const column of Object.keys(record)) record[column] = openCell(record[column]);
121
+ }
122
+ }
123
+ };
124
+ const open = <T,>(result: T): T => {
125
+ if (!keys) return result;
126
+ // Several statements in one query string come back as several results.
127
+ if (Array.isArray(result)) result.forEach(openOne);
128
+ else openOne(result);
129
+ return result;
130
+ };
131
+
132
+ const base = pg.Client.prototype.query as unknown as QueryFn;
133
+ class KeyedClient extends pg.Client {}
134
+ const query: QueryFn = function (config, values, callback) {
135
+ if (typeof values === 'function') {
136
+ callback = values;
137
+ values = undefined;
138
+ }
139
+ if (Array.isArray(values)) {
140
+ values = values.map(stored);
141
+ } else if (config && typeof config === 'object' && Array.isArray((config as { values?: unknown }).values)) {
142
+ const withValues = config as { values: unknown[]; submit?: unknown };
143
+ if (typeof withValues.submit === 'function') withValues.values = withValues.values.map(stored);
144
+ else config = { ...withValues, values: withValues.values.map(stored) };
145
+ }
146
+ if (typeof callback === 'function') {
147
+ const done = callback as ResultCallback;
148
+ return base.call(this, config, values, (err: unknown, result: unknown) => done(err, err ? result : open(result)));
149
+ }
150
+ const pending = base.call(this, config, values);
151
+ return pending instanceof Promise ? pending.then(open) : pending;
152
+ };
153
+ Object.defineProperty(KeyedClient.prototype, 'query', { value: query, writable: true, configurable: true });
154
+ return KeyedClient;
77
155
  }
78
156
 
79
157
  /**
@@ -96,7 +174,11 @@ const CONNECT_TIMEOUT_MS = 30_000;
96
174
  */
97
175
  export function createDb(databaseUrl: string, opts: DbOptions = {}) {
98
176
  const dbSchema = assertSchemaName(opts.schema ?? DEFAULT_DB_SCHEMA);
177
+ // Only an ABSENT key means "this handle holds none". A blank or malformed
178
+ // one was meant to be a key, and is refused here rather than read as none.
179
+ const keys = opts.piiKey === undefined ? null : derivePiiKeys(opts.piiKey);
99
180
  const pool = new pg.Pool({
181
+ Client: keyedClient(keys),
100
182
  connectionString: databaseUrl,
101
183
  connectionTimeoutMillis: CONNECT_TIMEOUT_MS,
102
184
  max: opts.max,
@@ -109,6 +191,7 @@ export function createDb(databaseUrl: string, opts: DbOptions = {}) {
109
191
  });
110
192
  const db = drizzle(pool, { schema });
111
193
  schemas.set(db, dbSchema);
194
+ if (keys) piiKeys.set(db, { keys, of: opts.piiKey! });
112
195
  return db;
113
196
  }
114
197
 
@@ -119,7 +202,22 @@ export function dbSchemaOf(db: Pick<Database, '$client'>): string {
119
202
  return schemas.get(db as Database) ?? DEFAULT_DB_SCHEMA;
120
203
  }
121
204
 
205
+ /**
206
+ * The keys a handle seals personal data with — for the few callers that
207
+ * handle the stored form themselves (the backfill at start; an overlay's own
208
+ * SQL). Everything else never sees a key: the column types and the handle's
209
+ * connection do the work. Throws for a handle built without one.
210
+ */
211
+ export function piiKeysOf(db: Pick<Database, '$client'>): PiiKeys {
212
+ const held = piiKeys.get(db as Database);
213
+ if (!held) {
214
+ throw new Error('This database handle holds no personal-data key: build it with createDb/getDb and its piiKey.');
215
+ }
216
+ return held.keys;
217
+ }
218
+
122
219
  const schemas = new WeakMap<Database, string>();
220
+ const piiKeys = new WeakMap<Database, { keys: PiiKeys; of: string }>();
123
221
  const cache = new Map<string, Database>();
124
222
 
125
223
  const cacheKey = (databaseUrl: string, dbSchema: string) => `${dbSchema}\u0000${databaseUrl}`;
@@ -136,10 +234,29 @@ export function getDb(databaseUrl: string, opts: DbOptions = {}): Database {
136
234
  if (!db) {
137
235
  db = createDb(databaseUrl, opts);
138
236
  cache.set(key, db);
237
+ } else if (opts.piiKey !== undefined && !holdsKey(db, opts.piiKey)) {
238
+ // One schema is one knowledge base and one key. Handing back the cached
239
+ // pool would seal this caller's rows under the other caller's key.
240
+ throw new Error(`The database handle for schema "${dbSchema}" is already open with a different personal-data key.`);
139
241
  }
140
242
  return db;
141
243
  }
142
244
 
245
+ /**
246
+ * Whether `db` was opened with the key `piiKey` spells. The KEY, not its
247
+ * spelling: `SECRETS_ENC_KEY` may be written as hex or as base64, and one
248
+ * process can be handed the same 32 bytes both ways — refusing the second as
249
+ * "a different key" would refuse a caller that holds the right one. Compared
250
+ * through what the two derive, so no key material is held here to compare.
251
+ */
252
+ function holdsKey(db: Database, piiKey: string): boolean {
253
+ const held = piiKeys.get(db);
254
+ if (!held) return false;
255
+ if (held.of === piiKey) return true;
256
+ const probe = 'same-key?';
257
+ return held.keys.index(probe) === derivePiiKeys(piiKey).index(probe);
258
+ }
259
+
143
260
  /**
144
261
  * End a handle's pool and forget it, so a later {@link getDb} for the same
145
262
  * schema opens a fresh one rather than handing out a pool that was ended.
@@ -1,23 +1,40 @@
1
1
  /**
2
2
  * CORE platform tables — the git-backed workspace/workflow, auth, access,
3
- * change requests, locks, the pending-commits queue, the ontology-session
4
- * touched-set, connection keys, MCP OAuth, and the Secrets Vault. A core-only
5
- * deployment migrates and runs on exactly these tables.
3
+ * change requests, locks, the pending-commits queue, connection keys, MCP
4
+ * OAuth, and the Secrets Vault. A core-only deployment migrates and runs on
5
+ * exactly these tables.
6
6
  *
7
7
  * Enterprise-only tables (chat, routines, watchlist, connectors, LLM config,
8
8
  * SharePoint/Atlassian links, feedback, upload, kb-revalidation) live in
9
9
  * `enterprise-schema.ts`, which imports the FK targets (`users`,
10
10
  * `externalApiKeys`) from here. `schema.ts` re-exports both, so existing
11
11
  * imports keep working unchanged.
12
+ *
13
+ * PII columns (emails, display names, change-request/comment text) are
14
+ * `encryptedText` — AES-256-GCM ciphertext in the database, transparently
15
+ * decrypted on read (see `shared/column-crypto.ts`). Because the ciphertext is
16
+ * randomized, equality lookups and unique constraints on those columns go
17
+ * through their deterministic `*_bidx` companions (HMAC-SHA256 blind indexes,
18
+ * `blindIndexText`) — never `eq()` an encrypted column directly. A `*_bidx`
19
+ * column is written and compared WITH THE ADDRESS ITSELF; the database handle
20
+ * the statement runs on turns it into the index, under that handle's key.
12
21
  */
13
22
  import { sql } from 'drizzle-orm';
14
23
  import { boolean, check, index, integer, jsonb, pgTable, primaryKey, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core';
24
+ import { blindIndexText, encryptedText } from '../../shared/column-crypto.js';
15
25
 
16
26
  export const users = pgTable('users', {
17
27
  id: uuid('id').defaultRandom().primaryKey(),
18
- email: text('email').notNull().unique(),
19
- name: text('name').notNull(),
20
- avatarUrl: text('avatar_url'),
28
+ email: encryptedText('email').notNull(),
29
+ /**
30
+ * Blind index of the lowercased email — carries the uniqueness constraint
31
+ * and every lookup-by-email (login, upsert conflict target), which the
32
+ * randomized `email` ciphertext cannot. Write the email into it on every
33
+ * insert, and compare it with the email.
34
+ */
35
+ emailBidx: blindIndexText('email_bidx').notNull(),
36
+ name: encryptedText('name').notNull(),
37
+ avatarUrl: encryptedText('avatar_url'),
21
38
  /**
22
39
  * scrypt hash for password login (see auth/password-hash.ts). NULL for
23
40
  * accounts that only ever signed in via SSO — password login refuses them
@@ -37,9 +54,24 @@ export const users = pgTable('users', {
37
54
  * backfill would need, and being shown the setup once costs a click.
38
55
  */
39
56
  onboardingDone: boolean('onboarding_done').default(false).notNull(),
57
+ /**
58
+ * When an admin switched this account off; NULL while it is on. A
59
+ * deactivated account keeps its row, its history and its place in roles
60
+ * and groups, but nothing it holds is honoured: it cannot sign in, and its
61
+ * session, connection keys, agent tokens and internal tokens are refused
62
+ * the next time they are presented (see `AuthService.isActive`). Turning it
63
+ * back on restores all of them as they were.
64
+ *
65
+ * The off switch a host that sells seats needs — a seat is an account that
66
+ * is on — and the one an admin uses for someone who left without erasing
67
+ * what they did.
68
+ */
69
+ deactivatedAt: timestamp('deactivated_at'),
40
70
  createdAt: timestamp('created_at').defaultNow().notNull(),
41
71
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
42
- });
72
+ }, (t) => ({
73
+ emailBidxUnq: uniqueIndex('users_email_bidx_unq').on(t.emailBidx),
74
+ }));
43
75
 
44
76
  /**
45
77
  * Per-file owner approvals on a PR. A PR is mergeable-in-Bevel when every
@@ -56,15 +88,18 @@ export const prFileApprovals = pgTable('pr_file_approvals', {
56
88
  id: uuid('id').defaultRandom().primaryKey(),
57
89
  prNumber: integer('pr_number').notNull(),
58
90
  path: text('path').notNull(),
59
- approverEmail: text('approver_email').notNull(), // lowercased at insert
60
- approverName: text('approver_name').notNull(),
91
+ approverEmail: encryptedText('approver_email').notNull(), // lowercased at insert
92
+ /** Blind index of `approver_email` — the uniqueness key and eq() lookup. */
93
+ approverEmailBidx: blindIndexText('approver_email_bidx').notNull(),
94
+ approverName: encryptedText('approver_name').notNull(),
61
95
  headSha: text('head_sha').notNull(),
62
96
  approvedAt: timestamp('approved_at').defaultNow().notNull(),
63
97
  }, (t) => ({
64
98
  // Idempotency: approving the same path on the same SHA twice must be a no-op,
65
- // not a duplicate row. Unique on the (PR, path, approver, headSha) tuple.
66
- unq: uniqueIndex('pr_file_approvals_unq')
67
- .on(t.prNumber, t.path, t.approverEmail, t.headSha),
99
+ // not a duplicate row. Unique on the (PR, path, approver, headSha) tuple —
100
+ // via the blind index, because the encrypted email is randomized.
101
+ unq: uniqueIndex('pr_file_approvals_bidx_unq')
102
+ .on(t.prNumber, t.path, t.approverEmailBidx, t.headSha),
68
103
  byPr: index('pr_file_approvals_by_pr').on(t.prNumber),
69
104
  }));
70
105
 
@@ -78,12 +113,15 @@ export const prFileApprovals = pgTable('pr_file_approvals', {
78
113
  export const prMergeLog = pgTable('pr_merge_log', {
79
114
  id: uuid('id').defaultRandom().primaryKey(),
80
115
  prNumber: integer('pr_number').notNull(),
81
- triggeredByEmail: text('triggered_by_email').notNull(),
82
- triggeredByName: text('triggered_by_name').notNull(),
116
+ triggeredByEmail: encryptedText('triggered_by_email').notNull(),
117
+ /** Blind index of `triggered_by_email` — GDPR-erasure lookup. */
118
+ triggeredByEmailBidx: blindIndexText('triggered_by_email_bidx').notNull(),
119
+ triggeredByName: encryptedText('triggered_by_name').notNull(),
83
120
  headShaAtMerge: text('head_sha_at_merge').notNull(),
84
121
  mergeMethod: text('merge_method').notNull(),
85
122
  succeeded: boolean('succeeded').notNull(),
86
- error: text('error'),
123
+ // Encrypted: merge failure output can quote author identities and CR text.
124
+ error: encryptedText('error'),
87
125
  startedAt: timestamp('started_at').defaultNow().notNull(),
88
126
  completedAt: timestamp('completed_at'),
89
127
  }, (t) => ({
@@ -107,12 +145,15 @@ export const prMergeLog = pgTable('pr_merge_log', {
107
145
  export const prComments = pgTable('pr_comments', {
108
146
  id: uuid('id').defaultRandom().primaryKey(),
109
147
  prNumber: integer('pr_number').notNull(),
110
- authorEmail: text('author_email').notNull(),
111
- authorName: text('author_name').notNull(),
148
+ authorEmail: encryptedText('author_email').notNull(),
149
+ /** Blind index of `author_email` — GDPR-erasure lookup. */
150
+ authorEmailBidx: blindIndexText('author_email_bidx').notNull(),
151
+ authorName: encryptedText('author_name').notNull(),
112
152
  path: text('path'),
113
153
  line: integer('line'),
114
154
  headSha: text('head_sha').notNull(),
115
- body: text('body').notNull(),
155
+ // Encrypted: review conversation exists only in this DB (never on GitHub).
156
+ body: encryptedText('body').notNull(),
116
157
  parentId: uuid('parent_id'),
117
158
  createdAt: timestamp('created_at').defaultNow().notNull(),
118
159
  updatedAt: timestamp('updated_at'),
@@ -141,25 +182,53 @@ export const changeRequests = pgTable('change_requests', {
141
182
  number: integer('number').generatedAlwaysAsIdentity(),
142
183
  sourceBranch: text('source_branch').notNull(),
143
184
  targetBranch: text('target_branch').notNull(),
144
- title: text('title').notNull(),
145
- body: text('body').notNull().default(''),
146
- authorEmail: text('author_email').notNull(), // lowercased at insert
147
- authorName: text('author_name').notNull(),
185
+ // Encrypted: CR title/body exist only in this DB (the remote sees branches).
186
+ // The DB-level default '' bypasses toDriver, which is fine: the empty string
187
+ // is deliberately stored unencrypted (see encryptPii), so a defaulted row
188
+ // and an app-written empty body hold the identical representation.
189
+ title: encryptedText('title').notNull(),
190
+ body: encryptedText('body').notNull().default(''),
191
+ authorEmail: encryptedText('author_email').notNull(), // lowercased at insert
192
+ /** Blind index of `author_email` — GDPR-erasure lookup. */
193
+ authorEmailBidx: blindIndexText('author_email_bidx').notNull(),
194
+ authorName: encryptedText('author_name').notNull(),
148
195
  state: text('state').notNull().default('open'), // 'open' | 'merged' | 'closed'
149
196
  mergedSha: text('merged_sha'),
150
197
  // The last apply attempt that did not land (null when none, or once a gate
151
198
  // input it depended on changed). Persisted rather than only pushed to the
152
199
  // clicker so every viewer of the still-open request — its author first —
153
200
  // sees the refusal.
154
- applyFailureReason: text('apply_failure_reason'),
201
+ // Encrypted: the reason can quote identities the way the merge log's error
202
+ // does, and the name is a person's.
203
+ applyFailureReason: encryptedText('apply_failure_reason'),
155
204
  applyFailureConflicts: boolean('apply_failure_conflicts'),
156
205
  applyFailedAt: timestamp('apply_failed_at'),
157
- applyFailedByName: text('apply_failed_by_name'),
206
+ applyFailedByName: encryptedText('apply_failed_by_name'),
207
+ /**
208
+ * Blind index of the email of the person named above — what account erasure
209
+ * finds their name by. Written and cleared with the name, so a name never
210
+ * stands without it: a refusal recorded before the encryption release kept
211
+ * no address, and its name is taken off at the first start on this release
212
+ * (`clearUnindexedRefusalNames` in `migrate.ts`).
213
+ */
214
+ applyFailedByEmailBidx: blindIndexText('apply_failed_by_email_bidx'),
158
215
  /** What refused the last apply: 'gate' (approvals), 'conflicts' (git), 'error' (anything else). */
159
216
  applyFailureKind: text('apply_failure_kind'),
160
217
  createdAt: timestamp('created_at').defaultNow().notNull(),
161
218
  updatedAt: timestamp('updated_at'),
162
219
  closedAt: timestamp('closed_at'),
220
+ /**
221
+ * Why a closed request was closed, when something other than a person
222
+ * closed it. Today the one value is 'repository-replaced': the admin
223
+ * pointed the deployment at a different knowledge-base repository and chose
224
+ * to close the requests rather than keep them. Null for every request
225
+ * closed the ordinary way — by its author, or by the deleted-branch sweep —
226
+ * so the column reads as "nobody recorded a reason", not as a default.
227
+ *
228
+ * The row is never deleted: a replaced repository loses the branches, not
229
+ * the record of what people asked for.
230
+ */
231
+ closedReason: text('closed_reason'),
163
232
  }, (t) => ({
164
233
  numberUnq: uniqueIndex('change_requests_number_unq').on(t.number),
165
234
  // Enforce the "A→B blocks A→B while open" uniqueness rule at the DB level —
@@ -245,7 +314,7 @@ export const fileLocks = pgTable('file_locks', {
245
314
  branch: text('branch').notNull(),
246
315
  path: text('path').notNull(),
247
316
  holderUserId: uuid('holder_user_id').notNull().references(() => users.id),
248
- holderName: text('holder_name').notNull(),
317
+ holderName: encryptedText('holder_name').notNull(),
249
318
  // How the lock was acquired. 'edit' (the default) is a normal write hold —
250
319
  // the holder is editing and the release publishes their bytes.
251
320
  // 'coordination' is a pure-mutex hold (see IWorkflowService.acquireLock)
@@ -306,8 +375,10 @@ export const pendingCommits = pgTable('pending_commits', {
306
375
  // Author attribution captured at enqueue. The denormalised name/email pair
307
376
  // lands on the eventual `git commit --author=` so the commit history shows
308
377
  // the human who triggered the save, not the worker.
309
- authorEmail: text('author_email').notNull(), // lowercased at insert
310
- authorName: text('author_name').notNull(),
378
+ authorEmail: encryptedText('author_email').notNull(), // lowercased at insert
379
+ /** Blind index of `author_email` — GDPR-erasure lookup. */
380
+ authorEmailBidx: blindIndexText('author_email_bidx').notNull(),
381
+ authorName: encryptedText('author_name').notNull(),
311
382
  queuedAt: timestamp('queued_at').defaultNow().notNull(),
312
383
  // `running` is set while the worker is mid-commit so a second worker
313
384
  // (e.g. after a restart with another instance still draining) doesn't
@@ -320,7 +391,8 @@ export const pendingCommits = pgTable('pending_commits', {
320
391
  // `needs_attention` + feedback notice instead of looping forever.
321
392
  recoveryAgentRuns: integer('recovery_agent_runs').notNull().default(0),
322
393
  lastAttemptedAt: timestamp('last_attempted_at'),
323
- lastError: text('last_error'),
394
+ // Encrypted: git/agent failure output can quote author identities and paths.
395
+ lastError: encryptedText('last_error'),
324
396
  }, (t) => ({
325
397
  // Primary read pattern: drain in queued order, scoped to a workspace.
326
398
  byWorkspaceQueued: index('pending_commits_by_workspace_queued').on(t.workspaceId, t.queuedAt),
@@ -382,30 +454,6 @@ export const secrets = pgTable('secrets', {
382
454
  sharedKeyUnq: uniqueIndex('secrets_shared_key_unq').on(t.key).where(sql`${t.userId} is null`),
383
455
  }));
384
456
 
385
- /**
386
- * Ontology-session boundary — the per-session log of which named ontologies a
387
- * run has TOUCHED (one row per `(session_id, ontology)`). Each agent operation
388
- * that resolves to a named ontology records a row here (idempotent on the PK).
389
- *
390
- * The rule the gate enforces from this set:
391
- * - READS are always allowed; they just record the ontology they touched.
392
- * - WRITES are allowed only while the session's touched set is a SINGLE named
393
- * ontology equal to the write target. Once a session has touched two or
394
- * more ontologies (e.g. by reading across them), every write is blocked —
395
- * "read across ontologies → you can't write anymore at all."
396
- *
397
- * Postgres is the durable source of truth so the boundary holds across a
398
- * backend restart (an in-memory set would reset and silently un-poison a run).
399
- * A write-through in-process cache fronts this table for the chatty read path;
400
- * a cache miss falls back here.
401
- *
402
- * - PK is `(session_id, ontology)` — a set, idempotent on repeat touches.
403
- * - `ontology` is the resolved ontology id (e.g. `KnowledgeBase/Product`).
404
- * Neutral touches don't record, so they never write a row.
405
- * - `touched_at` drives the abandoned-run `sweepOlderThan` backstop; the
406
- * primary reclamation is an explicit delete of all of a session's rows at
407
- * run end.
408
- */
409
457
  /**
410
458
  * OAuth clients dynamically registered by MCP clients (RFC 7591 DCR). When an
411
459
  * MCP client (claude.ai, Claude Code) connects to `/api/mcp` without a
@@ -575,6 +623,13 @@ export const agentEvents = pgTable('agent_events', {
575
623
  ),
576
624
  }));
577
625
 
626
+ /**
627
+ * UNUSED IN THIS RELEASE. It backed the per-session boundary that a deployment
628
+ * now owns for itself, behind the agent read/write hooks; nothing in this
629
+ * package reads or writes it any more. The definition stays for ONE release so
630
+ * that no deployment runs a version in which neither side has the table, and
631
+ * the NEXT release drops it (see `TODOS.md`). Do not build on it.
632
+ */
578
633
  export const sessionOntologyTouches = pgTable('session_ontology_touches', {
579
634
  sessionId: text('session_id').notNull(),
580
635
  ontology: text('ontology').notNull(),
@@ -710,13 +765,15 @@ export const githubFacadeCodes = pgTable('github_facade_codes', {
710
765
  */
711
766
  export const pluginJoinRequests = pgTable('plugin_join_requests', {
712
767
  id: uuid('id').defaultRandom().primaryKey(),
713
- requesterEmail: text('requester_email').notNull(), // lowercased at insert
768
+ requesterEmail: encryptedText('requester_email').notNull(), // lowercased at insert
769
+ /** Blind index of `requester_email` — the uniqueness key, the by-requester read and the erasure delete. */
770
+ requesterEmailBidx: blindIndexText('requester_email_bidx').notNull(),
714
771
  /** Denormalised for the commit/change-request authorship, like `file_locks.holder_name`. */
715
- requesterName: text('requester_name').notNull(),
772
+ requesterName: encryptedText('requester_name').notNull(),
716
773
  pluginKey: text('plugin_key').notNull(),
717
774
  status: text('status').notNull().default('pending'),
718
- /** What the git work said when it refused — shown to the requester verbatim. */
719
- failureReason: text('failure_reason'),
775
+ /** What the git work said when it refused — shown to the requester verbatim. Encrypted: git quotes identities. */
776
+ failureReason: encryptedText('failure_reason'),
720
777
  changeRequestNumber: integer('change_request_number'),
721
778
  /**
722
779
  * When a process took this row's git work, and the whole of the mutual
@@ -762,8 +819,9 @@ export const pluginJoinRequests = pgTable('plugin_join_requests', {
762
819
  }, (t) => ({
763
820
  // One request per person per plugin — the DB's rule, not a caller's. Also
764
821
  // the index the plugin listing's by-requester read is served from.
765
- requesterPluginUnq: uniqueIndex('plugin_join_requests_requester_plugin_unq')
766
- .on(t.requesterEmail, t.pluginKey),
822
+ // Via the blind index, because the encrypted email is randomized.
823
+ requesterPluginUnq: uniqueIndex('plugin_join_requests_requester_bidx_plugin_unq')
824
+ .on(t.requesterEmailBidx, t.pluginKey),
767
825
  // The boot sweep: every row still `pending`, without a full scan.
768
826
  byStatus: index('plugin_join_requests_by_status').on(t.status),
769
827
  statusCheck: check(