@bevel-software/platform-core-backend 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (466) hide show
  1. package/dist/core/core-ports.d.ts +11 -1
  2. package/dist/core/core-ports.d.ts.map +1 -1
  3. package/dist/core/core-ports.js.map +1 -1
  4. package/dist/core/create-core-server.d.ts +3 -3
  5. package/dist/core/create-core-server.d.ts.map +1 -1
  6. package/dist/core/create-core-server.js +46 -13
  7. package/dist/core/create-core-server.js.map +1 -1
  8. package/dist/core/create-core-services.d.ts +12 -2
  9. package/dist/core/create-core-services.d.ts.map +1 -1
  10. package/dist/core/create-core-services.js +137 -24
  11. package/dist/core/create-core-services.js.map +1 -1
  12. package/dist/core/lifecycle.d.ts +46 -1
  13. package/dist/core/lifecycle.d.ts.map +1 -1
  14. package/dist/core/lifecycle.js +99 -13
  15. package/dist/core/lifecycle.js.map +1 -1
  16. package/dist/core-config.d.ts +16 -12
  17. package/dist/core-config.d.ts.map +1 -1
  18. package/dist/core-config.js +28 -13
  19. package/dist/core-config.js.map +1 -1
  20. package/dist/index.d.ts +6 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +12 -3
  23. package/dist/index.js.map +1 -1
  24. package/dist/modules/access/access-control.interface.d.ts +42 -12
  25. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  26. package/dist/modules/access/access-control.service.d.ts +67 -10
  27. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  28. package/dist/modules/access/access-control.service.js +214 -35
  29. package/dist/modules/access/access-control.service.js.map +1 -1
  30. package/dist/modules/access/access.routes.d.ts.map +1 -1
  31. package/dist/modules/access/access.routes.js +17 -16
  32. package/dist/modules/access/access.routes.js.map +1 -1
  33. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
  34. package/dist/modules/access/directory-sync-bot.js +7 -3
  35. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  36. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +8 -1
  37. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
  38. package/dist/modules/agent-instructions/agent-instructions.routes.js +8 -2
  39. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
  40. package/dist/modules/agent-instructions/compose.d.ts +38 -6
  41. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  42. package/dist/modules/agent-instructions/compose.js +39 -6
  43. package/dist/modules/agent-instructions/compose.js.map +1 -1
  44. package/dist/modules/agent-instructions/index.d.ts +2 -1
  45. package/dist/modules/agent-instructions/index.d.ts.map +1 -1
  46. package/dist/modules/agent-instructions/index.js +2 -1
  47. package/dist/modules/agent-instructions/index.js.map +1 -1
  48. package/dist/modules/agent-instructions/shared-file-rules.d.ts +115 -0
  49. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -0
  50. package/dist/modules/agent-instructions/shared-file-rules.js +272 -0
  51. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -0
  52. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  53. package/dist/modules/audit/agent-audit.service.js +4 -2
  54. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  55. package/dist/modules/auth/account-admission.d.ts +58 -7
  56. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  57. package/dist/modules/auth/account-admission.js +44 -1
  58. package/dist/modules/auth/account-admission.js.map +1 -1
  59. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
  60. package/dist/modules/auth/account-erasure.service.js +57 -16
  61. package/dist/modules/auth/account-erasure.service.js.map +1 -1
  62. package/dist/modules/auth/account.routes.d.ts +1 -1
  63. package/dist/modules/auth/account.routes.d.ts.map +1 -1
  64. package/dist/modules/auth/account.routes.js +61 -5
  65. package/dist/modules/auth/account.routes.js.map +1 -1
  66. package/dist/modules/auth/auth.middleware.d.ts +1 -1
  67. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  68. package/dist/modules/auth/auth.middleware.js +18 -7
  69. package/dist/modules/auth/auth.middleware.js.map +1 -1
  70. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  71. package/dist/modules/auth/auth.routes.js +8 -0
  72. package/dist/modules/auth/auth.routes.js.map +1 -1
  73. package/dist/modules/auth/auth.service.d.ts +70 -2
  74. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  75. package/dist/modules/auth/auth.service.js +197 -18
  76. package/dist/modules/auth/auth.service.js.map +1 -1
  77. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  78. package/dist/modules/auth/oidc-auth-provider.js +7 -2
  79. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  80. package/dist/modules/code-mode/code-mode.tool.d.ts +20 -2
  81. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  82. package/dist/modules/code-mode/code-mode.tool.js +66 -35
  83. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  84. package/dist/modules/database/connection.d.ts +16 -0
  85. package/dist/modules/database/connection.d.ts.map +1 -1
  86. package/dist/modules/database/connection.js +117 -0
  87. package/dist/modules/database/connection.js.map +1 -1
  88. package/dist/modules/database/core-schema.d.ts +337 -120
  89. package/dist/modules/database/core-schema.d.ts.map +1 -1
  90. package/dist/modules/database/core-schema.js +116 -58
  91. package/dist/modules/database/core-schema.js.map +1 -1
  92. package/dist/modules/database/migrate.d.ts +8 -0
  93. package/dist/modules/database/migrate.d.ts.map +1 -1
  94. package/dist/modules/database/migrate.js +271 -1
  95. package/dist/modules/database/migrate.js.map +1 -1
  96. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -1
  97. package/dist/modules/kb-fs/branch-name.js +12 -2
  98. package/dist/modules/kb-fs/branch-name.js.map +1 -1
  99. package/dist/modules/kb-fs/repo-path.d.ts +11 -16
  100. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  101. package/dist/modules/kb-fs/repo-path.js +79 -0
  102. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  103. package/dist/modules/kb-sync/kb-sync.routes.d.ts +2 -2
  104. package/dist/modules/kb-sync/kb-sync.routes.d.ts.map +1 -1
  105. package/dist/modules/kb-sync/kb-sync.routes.js +14 -3
  106. package/dist/modules/kb-sync/kb-sync.routes.js.map +1 -1
  107. package/dist/modules/kb-sync/sync-auth.d.ts +3 -1
  108. package/dist/modules/kb-sync/sync-auth.d.ts.map +1 -1
  109. package/dist/modules/kb-sync/sync-auth.js +1 -1
  110. package/dist/modules/kb-sync/sync-auth.js.map +1 -1
  111. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  112. package/dist/modules/mcp/mcp-auth.middleware.js +21 -6
  113. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  114. package/dist/modules/mcp/mcp.service.d.ts +8 -0
  115. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  116. package/dist/modules/mcp/mcp.service.js +38 -8
  117. package/dist/modules/mcp/mcp.service.js.map +1 -1
  118. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  119. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +3 -2
  120. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  121. package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -1
  122. package/dist/modules/plugins/join-request-records.store.js +7 -4
  123. package/dist/modules/plugins/join-request-records.store.js.map +1 -1
  124. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  125. package/dist/modules/settings/deployment-settings.service.js +8 -3
  126. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  127. package/dist/modules/settings/setup.routes.d.ts +52 -1
  128. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  129. package/dist/modules/settings/setup.routes.js +207 -19
  130. package/dist/modules/settings/setup.routes.js.map +1 -1
  131. package/dist/modules/skills/skills.contract.d.ts +90 -18
  132. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  133. package/dist/modules/skills/skills.contract.js +4 -1
  134. package/dist/modules/skills/skills.contract.js.map +1 -1
  135. package/dist/modules/skills/skills.service.d.ts +98 -9
  136. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  137. package/dist/modules/skills/skills.service.js +239 -37
  138. package/dist/modules/skills/skills.service.js.map +1 -1
  139. package/dist/modules/skills/skills.tools.d.ts +7 -0
  140. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  141. package/dist/modules/skills/skills.tools.js +71 -7
  142. package/dist/modules/skills/skills.tools.js.map +1 -1
  143. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  144. package/dist/modules/tool-auth/external-api-key.service.js +12 -4
  145. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  146. package/dist/modules/tool-auth/internal-token.service.d.ts +3 -3
  147. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +16 -7
  148. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  149. package/dist/modules/tool-auth/tool-auth.middleware.js +34 -12
  150. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  151. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -1
  152. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  153. package/dist/modules/tool-helpers/tool-handler.js +25 -4
  154. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  155. package/dist/modules/tool-helpers/tool.contract.d.ts +3 -2
  156. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  157. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  158. package/dist/modules/tool-helpers/validate-token.js +1 -1
  159. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  160. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  161. package/dist/modules/tool-manuals/tool-manuals.tools.js +42 -31
  162. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  163. package/dist/modules/tool-registry/description-length.d.ts +80 -0
  164. package/dist/modules/tool-registry/description-length.d.ts.map +1 -0
  165. package/dist/modules/tool-registry/description-length.js +108 -0
  166. package/dist/modules/tool-registry/description-length.js.map +1 -0
  167. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +98 -0
  168. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -0
  169. package/dist/modules/workflow/agent-tools/change-request-summary.js +81 -0
  170. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -0
  171. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  172. package/dist/modules/workflow/agent-tools/workflow.tools.js +75 -35
  173. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  174. package/dist/modules/workflow/file-lock.service.d.ts +24 -0
  175. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  176. package/dist/modules/workflow/file-lock.service.js +30 -0
  177. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  178. package/dist/modules/workflow/git/git.service.d.ts +94 -0
  179. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  180. package/dist/modules/workflow/git/git.service.js +193 -3
  181. package/dist/modules/workflow/git/git.service.js.map +1 -1
  182. package/dist/modules/workflow/pending-commits.service.d.ts +38 -0
  183. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  184. package/dist/modules/workflow/pending-commits.service.js +60 -1
  185. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  186. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
  187. package/dist/modules/workflow/recovery-bot.js +7 -3
  188. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  189. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  190. package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
  191. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  192. package/dist/modules/workflow/workflow-hooks.d.ts +54 -32
  193. package/dist/modules/workflow/workflow-hooks.d.ts.map +1 -1
  194. package/dist/modules/workflow/workflow-hooks.js +16 -1
  195. package/dist/modules/workflow/workflow-hooks.js.map +1 -1
  196. package/dist/modules/workflow/workflow.service.d.ts +60 -6
  197. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  198. package/dist/modules/workflow/workflow.service.js +115 -5
  199. package/dist/modules/workflow/workflow.service.js.map +1 -1
  200. package/dist/modules/workspace/agent-access.gate.d.ts +94 -0
  201. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -0
  202. package/dist/modules/workspace/agent-access.gate.js +123 -0
  203. package/dist/modules/workspace/agent-access.gate.js.map +1 -0
  204. package/dist/modules/workspace/agent-upload.routes.d.ts +77 -0
  205. package/dist/modules/workspace/agent-upload.routes.d.ts.map +1 -0
  206. package/dist/modules/workspace/agent-upload.routes.js +210 -0
  207. package/dist/modules/workspace/agent-upload.routes.js.map +1 -0
  208. package/dist/modules/workspace/agent-upload.store.d.ts +284 -0
  209. package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -0
  210. package/dist/modules/workspace/agent-upload.store.js +553 -0
  211. package/dist/modules/workspace/agent-upload.store.js.map +1 -0
  212. package/dist/modules/workspace/routine-write-policy.d.ts +5 -6
  213. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  214. package/dist/modules/workspace/routine-write-policy.js +5 -6
  215. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  216. package/dist/modules/workspace/session-sink.d.ts +5 -5
  217. package/dist/modules/workspace/set-aside-clone.d.ts +46 -0
  218. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -0
  219. package/dist/modules/workspace/set-aside-clone.js +92 -0
  220. package/dist/modules/workspace/set-aside-clone.js.map +1 -0
  221. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +59 -9
  222. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  223. package/dist/modules/workspace/startup/kb-startup-runner.js +65 -24
  224. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  225. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  226. package/dist/modules/workspace/startup/steps/seed-tree.js +3 -3
  227. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  228. package/dist/modules/workspace/startup/steps/template-source.d.ts +40 -0
  229. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  230. package/dist/modules/workspace/startup/steps/template-source.js +46 -4
  231. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  232. package/dist/modules/workspace/upload-limits.d.ts +13 -0
  233. package/dist/modules/workspace/upload-limits.d.ts.map +1 -0
  234. package/dist/modules/workspace/upload-limits.js +13 -0
  235. package/dist/modules/workspace/upload-limits.js.map +1 -0
  236. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  237. package/dist/modules/workspace/workspace.routes.js +93 -4
  238. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  239. package/dist/modules/workspace/workspace.service.d.ts +128 -5
  240. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  241. package/dist/modules/workspace/workspace.service.js +316 -58
  242. package/dist/modules/workspace/workspace.service.js.map +1 -1
  243. package/dist/modules/workspace/workspace.tools.d.ts +13 -11
  244. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  245. package/dist/modules/workspace/workspace.tools.js +791 -198
  246. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  247. package/dist/modules/workspace/write-denial.d.ts +0 -6
  248. package/dist/modules/workspace/write-denial.d.ts.map +1 -1
  249. package/dist/modules/workspace/write-denial.js +0 -6
  250. package/dist/modules/workspace/write-denial.js.map +1 -1
  251. package/dist/modules/workspace/zip-entry-rules.d.ts +114 -0
  252. package/dist/modules/workspace/zip-entry-rules.d.ts.map +1 -0
  253. package/dist/modules/workspace/zip-entry-rules.js +154 -0
  254. package/dist/modules/workspace/zip-entry-rules.js.map +1 -0
  255. package/dist/modules/write-access/write-access.d.ts +60 -0
  256. package/dist/modules/write-access/write-access.d.ts.map +1 -0
  257. package/dist/modules/write-access/write-access.js +129 -0
  258. package/dist/modules/write-access/write-access.js.map +1 -0
  259. package/dist/shared/column-crypto.d.ts +194 -0
  260. package/dist/shared/column-crypto.d.ts.map +1 -0
  261. package/dist/shared/column-crypto.js +144 -0
  262. package/dist/shared/column-crypto.js.map +1 -0
  263. package/dist/shared/domain-errors.d.ts +25 -0
  264. package/dist/shared/domain-errors.d.ts.map +1 -1
  265. package/dist/shared/domain-errors.js +28 -0
  266. package/dist/shared/domain-errors.js.map +1 -1
  267. package/dist/shared/git.contract.d.ts +20 -0
  268. package/dist/shared/git.contract.d.ts.map +1 -1
  269. package/dist/shared/git.contract.js +26 -0
  270. package/dist/shared/git.contract.js.map +1 -1
  271. package/dist/shared/token-crypto.d.ts.map +1 -1
  272. package/dist/shared/token-crypto.js +25 -1
  273. package/dist/shared/token-crypto.js.map +1 -1
  274. package/dist/tenancy/static-tenant-source.d.ts +0 -1
  275. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  276. package/dist/tenancy/static-tenant-source.js +1 -2
  277. package/dist/tenancy/static-tenant-source.js.map +1 -1
  278. package/dist/tenancy/tenant-secrets.d.ts +5 -1
  279. package/dist/tenancy/tenant-secrets.d.ts.map +1 -1
  280. package/dist/tenancy/tenant-secrets.js +4 -0
  281. package/dist/tenancy/tenant-secrets.js.map +1 -1
  282. package/kb-template/AGENTS.md +2 -0
  283. package/migrations/0014_change_request_closed_reason.sql +1 -0
  284. package/migrations/0015_account_deactivation.sql +3 -0
  285. package/migrations/0016_pii_encryption.sql +20 -0
  286. package/migrations/meta/0014_snapshot.json +2265 -0
  287. package/migrations/meta/0015_snapshot.json +2271 -0
  288. package/migrations/meta/0016_snapshot.json +2327 -0
  289. package/migrations/meta/_journal.json +21 -0
  290. package/package.json +3 -3
  291. package/src/__tests__/kb-layout-config.test.ts +0 -2
  292. package/src/__tests__/retired-settings.test.ts +96 -0
  293. package/src/core/__tests__/gated-boot.test.ts +132 -0
  294. package/src/core/__tests__/lifecycle.test.ts +241 -5
  295. package/src/core/__tests__/set-aside-root-is-one-place.test.ts +63 -0
  296. package/src/core/core-ports.ts +11 -1
  297. package/src/core/create-core-server.ts +52 -16
  298. package/src/core/create-core-services.ts +156 -24
  299. package/src/core/lifecycle.ts +117 -13
  300. package/src/core-config.ts +31 -14
  301. package/src/index.ts +30 -1
  302. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +385 -0
  303. package/src/modules/access/__tests__/access-control.prospective.test.ts +94 -18
  304. package/src/modules/access/__tests__/access.routes.prospective.test.ts +6 -7
  305. package/src/modules/access/__tests__/users-db-double.ts +21 -12
  306. package/src/modules/access/access-control.interface.ts +43 -12
  307. package/src/modules/access/access-control.service.ts +234 -36
  308. package/src/modules/access/access.routes.ts +17 -16
  309. package/src/modules/access/directory-sync-bot.ts +7 -3
  310. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +11 -5
  311. package/src/modules/agent-instructions/__tests__/compose.test.ts +19 -10
  312. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +238 -0
  313. package/src/modules/agent-instructions/agent-instructions.routes.ts +12 -2
  314. package/src/modules/agent-instructions/compose.ts +50 -7
  315. package/src/modules/agent-instructions/index.ts +12 -0
  316. package/src/modules/agent-instructions/shared-file-rules.ts +314 -0
  317. package/src/modules/audit/agent-audit.service.ts +4 -2
  318. package/src/modules/auth/__tests__/account-deactivation.test.ts +253 -0
  319. package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +6 -2
  320. package/src/modules/auth/__tests__/account.routes.test.ts +87 -6
  321. package/src/modules/auth/__tests__/auth.middleware.test.ts +45 -22
  322. package/src/modules/auth/__tests__/auth.service.test.ts +4 -2
  323. package/src/modules/auth/account-admission.ts +78 -9
  324. package/src/modules/auth/account-erasure.service.ts +69 -18
  325. package/src/modules/auth/account.routes.ts +63 -7
  326. package/src/modules/auth/auth.middleware.ts +19 -8
  327. package/src/modules/auth/auth.routes.ts +8 -0
  328. package/src/modules/auth/auth.service.ts +206 -23
  329. package/src/modules/auth/oidc-auth-provider.ts +7 -2
  330. package/src/modules/code-mode/__tests__/chain-runtime.e2e.test.ts +335 -0
  331. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +46 -2
  332. package/src/modules/code-mode/code-mode.tool.ts +80 -34
  333. package/src/modules/database/__tests__/connection.test.ts +12 -0
  334. package/src/modules/database/__tests__/pii-backfill.pg.test.ts +561 -0
  335. package/src/modules/database/connection.ts +117 -0
  336. package/src/modules/database/core-schema.ts +116 -58
  337. package/src/modules/database/migrate.ts +353 -1
  338. package/src/modules/kb-fs/__tests__/branch-name.test.ts +10 -0
  339. package/src/modules/kb-fs/__tests__/repo-path.test.ts +123 -0
  340. package/src/modules/kb-fs/branch-name.ts +14 -1
  341. package/src/modules/kb-fs/repo-path.ts +85 -0
  342. package/src/modules/kb-sync/__tests__/kb-sync.routes.test.ts +26 -1
  343. package/src/modules/kb-sync/kb-sync.routes.ts +14 -4
  344. package/src/modules/kb-sync/sync-auth.ts +2 -2
  345. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -3
  346. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +4 -3
  347. package/src/modules/mcp/__tests__/mcp.service.test.ts +115 -13
  348. package/src/modules/mcp/mcp-auth.middleware.ts +20 -6
  349. package/src/modules/mcp/mcp.service.ts +46 -7
  350. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +3 -1
  351. package/src/modules/plugins/__tests__/plugins.tools.test.ts +2 -1
  352. package/src/modules/plugins/join-request-records.store.ts +7 -4
  353. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +16 -0
  354. package/src/modules/settings/__tests__/setup.routes.git-mode.test.ts +91 -58
  355. package/src/modules/settings/__tests__/setup.routes.github-app.test.ts +25 -8
  356. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +14 -11
  357. package/src/modules/settings/__tests__/setup.routes.repository-change.test.ts +390 -0
  358. package/src/modules/settings/__tests__/setup.routes.test.ts +3 -0
  359. package/src/modules/settings/deployment-settings.service.ts +8 -3
  360. package/src/modules/settings/setup.routes.ts +263 -19
  361. package/src/modules/skills/__tests__/allowed-tools-warn.tools.test.ts +2 -1
  362. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +218 -0
  363. package/src/modules/skills/__tests__/skills.service.test.ts +229 -5
  364. package/src/modules/skills/skills.contract.ts +91 -18
  365. package/src/modules/skills/skills.service.ts +278 -41
  366. package/src/modules/skills/skills.tools.ts +80 -8
  367. package/src/modules/tool-auth/__tests__/manual-auth.middleware.test.ts +14 -1
  368. package/src/modules/tool-auth/external-api-key.service.ts +12 -4
  369. package/src/modules/tool-auth/internal-token.service.ts +3 -3
  370. package/src/modules/tool-auth/tool-auth.middleware.ts +34 -11
  371. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +5 -3
  372. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +3 -3
  373. package/src/modules/tool-helpers/__tests__/validate-token.test.ts +11 -1
  374. package/src/modules/tool-helpers/tool-handler.ts +24 -4
  375. package/src/modules/tool-helpers/tool.contract.ts +3 -2
  376. package/src/modules/tool-helpers/validate-token.ts +1 -1
  377. package/src/modules/tool-manuals/tool-manuals.tools.ts +44 -31
  378. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +378 -0
  379. package/src/modules/tool-registry/description-length.ts +111 -0
  380. package/src/modules/workflow/__tests__/pending-commits.repository-replaced.test.ts +111 -0
  381. package/src/modules/workflow/__tests__/workflow.routes.history-read-gate.test.ts +25 -0
  382. package/src/modules/workflow/__tests__/workflow.service.repository-replaced.test.ts +249 -0
  383. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +264 -6
  384. package/src/modules/workflow/agent-tools/change-request-summary.ts +182 -0
  385. package/src/modules/workflow/agent-tools/workflow.tools.ts +86 -35
  386. package/src/modules/workflow/file-lock.service.ts +31 -0
  387. package/src/modules/workflow/git/__tests__/git.service.fileBytesAtCommit.test.ts +260 -0
  388. package/src/modules/workflow/git/__tests__/git.service.prFetchFailure.test.ts +170 -0
  389. package/src/modules/workflow/git/git.service.ts +215 -1
  390. package/src/modules/workflow/pending-commits.service.ts +64 -2
  391. package/src/modules/workflow/recovery-bot.ts +7 -3
  392. package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +4 -0
  393. package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +25 -3
  394. package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
  395. package/src/modules/workflow/workflow-hooks.ts +64 -26
  396. package/src/modules/workflow/workflow.service.ts +125 -5
  397. package/src/modules/workspace/__tests__/agent-access.gate.test.ts +208 -0
  398. package/src/modules/workspace/__tests__/agent-uploads.test.ts +1604 -0
  399. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +394 -0
  400. package/src/modules/workspace/__tests__/file-stat-access.test.ts +2 -1
  401. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -1
  402. package/src/modules/workspace/__tests__/set-aside-clone.test.ts +52 -0
  403. package/src/modules/workspace/__tests__/workspace.routes.at-ref.test.ts +305 -0
  404. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -0
  405. package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +156 -0
  406. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +147 -0
  407. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +402 -0
  408. package/src/modules/workspace/__tests__/workspace.service.test.ts +77 -9
  409. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +41 -39
  410. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +2 -3
  411. package/src/modules/workspace/__tests__/workspace.tools.test.ts +766 -56
  412. package/src/modules/workspace/agent-access.gate.ts +164 -0
  413. package/src/modules/workspace/agent-upload.routes.ts +214 -0
  414. package/src/modules/workspace/agent-upload.store.ts +668 -0
  415. package/src/modules/workspace/routine-write-policy.ts +5 -6
  416. package/src/modules/workspace/session-sink.ts +5 -5
  417. package/src/modules/workspace/set-aside-clone.ts +96 -0
  418. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +86 -0
  419. package/src/modules/workspace/startup/kb-startup-runner.ts +115 -34
  420. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +9 -9
  421. package/src/modules/workspace/startup/steps/seed-tree.ts +3 -6
  422. package/src/modules/workspace/startup/steps/template-source.ts +53 -5
  423. package/src/modules/workspace/upload-limits.ts +12 -0
  424. package/src/modules/workspace/workspace.routes.ts +96 -5
  425. package/src/modules/workspace/workspace.service.ts +319 -61
  426. package/src/modules/workspace/workspace.tools.ts +894 -214
  427. package/src/modules/workspace/write-denial.ts +0 -8
  428. package/src/modules/workspace/zip-entry-rules.ts +173 -0
  429. package/src/modules/write-access/__tests__/write-access.test.ts +248 -0
  430. package/src/modules/write-access/write-access.ts +153 -0
  431. package/src/shared/__tests__/column-crypto.test.ts +217 -0
  432. package/src/shared/column-crypto.ts +218 -0
  433. package/src/shared/domain-errors.ts +31 -0
  434. package/src/shared/git.contract.ts +27 -0
  435. package/src/shared/token-crypto.ts +28 -1
  436. package/src/tenancy/__tests__/static-tenant-source.test.ts +4 -1
  437. package/src/tenancy/static-tenant-source.ts +1 -3
  438. package/src/tenancy/tenant-secrets.ts +5 -1
  439. package/dist/modules/workflow/session-ontology.policy.d.ts +0 -52
  440. package/dist/modules/workflow/session-ontology.policy.d.ts.map +0 -1
  441. package/dist/modules/workflow/session-ontology.policy.js +0 -62
  442. package/dist/modules/workflow/session-ontology.policy.js.map +0 -1
  443. package/dist/modules/workflow/session-ontology.service.d.ts +0 -105
  444. package/dist/modules/workflow/session-ontology.service.d.ts.map +0 -1
  445. package/dist/modules/workflow/session-ontology.service.js +0 -147
  446. package/dist/modules/workflow/session-ontology.service.js.map +0 -1
  447. package/dist/modules/workspace/session-ontology.gate.d.ts +0 -114
  448. package/dist/modules/workspace/session-ontology.gate.d.ts.map +0 -1
  449. package/dist/modules/workspace/session-ontology.gate.js +0 -161
  450. package/dist/modules/workspace/session-ontology.gate.js.map +0 -1
  451. package/dist/shared/kb-layout.d.ts +0 -39
  452. package/dist/shared/kb-layout.d.ts.map +0 -1
  453. package/dist/shared/kb-layout.js +0 -103
  454. package/dist/shared/kb-layout.js.map +0 -1
  455. package/dist/shared/kb-layout.test.d.ts +0 -2
  456. package/dist/shared/kb-layout.test.d.ts.map +0 -1
  457. package/dist/shared/kb-layout.test.js +0 -75
  458. package/dist/shared/kb-layout.test.js.map +0 -1
  459. package/src/modules/workflow/__tests__/session-ontology.policy.test.ts +0 -62
  460. package/src/modules/workflow/__tests__/session-ontology.service.test.ts +0 -201
  461. package/src/modules/workflow/session-ontology.policy.ts +0 -70
  462. package/src/modules/workflow/session-ontology.service.ts +0 -183
  463. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +0 -239
  464. package/src/modules/workspace/session-ontology.gate.ts +0 -191
  465. package/src/shared/kb-layout.test.ts +0 -98
  466. package/src/shared/kb-layout.ts +0 -102
@@ -0,0 +1,314 @@
1
+ /**
2
+ * The rules that apply to MORE THAN ONE file tool, written ONCE.
3
+ *
4
+ * They used to be appended, in full, to every tool description that they
5
+ * covered: the content rule rode on all twelve file tools, the agent-guide
6
+ * reminder on all of them plus the shell, the write-mode and escape-sequence
7
+ * paragraphs on two or three apiece. A description then ran to two or three
8
+ * thousand characters, most of it text the agent had already read on the tool
9
+ * above — and clients cut long descriptions from the END, which is where the
10
+ * text specific to the tool sits. Agents saw `file_stat`, `read_file`,
11
+ * `write_file` and `write_files` arrive ending in "[truncated]".
12
+ *
13
+ * So the shared rules are stated in TWO places and in neither description:
14
+ *
15
+ * - the MCP `instructions` of the initialize handshake (see `compose.ts`),
16
+ * which Claude Code, Claude Desktop and Cursor put in the system prompt;
17
+ * - the platform-managed agent guide at the repository root (`AGENTS.md` by
18
+ * default), rendered from `{{sharedFileRules}}` in the template — claude.ai
19
+ * on the web, the Agent SDK and Cline drop `instructions`, and a guide the
20
+ * agent is told to read before its first action is always available.
21
+ *
22
+ * Both places get the SAME string, from {@link sharedFileRulesSection} — not
23
+ * two hand-mirrored copies. A rule written twice is a rule that drifts, and a
24
+ * drifted rule is worse than a repeated one, because the agent cannot tell
25
+ * which copy is current. Each description ends instead with
26
+ * {@link sharedRulesPointer}, one sentence naming the section and the file.
27
+ *
28
+ * Pure text, a function of the layout only: the guide's file name is a
29
+ * deployment setting, so nothing here may snapshot `AGENTS.md`.
30
+ */
31
+
32
+ import {
33
+ DEFAULT_KB_LAYOUT,
34
+ agentsFileOf,
35
+ LEGACY_AGENTS_FILE,
36
+ platformFilesByDepth,
37
+ type KbLayout,
38
+ } from '@bevel-software/platform-shared';
39
+ import { CHAIN_FAILURES_RULE, CHAIN_LARGE_RESULTS_RULE } from '@bevel-software/platform-mcp-core';
40
+
41
+ /** The heading the rules live under, in both places and in the pointer sentence. */
42
+ export const SHARED_RULES_SECTION = 'Working with files';
43
+
44
+ /**
45
+ * The guide's name on a knowledge base seeded before it was renamed to
46
+ * {@link LEGACY_AGENTS_FILE}. Named in the conventions rule because the seeder
47
+ * never deletes a file it did not expect, so such a knowledge base still
48
+ * carries one.
49
+ */
50
+ const PRE_RENAME_AGENTS_FILE = 'CLAUDE.md';
51
+
52
+ /**
53
+ * The ceiling on the whole section. Pinned by a test: the section lands in
54
+ * every conversation through two channels, and a rule moved here to shorten a
55
+ * description has only moved the cost if the section itself grows without
56
+ * limit. Measured against {@link sharedFileRulesSection} under the default
57
+ * layout.
58
+ *
59
+ * 7,000 since two rules moved in from descriptions they took past their cap:
60
+ * the chain's own (what a failed chain and an oversized result answer), true
61
+ * of every call rather than of how to write one, and why large, escape-heavy
62
+ * and binary content goes by upload, which three write tools each said in
63
+ * full.
64
+ */
65
+ export const SHARED_FILE_RULES_CAP = 7_000;
66
+
67
+ /** One shared rule: how the guide heads it, and the rule itself. */
68
+ export interface SharedFileRule {
69
+ /** Stable id, so a test can name the rule that went missing. */
70
+ id: string;
71
+ /** The heading this rule gets in the section. */
72
+ heading: string;
73
+ /** The rule, as both places state it. Markdown paragraphs, blank-line separated. */
74
+ body: string;
75
+ }
76
+
77
+ /**
78
+ * Every shared rule, in reading order: what to read first, what the content
79
+ * is, what a write may do, then the two protocols that guard a destructive
80
+ * call.
81
+ *
82
+ * The bodies are the sentences the descriptions used to carry, moved rather
83
+ * than rewritten wherever the wording still reads outside its old tool — the
84
+ * ones that said "this tool" or "this call" name the tools instead.
85
+ */
86
+ export function sharedFileRules(layout: KbLayout): readonly SharedFileRule[] {
87
+ const agentsFile = agentsFileOf(layout);
88
+ return [
89
+ {
90
+ id: 'agent-guide',
91
+ heading: "This knowledge base's own conventions",
92
+ body: conventionsRule(agentsFile),
93
+ },
94
+ {
95
+ id: 'content-kinds',
96
+ heading: 'Text, documents, images and other binaries',
97
+ body:
98
+ 'read_file returns text for text files and extracted text for documents ' +
99
+ '(.docx/.pptx/.xlsx/.odt/.odp/.ods/.pdf, .eml/.msg); write_file, write_files and edit_file accept TEXT only — ' +
100
+ 'they refuse documents, images, archives and other binary files (legacy .doc/.ppt/.xls included) with kind ' +
101
+ "`binary_not_writable`, naming the file's kind and the tool to use instead; copy_file, move_file and delete_file " +
102
+ 'act on bytes of any kind and unzip extracts the entries of a `.zip`; new binary content arrives by upload ' +
103
+ '(see below). file_stat reports `contentMode` ' +
104
+ '(`text` | `document` | `binary`) so you can decide before acting.\n\n' +
105
+ 'Office and OpenDocument files (.docx/.pptx/.xlsx, .odt/.odp/.ods) and PDFs read as EXTRACTED text under an ' +
106
+ 'honest `[extracted text of …]` header, with `[slide N]`/`[sheet: Name]`/`[page N]` markers — the extraction ' +
107
+ 'is READ-ONLY (layout/images omitted; such files cannot be edited as text, only replaced by uploading a new ' +
108
+ 'version). Email files (.eml/.msg) read the same way: a `[from]`/`[to]`/`[subject]`/`[date]` header block, the ' +
109
+ 'body (plain-text part preferred; an HTML-only body is stripped to text), and an `[attachments]` name list — ' +
110
+ 'attachments are listed, never extracted. grep searches inside all of those, through the same extractions.\n\n' +
111
+ 'Images (.png/.jpg/.jpeg/.gif/.webp) read as the IMAGE ITSELF, as native MCP image content with a one-line ' +
112
+ 'text note naming the file, so you can look at the picture — up to 3.5 MB of raw image data; a larger image ' +
113
+ 'gets an honest refusal asking for a locally downscaled copy or a smaller export (`.svg` is text and reads as ' +
114
+ 'text). Images come back only on a DIRECT read_file: inside `call_tool_chain` an image read yields an ' +
115
+ '`{ image_omitted, note }` stub instead, so read an image outside a chain when you mean to look at it. Any other ' +
116
+ 'binary file reads as a one-line description rather than raw bytes.',
117
+ },
118
+ {
119
+ id: 'write-mode',
120
+ heading: 'What a write may do at a path',
121
+ body:
122
+ 'On write_file and write_files, `mode` decides what may happen at a path and DEFAULTS TO `create`: `create` ' +
123
+ 'writes a new file and refuses a path that already exists (`exists`, with the path — pass `mode: overwrite` to ' +
124
+ 'replace it), `overwrite` replaces what is there (creating it if there is nothing), `update` replaces an ' +
125
+ 'existing file and refuses a path that does not exist (`missing`). A refused path is left exactly as it was.',
126
+ },
127
+ {
128
+ id: 'images-in-pages',
129
+ heading: 'Where the images a page uses go',
130
+ body:
131
+ 'Keep them in an `assets/` folder next to the page that uses them and link them with a relative path, e.g. ' +
132
+ '`![Approval screen](./assets/approval-screen.png)`; the page renders them inline.',
133
+ },
134
+ {
135
+ id: 'escape-sequences',
136
+ heading: 'Escape sequences in content you send',
137
+ body:
138
+ 'On write_file, write_files and edit_file, which take content as a JSON string: some clients decode escape ' +
139
+ 'sequences in arguments before sending, so content meant to CONTAIN an escape rather than what it stands for ' +
140
+ '(the six characters backslash, `u`, `0`, `0`, `4`, `1`, say, rather than the letter `A`) can reach the tool ' +
141
+ 'already decoded — what arrives is stored byte for byte, so when that distinction matters, verify what landed ' +
142
+ '(`read_file`, or a hash) and send such content by upload (see below), which lands it unchanged.',
143
+ },
144
+ {
145
+ // The reason the upload route exists, said once. Each of the three tools
146
+ // that take content as a JSON string names the route in one sentence of
147
+ // its own; WHY a file should go that way, and how, is here.
148
+ id: 'upload-route',
149
+ heading: 'Large, escape-heavy and binary content goes by upload',
150
+ body:
151
+ 'write_file, write_files and edit_file take content as a JSON string you have to type out in full, so a long ' +
152
+ 'file is cut off mid-answer, a file full of backslashes or `\\u` escapes fails to parse as a parameter, and ' +
153
+ 'an image, a PDF or a zip cannot be sent at all. For any of those: call `request_file_upload`, POST the file — ' +
154
+ 'or one zip holding many files — to the address it answers with any HTTP client ' +
155
+ '(`curl -X POST --data-binary @<file> "<uploadUrl>?filename=<name>"`), then `apply_file_upload` to land it on ' +
156
+ 'a branch in one commit. The bytes never pass through the conversation, so nothing is cut or mangled on the ' +
157
+ 'way. A person can also use Upload in the app.',
158
+ },
159
+ {
160
+ id: 'dry-run-confirm',
161
+ heading: 'Dry-run before a move, a copy or a folder delete',
162
+ body:
163
+ 'move_file, copy_file and delete_folder take `dryRun: true`: it changes nothing and answers the impact — ' +
164
+ 'what the call would touch, `allowed`, and `reason` when it may not run. A non-empty folder is deleted, and a ' +
165
+ 'move that changes your access runs, only with `confirm: true`; without it the call changes nothing and ' +
166
+ 'returns the same impact with `confirmationRequired: true`. Do NOT set `confirm: true` on your first call — ' +
167
+ 'dry-run, check the impact, then confirm. delete_file takes neither: it removes the one file you named, so ' +
168
+ 'check it first with file_stat (`deletable`) if you are unsure.',
169
+ },
170
+ {
171
+ id: 'managed-items',
172
+ heading: 'What these tools never move or delete',
173
+ body:
174
+ `A platform file (${platformFileList(layout)}) is refused with ` +
175
+ '"<name> is a platform file and stays in its folder." — a folder that moves ' +
176
+ 'takes its own platform files along, still in their folder, and a folder that is deleted takes them with it in ' +
177
+ 'the same one change, so its files are never left ungoverned part-way. A platform folder (the repository root ' +
178
+ `or a reserved root folder such as \`${layout.knowledgeBaseDir}/\`) and git metadata are refused, and so is creating a ` +
179
+ 'platform file or folder at a destination (renaming a note to `access.md` is refused). A path that is, or goes ' +
180
+ 'through, a symbolic link is refused: links are never followed or removed. On a protected branch you must be ' +
181
+ 'able to write everything the call touches — for a folder, every file under it, at its old and its new path. ' +
182
+ 'file_stat reports `managed`, `movable` and `deletable` so you can tell before the call.',
183
+ },
184
+ {
185
+ id: 'refused-for-permissions',
186
+ heading: 'When a write is refused for permissions',
187
+ body:
188
+ 'A refusal is not necessarily the end of the road: the `write-denied` error says whether you may propose the ' +
189
+ 'change instead (create a branch from this one, repeat the call on it, then `open_change_request` into this ' +
190
+ 'branch) and lists those steps.',
191
+ },
192
+ {
193
+ // What a chain DOES, as opposed to how to write one — which stays in
194
+ // `call_tool_chain`'s own description. The sentences are `mcp-core`'s,
195
+ // the same ones a surface with no shared rules puts in the description
196
+ // itself, so the rule reads the same wherever an agent finds it. What a
197
+ // chained read does to an image is in the content rule above.
198
+ id: 'tool-chain',
199
+ heading: 'When several calls run as one chain',
200
+ body: `On call_tool_chain: ${CHAIN_FAILURES_RULE}\n\n${CHAIN_LARGE_RESULTS_RULE}`,
201
+ },
202
+ ];
203
+ }
204
+
205
+ /**
206
+ * The platform files as the rules list them — from the one function that knows
207
+ * which they are, so the list cannot drift from what actually refuses a move,
208
+ * and the guide appears under this deployment's name for it.
209
+ *
210
+ * WITH THE DEPTH each name counts at, because the name alone is half the rule:
211
+ * `access.md` governs the folder it sits in and `.bevelignore` layers, so both
212
+ * are platform files wherever they are; `roles.yaml` and the guide are read
213
+ * from the repository root only, so a nested copy of either is ordinary
214
+ * content that moves and deletes like any page. An agent told only the names
215
+ * refuses a rename it may make, and trusts a nested `access.md` it may not.
216
+ */
217
+ function platformFileList(layout: KbLayout): string {
218
+ const { anyDepth, rootOnly } = platformFilesByDepth(layout);
219
+ const quoted = (names: readonly string[]): string => names.map((name) => `\`${name}\``).join(' or ');
220
+ return `${quoted(anyDepth)} in any folder, ${quoted(rootOnly)} at the repository root`;
221
+ }
222
+
223
+ /**
224
+ * The conventions reminder — which file holds the author's own rules for this
225
+ * knowledge base, and to read it first.
226
+ *
227
+ * `CLAUDE.md` is named as a fallback because knowledge bases seeded before the
228
+ * rename still carry one. WHEN THE GUIDE HAS BEEN RENAMED the sentence names
229
+ * two files, ours first: the second is the organisation's OWN `AGENTS.md`,
230
+ * which on such a deployment is ordinary content the platform never touches —
231
+ * and which no harness reads for a remote agent, because a remote agent has no
232
+ * checkout. Under the default name the wording collapses to the one file it
233
+ * has always named.
234
+ */
235
+ function conventionsRule(agentsFile: string): string {
236
+ if (agentsFile === LEGACY_AGENTS_FILE) {
237
+ return (
238
+ `Before your first read or change in a workspace, read \`${LEGACY_AGENTS_FILE}\` at the KB root — or ` +
239
+ `\`${PRE_RENAME_AGENTS_FILE}\` on a knowledge base seeded before it was renamed — if either exists: it holds ` +
240
+ "the author's conventions for this knowledge base, and you should follow them."
241
+ );
242
+ }
243
+ return (
244
+ `Before your first read or change in a workspace, read \`${agentsFile}\` at the KB root, then ` +
245
+ `\`${LEGACY_AGENTS_FILE}\` if it also exists (the organisation's own conventions) — or ` +
246
+ `\`${PRE_RENAME_AGENTS_FILE}\` on a knowledge base seeded before it was renamed: together they hold the ` +
247
+ 'conventions for this knowledge base, and you should follow them.'
248
+ );
249
+ }
250
+
251
+ /**
252
+ * The shared rules as ONE markdown section — the string both channels carry,
253
+ * byte for byte. A `##` section so it drops into the managed guide at that
254
+ * level and still reads as a block in the instructions blob.
255
+ */
256
+ export function sharedFileRulesSection(layout: KbLayout): string {
257
+ const body = sharedFileRules(layout)
258
+ .map((rule) => `### ${rule.heading}\n\n${rule.body}`)
259
+ .join('\n\n');
260
+ return `## ${SHARED_RULES_SECTION}\n\n${body}`;
261
+ }
262
+
263
+ /**
264
+ * The longest guide file name the pointer sentence spells out. Beyond this it
265
+ * names the guide by its ROLE instead (see {@link sharedRulesPointer}).
266
+ *
267
+ * There has to be a bound somewhere, because the pointer rides on every file
268
+ * tool and a file name is not a fixed cost: `validateFilename` allows a name
269
+ * of up to 255 bytes, so an unbounded pointer could reach 318 characters and
270
+ * push `file_stat` to 1,428 — over the description cap, recreating on a
271
+ * renamed deployment exactly the truncation this module exists to prevent, and
272
+ * invisibly, because every measurement is taken under the default layout.
273
+ *
274
+ * 40 is well past any name a deployment plausibly picks
275
+ * (`ENGINEERING-AGENT-CONVENTIONS.md` is 32) and the fallback below is only
276
+ * reachable past it.
277
+ */
278
+ export const POINTER_GUIDE_NAME_BUDGET = 40;
279
+
280
+ /**
281
+ * The one sentence a tool description ends with, in place of the paragraphs it
282
+ * used to carry. Short on purpose: it costs every description the same ~100
283
+ * characters at worst, and its whole job is to name the section and the file
284
+ * to read.
285
+ *
286
+ * BOUNDED BY CONSTRUCTION, which is what lets the description cap mean
287
+ * something on a deployment that renamed its guide: a name within
288
+ * {@link POINTER_GUIDE_NAME_BUDGET} is spelled out, and a longer one gets the
289
+ * generic wording. Naming the file is the better sentence and wins whenever it
290
+ * fits; a name past the budget is pathological, and there the choice is between
291
+ * a sentence that says where to look and a catalog entry the client cuts. The
292
+ * guide's own name is still in the section's first rule either way.
293
+ *
294
+ * An absent layout means the default one, as it does in
295
+ * `composeAgentInstructions`: a caller reading the layout from configuration
296
+ * gets `undefined` when none is set, and the pointer must still name a file.
297
+ */
298
+ export function sharedRulesPointer(layout: KbLayout = DEFAULT_KB_LAYOUT): string {
299
+ const agentsFile = agentsFileOf(layout);
300
+ const where =
301
+ agentsFile.length <= POINTER_GUIDE_NAME_BUDGET ? agentsFile : 'the agent guide at the KB root';
302
+ return ` Shared rules for all file tools: see "${SHARED_RULES_SECTION}" in ${where}.`;
303
+ }
304
+
305
+ /**
306
+ * The most the pointer can ever cost a description, over every layout. What the
307
+ * description cap is measured against, the way the tool prefix is measured at
308
+ * ITS cap rather than at whatever the current admin wrote: a description that
309
+ * only fits beside the short default guide name does not really fit.
310
+ */
311
+ export const SHARED_RULES_POINTER_MAX = Math.max(
312
+ sharedRulesPointer({ ...DEFAULT_KB_LAYOUT, agentsFile: `${'x'.repeat(POINTER_GUIDE_NAME_BUDGET - 3)}.md` }).length,
313
+ sharedRulesPointer({ ...DEFAULT_KB_LAYOUT, agentsFile: `${'x'.repeat(POINTER_GUIDE_NAME_BUDGET + 10)}.md` }).length,
314
+ );
@@ -1,4 +1,4 @@
1
- import { and, asc, count, desc, eq, isNull, lt, or, type SQL } from 'drizzle-orm';
1
+ import { and, count, desc, eq, isNull, lt, or, type SQL } from 'drizzle-orm';
2
2
  import { logger } from '../../shared/logging.js';
3
3
  import type { Database } from '../database/connection.js';
4
4
  import { agentConnections, agentEvents, oauthTokens, users } from '../database/schema.js';
@@ -101,7 +101,9 @@ export class AgentAuditService implements IAgentAuditService, IAgentEventRecorde
101
101
  .from(agentConnections)
102
102
  .innerJoin(users, eq(agentConnections.userId, users.id))
103
103
  .where(mine === null ? undefined : eq(agentConnections.userId, mine))
104
- .orderBy(asc(users.email), desc(agentConnections.connectedAt)),
104
+ // Not by email: that column is randomized ciphertext, and ORDER BY
105
+ // on it is noise. The sort below orders by email in-process.
106
+ .orderBy(desc(agentConnections.connectedAt)),
105
107
  this.db
106
108
  .select({ keyId: agentEvents.keyId, connectionId: agentEvents.connectionId, n: count() })
107
109
  .from(agentEvents)
@@ -0,0 +1,253 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { Database } from '../../database/connection.js';
3
+ import type { CoreConfig } from '../../../core-config.js';
4
+ import { AuthService } from '../auth.service.js';
5
+ import {
6
+ AccountAdmissionRefusedError,
7
+ AccountChangeRefusedError,
8
+ AccountDeactivatedError,
9
+ AuthBackendError,
10
+ type AccountProvisionReason,
11
+ type IAccountAdmission,
12
+ } from '../account-admission.js';
13
+ import { hashPassword } from '../password-hash.js';
14
+ import { RECOVERY_BOT_EMAIL } from '../../workflow/recovery-bot.js';
15
+
16
+ /**
17
+ * Drizzle chain stub, as in `auth.service.test.ts`: each db.select() /
18
+ * insert() / update() consumes the next queued result; awaiting any point of
19
+ * the chain resolves it. Records what was inserted and set.
20
+ */
21
+ function makeFakeDb(queue: unknown[]) {
22
+ const captured: { values: Record<string, unknown>[]; set: Record<string, unknown>[] } = { values: [], set: [] };
23
+ function nextChain(): unknown {
24
+ const result = queue.shift();
25
+ const chain: Record<string, unknown> = {};
26
+ const passthrough = (recorder?: (arg: Record<string, unknown>) => void) =>
27
+ vi.fn((arg?: unknown) => {
28
+ recorder?.(arg as Record<string, unknown>);
29
+ return chain;
30
+ });
31
+ Object.assign(chain, {
32
+ values: passthrough((a) => captured.values.push(a)),
33
+ set: passthrough((a) => captured.set.push(a)),
34
+ where: passthrough(),
35
+ limit: passthrough(),
36
+ from: passthrough(),
37
+ orderBy: passthrough(),
38
+ returning: passthrough(),
39
+ onConflictDoUpdate: passthrough(),
40
+ onConflictDoNothing: passthrough(),
41
+ then: (ok: (v: unknown) => unknown, ko: (e: unknown) => unknown) => Promise.resolve(result).then(ok, ko),
42
+ });
43
+ return chain;
44
+ }
45
+ const db = {
46
+ insert: vi.fn(() => nextChain()),
47
+ select: vi.fn(() => nextChain()),
48
+ update: vi.fn(() => nextChain()),
49
+ } as unknown as Database;
50
+ return { db, captured };
51
+ }
52
+
53
+ function makeConfig(over: Partial<CoreConfig> = {}): CoreConfig {
54
+ return { jwtSecret: 'test-jwt-secret', adminEmail: '', adminPassword: '', allowedEmailDomains: [], ...over } as CoreConfig;
55
+ }
56
+
57
+ const ROW = { id: 'user-1', email: 'alice@example.com', name: 'Alice', avatarUrl: null, onboardingDone: false, deactivatedAt: null };
58
+ const OFF = { ...ROW, deactivatedAt: new Date('2026-09-01') };
59
+
60
+ function recordingPort(verdict: Awaited<ReturnType<IAccountAdmission['canProvision']>>) {
61
+ const asked: Array<[string, AccountProvisionReason]> = [];
62
+ const port: IAccountAdmission = {
63
+ canProvision: async (email, reason) => {
64
+ asked.push([email, reason]);
65
+ return verdict;
66
+ },
67
+ };
68
+ return { port, asked };
69
+ }
70
+
71
+ describe('signing in with a switched-off account', () => {
72
+ it('refuses a password sign-in once the password is proven, and says why', async () => {
73
+ const passwordHash = await hashPassword('a-long-enough-password');
74
+ const { db } = makeFakeDb([[{ ...OFF, passwordHash }]]);
75
+ const svc = new AuthService(db, makeConfig());
76
+ await expect(svc.loginWithPassword('alice@example.com', 'a-long-enough-password')).rejects.toBeInstanceOf(
77
+ AccountDeactivatedError,
78
+ );
79
+ });
80
+
81
+ it('gives a wrong password the same answer as for any account, so it reveals nothing', async () => {
82
+ const passwordHash = await hashPassword('a-long-enough-password');
83
+ const { db } = makeFakeDb([[{ ...OFF, passwordHash }]]);
84
+ const svc = new AuthService(db, makeConfig());
85
+ await expect(svc.loginWithPassword('alice@example.com', 'the-wrong-password')).rejects.toThrow('Invalid credentials');
86
+ });
87
+
88
+ it('refuses a single sign-on sign-in, without issuing a session', async () => {
89
+ const { db } = makeFakeDb([[OFF]]);
90
+ const svc = new AuthService(db, makeConfig());
91
+ await expect(svc.loginWithSso('alice@example.com', 'Alice')).rejects.toBeInstanceOf(AccountDeactivatedError);
92
+ });
93
+
94
+ it('refuses an embed identity', async () => {
95
+ const { db } = makeFakeDb([[OFF]]);
96
+ const svc = new AuthService(db, makeConfig());
97
+ await expect(svc.getOrCreateByEmail('alice@example.com')).rejects.toBeInstanceOf(AccountDeactivatedError);
98
+ });
99
+ });
100
+
101
+ describe('a session that outlives a deactivation', () => {
102
+ it('stops being accepted once the account is switched off', async () => {
103
+ const { db } = makeFakeDb([[ROW], [{ deactivatedAt: new Date() }]]);
104
+ const svc = new AuthService(db, makeConfig());
105
+ const { token } = await svc.loginWithSso('alice@example.com', 'Alice');
106
+ // The signature alone is still good…
107
+ expect(svc.verifyToken(token).userId).toBe(ROW.id);
108
+ // …but the session is not.
109
+ await expect(svc.resolveSession(token)).rejects.toBeInstanceOf(AccountDeactivatedError);
110
+ });
111
+
112
+ it('is told apart from a refusal when the account cannot be looked up', async () => {
113
+ const { db } = makeFakeDb([[ROW]]);
114
+ const svc = new AuthService(db, makeConfig());
115
+ const { token } = await svc.loginWithSso('alice@example.com', 'Alice');
116
+ vi.mocked(db.select).mockImplementationOnce(() => {
117
+ throw new Error('db down');
118
+ });
119
+ await expect(svc.resolveSession(token)).rejects.toBeInstanceOf(AuthBackendError);
120
+ });
121
+
122
+ it('is accepted while the account is on', async () => {
123
+ const { db } = makeFakeDb([[ROW], [{ deactivatedAt: null }]]);
124
+ const svc = new AuthService(db, makeConfig());
125
+ const { token } = await svc.loginWithSso('alice@example.com', 'Alice');
126
+ await expect(svc.resolveSession(token)).resolves.toEqual({ userId: ROW.id, email: ROW.email });
127
+ });
128
+ });
129
+
130
+ describe('AuthService.isActive', () => {
131
+ it('reads once and keeps the answer briefly', async () => {
132
+ const { db } = makeFakeDb([[{ deactivatedAt: null }]]);
133
+ const svc = new AuthService(db, makeConfig());
134
+ expect(await svc.isActive('user-1')).toBe(true);
135
+ expect(await svc.isActive('user-1')).toBe(true);
136
+ expect(vi.mocked(db.select)).toHaveBeenCalledTimes(1);
137
+ });
138
+
139
+ it('treats the deployment admin as on whatever its row says, so its rescue session works', async () => {
140
+ const config = makeConfig({ adminEmail: 'root@example.com', adminPassword: 'sup3r-secret' });
141
+ const { db } = makeFakeDb([[{ email: 'root@example.com', deactivatedAt: new Date() }]]);
142
+ expect(await new AuthService(db, config).isActive('root-id')).toBe(true);
143
+ });
144
+
145
+ it('treats an account that no longer exists as off', async () => {
146
+ const { db } = makeFakeDb([[]]);
147
+ expect(await new AuthService(db, makeConfig()).isActive('gone')).toBe(false);
148
+ });
149
+
150
+ it('forgets the cached answer when this process switches the account off', async () => {
151
+ const { db } = makeFakeDb([[{ deactivatedAt: null }], [ROW], [], [{ deactivatedAt: new Date() }]]);
152
+ const svc = new AuthService(db, makeConfig());
153
+ expect(await svc.isActive(ROW.id)).toBe(true);
154
+ expect(await svc.deactivate(ROW.id)).toBe(true);
155
+ expect(await svc.isActive(ROW.id)).toBe(false);
156
+ });
157
+ });
158
+
159
+ describe('AuthService.deactivate', () => {
160
+ it('records when the account was switched off', async () => {
161
+ const { db, captured } = makeFakeDb([[ROW], []]);
162
+ expect(await new AuthService(db, makeConfig()).deactivate(ROW.id)).toBe(true);
163
+ expect(captured.set[0].deactivatedAt).toBeInstanceOf(Date);
164
+ });
165
+
166
+ it('answers false for an account that does not exist', async () => {
167
+ const { db } = makeFakeDb([[]]);
168
+ expect(await new AuthService(db, makeConfig()).deactivate('nobody')).toBe(false);
169
+ });
170
+
171
+ it('refuses the deployment admin, whose environment password is the way back in', async () => {
172
+ const config = makeConfig({ adminEmail: 'root@example.com', adminPassword: 'sup3r-secret' });
173
+ const { db } = makeFakeDb([[{ ...ROW, email: 'root@example.com' }]]);
174
+ await expect(new AuthService(db, config).deactivate(ROW.id)).rejects.toBeInstanceOf(AccountChangeRefusedError);
175
+ expect(vi.mocked(db.update)).not.toHaveBeenCalled();
176
+ });
177
+
178
+ it('refuses the accounts the platform runs its own work as', async () => {
179
+ const { db } = makeFakeDb([[{ ...ROW, email: RECOVERY_BOT_EMAIL }]]);
180
+ await expect(new AuthService(db, makeConfig()).deactivate(ROW.id)).rejects.toThrow('platform itself');
181
+ expect(vi.mocked(db.update)).not.toHaveBeenCalled();
182
+ });
183
+ });
184
+
185
+ describe('AuthService.reactivate', () => {
186
+ it('asks the admission port, as for a new account, and switches it on when admitted', async () => {
187
+ const { port, asked } = recordingPort({ ok: true });
188
+ const { db, captured } = makeFakeDb([[OFF], []]);
189
+ expect(await new AuthService(db, makeConfig(), port).reactivate(ROW.id)).toBe(true);
190
+ expect(asked).toEqual([['alice@example.com', 'reactivate']]);
191
+ expect(captured.set[0].deactivatedAt).toBeNull();
192
+ });
193
+
194
+ it("refuses with the port's words when there is no room, and changes nothing", async () => {
195
+ const { port } = recordingPort({ ok: false, message: 'All 3 seats are taken' });
196
+ const { db } = makeFakeDb([[OFF]]);
197
+ const svc = new AuthService(db, makeConfig(), port);
198
+ await expect(svc.reactivate(ROW.id)).rejects.toThrow('All 3 seats are taken');
199
+ expect(vi.mocked(db.update)).not.toHaveBeenCalled();
200
+ });
201
+
202
+ it('does not ask about an account that is already on', async () => {
203
+ const { port, asked } = recordingPort({ ok: false, message: 'full' });
204
+ const { db } = makeFakeDb([[ROW]]);
205
+ expect(await new AuthService(db, makeConfig(), port).reactivate(ROW.id)).toBe(true);
206
+ expect(asked).toEqual([]);
207
+ });
208
+ });
209
+
210
+ describe('a first sign-in the port would rather keep waiting', () => {
211
+ const waiting = { ok: false as const, message: 'No seat left; the admin can let you in', waitForAdmin: true };
212
+
213
+ it('puts the person on file switched off, and still refuses the sign-in', async () => {
214
+ const { port } = recordingPort(waiting);
215
+ const { db, captured } = makeFakeDb([[], []]);
216
+ const svc = new AuthService(db, makeConfig(), port);
217
+ const err = await svc.loginWithSso('new@example.com', 'New').catch((e: unknown) => e);
218
+ expect(err).toBeInstanceOf(AccountAdmissionRefusedError);
219
+ expect((err as AccountAdmissionRefusedError).waitingForAdmin).toBe(true);
220
+ expect((err as Error).message).toBe(waiting.message);
221
+ expect(captured.values).toHaveLength(1);
222
+ expect(captured.values[0]).toMatchObject({ email: 'new@example.com' });
223
+ expect(captured.values[0].deactivatedAt).toBeInstanceOf(Date);
224
+ });
225
+
226
+ it('is a plain refusal when an admin creates the account', async () => {
227
+ const { port } = recordingPort(waiting);
228
+ const { db } = makeFakeDb([[]]);
229
+ const err = await new AuthService(db, makeConfig(), port)
230
+ .createAccount('new@example.com', 'New')
231
+ .catch((e: unknown) => e);
232
+ expect(err).toBeInstanceOf(AccountAdmissionRefusedError);
233
+ expect((err as AccountAdmissionRefusedError).waitingForAdmin).toBe(false);
234
+ expect(vi.mocked(db.insert)).not.toHaveBeenCalled();
235
+ });
236
+ });
237
+
238
+ describe('AuthService.createAccount without a password', () => {
239
+ it('makes an account for single sign-on, with no hash stored', async () => {
240
+ const { db, captured } = makeFakeDb([[{ ...ROW, passwordHash: null }]]);
241
+ const user = await new AuthService(db, makeConfig()).createAccount('Alice@Example.com', '');
242
+ expect(user.email).toBe('alice@example.com');
243
+ // The index column is written with the address; the handle indexes it.
244
+ expect(captured.values[0]).toEqual({ email: 'alice@example.com', emailBidx: 'alice@example.com', name: 'alice' });
245
+ });
246
+
247
+ it('asks the admission port, as any new account does', async () => {
248
+ const { port, asked } = recordingPort({ ok: true });
249
+ const { db } = makeFakeDb([[], [{ ...ROW, passwordHash: null }]]);
250
+ await new AuthService(db, makeConfig(), port).createAccount('alice@example.com', 'Alice');
251
+ expect(asked).toEqual([['alice@example.com', 'admin-create']]);
252
+ });
253
+ });
@@ -70,6 +70,9 @@ function harness(opts: { participants?: IErasureParticipant[] } = {}) {
70
70
  locked.push((statement.queryChunks ?? []).filter((c) => typeof c === 'number'));
71
71
  return { rows: [] };
72
72
  },
73
+ // The one read the transaction makes: the refusals recorded before the
74
+ // encryption release, which it matches by name. None here.
75
+ select: () => ({ from: () => ({ where: async () => [] }) }),
73
76
  delete: (table: unknown) => ({
74
77
  where: () => {
75
78
  order.push(`delete:${tableOf(table)}`);
@@ -176,8 +179,9 @@ describe('AccountErasureService: approvals are rewritten under the approval lock
176
179
  const committed = h.order.indexOf('delete:users');
177
180
  const sweep = h.order.indexOf('after-commit:pr_file_approvals');
178
181
  expect(sweep).toBeGreaterThan(committed);
179
- // The change requests get the same treatment, and did before this.
180
- expect(h.order).toContain('after-commit:change_requests');
182
+ // The change requests get the same treatment, twice: once for the
183
+ // authorship, once for the refusal note of an apply that was in flight.
184
+ expect(h.order.filter((step) => step === 'after-commit:change_requests')).toHaveLength(2);
181
185
  // Both guarded on `users`: the marker is only recorded for a clause that
182
186
  // names it, so a sweep that dropped the `notExists` lands here instead.
183
187
  expect(h.order.filter((step) => step.startsWith('unguarded:'))).toEqual([]);