@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,378 @@
1
+ import express from 'express';
2
+ import { describe, expect, it } from 'vitest';
3
+ import {
4
+ CALL_TOOL_CHAIN_NAME,
5
+ CHAIN_FAILURES_RULE,
6
+ CHAIN_LARGE_RESULTS_RULE,
7
+ codeModeMetaTools,
8
+ } from '@bevel-software/platform-mcp-core';
9
+ import { testKbContext } from '../../../__tests__/kb-context.js';
10
+ import { EXTERNAL_KB_MANUAL_NAME } from '../../tool-manuals/tool-manuals.contract.js';
11
+ import { ToolRegistry } from '../tool-registry.js';
12
+ import type { UtcpTool } from '../tool.contract.js';
13
+ import { createToolHandlerFactory } from '../../tool-helpers/tool-handler.js';
14
+ import type { ToolAuth } from '../../tool-auth/tool-auth.middleware.js';
15
+ import { registerWorkspaceTools } from '../../workspace/workspace.tools.js';
16
+ import { registerWorkflowTools } from '../../workflow/agent-tools/workflow.tools.js';
17
+ import { registerPluginsTools } from '../../plugins/plugins.tools.js';
18
+ import { registerSkillsTools } from '../../skills/skills.tools.js';
19
+ import { registerToolManualsTools } from '../../tool-manuals/tool-manuals.tools.js';
20
+ import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
21
+ import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
22
+ import { UuidSessionSink } from '../../workspace/session-sink.js';
23
+ import { RoutineWritePolicyService } from '../../workspace/routine-write-policy.js';
24
+ import { CLIENT_SHORT_CUT, TOOL_DESCRIPTION_CAP, clientVisibleLength, firstSentenceEnd } from '../description-length.js';
25
+ import {
26
+ POINTER_GUIDE_NAME_BUDGET,
27
+ SHARED_RULES_POINTER_MAX,
28
+ TOOL_PREFIX_CAP,
29
+ sharedFileRules,
30
+ sharedRulesPointer,
31
+ } from '../../agent-instructions/index.js';
32
+ import { isPlatformFile, platformFilesByDepth } from '@bevel-software/platform-shared';
33
+
34
+ /**
35
+ * The cap exists because clients cut a long tool description, and they cut it
36
+ * from the END — where the text specific to the tool sits. Agents reported
37
+ * `file_stat`, `read_file`, `write_file` and `write_files` arriving as
38
+ * "[truncated]". So this measures what a client is actually handed, and fails
39
+ * NAMING the tool: the next paragraph someone appends to a description has to
40
+ * answer to this test rather than to an agent's truncated catalog.
41
+ *
42
+ * Every registrar Hexis owns is mounted here, on stand-ins, because the
43
+ * descriptions are the only thing under test: a stand-in that is never called
44
+ * is honest about that. Tools PROXIED from connected MCP servers are not
45
+ * measured — their text is the other server's.
46
+ */
47
+
48
+ /** Dependencies the registrars take but never touch while they are only building defs. */
49
+ const unused = <T,>(): T => ({}) as T;
50
+
51
+ /** A `.tool` catalog with nothing in it: the shortest honest answer for a listing. */
52
+ const emptyManuals = {
53
+ listLocalOnly: async () => [],
54
+ listAll: async () => [],
55
+ list: async () => [],
56
+ listInvalid: async () => [],
57
+ } as unknown as Parameters<typeof registerToolManualsTools>[4];
58
+
59
+ /** A skill catalog with nothing in it — the per-user skill line is then one fixed sentence. */
60
+ const emptySkills = { listSkills: async () => [] } as unknown as Parameters<typeof registerSkillsTools>[4];
61
+
62
+ /**
63
+ * The meta-tools as the hosted endpoint builds them (`mcp.service.ts`):
64
+ * examples written against the namespace it registers the knowledge-base tools
65
+ * under and against the catalog it serves, and the chain ending in the pointer.
66
+ * So the chain description measured here carries the worked call a client is
67
+ * really sent, which is its longest form.
68
+ */
69
+ function servedMetaTools(external: readonly UtcpTool[], pointer = sharedRulesPointer(testKbContext().layout)) {
70
+ return codeModeMetaTools(
71
+ EXTERNAL_KB_MANUAL_NAME,
72
+ external.map((t) => ({ utcpName: `${EXTERNAL_KB_MANUAL_NAME}.${t.name}`, inputSchema: t.inputs })),
73
+ { sharedRulesPointer: pointer },
74
+ );
75
+ }
76
+
77
+ /** Every tool Hexis itself registers, on both surfaces, deduplicated by name. */
78
+ async function hexisTools(): Promise<UtcpTool[]> {
79
+ const registry = new ToolRegistry();
80
+ const router = express.Router();
81
+ const toolAuth = ((_req, _res, next) => next()) as unknown as ToolAuth;
82
+ const toolHandler = createToolHandlerFactory(unused());
83
+ const kb = testKbContext();
84
+ const gate = { recoveryBotEmail: 'bot@x', hooks: new WorkflowHooks(), notes: new ToolDescriptionNotes() };
85
+
86
+ registerWorkspaceTools(
87
+ registry,
88
+ router,
89
+ toolAuth,
90
+ toolHandler,
91
+ unused(),
92
+ unused(),
93
+ unused(),
94
+ kb,
95
+ gate,
96
+ new RoutineWritePolicyService(),
97
+ new UuidSessionSink(),
98
+ undefined,
99
+ undefined,
100
+ // The two upload tools are mounted only when a store is supplied, and
101
+ // every real composition supplies one: without it they would be the two
102
+ // descriptions this suite never measured.
103
+ unused(),
104
+ );
105
+ registerWorkflowTools(registry, router, toolAuth, toolHandler, kb);
106
+ registerPluginsTools(registry);
107
+ registerSkillsTools(registry, router, toolAuth, toolHandler, emptySkills);
108
+ registerToolManualsTools(registry, router, toolAuth, toolHandler, emptyManuals, {
109
+ accessControl: unused(),
110
+ variableStatus: unused(),
111
+ kb,
112
+ });
113
+
114
+ const byName = new Map<string, UtcpTool>();
115
+ // The meta-tools as a client is served them: the chain carries its pointer
116
+ // (`mcp.service.ts` appends it at the mount), so the catalog measured here is
117
+ // the catalog that goes out rather than the unpointed constant.
118
+ const external = (await registry.listExternal()) as UtcpTool[];
119
+ const metaTools = servedMetaTools(external);
120
+ for (const tool of [...(await registry.listInternal()), ...external, ...metaTools]) {
121
+ byName.set(tool.name, tool as UtcpTool);
122
+ }
123
+ return [...byName.values()];
124
+ }
125
+
126
+ describe('no Hexis tool description is long enough to be cut', () => {
127
+ it(`keeps every description a client is handed within ${TOOL_DESCRIPTION_CAP} characters`, async () => {
128
+ const over = (await hexisTools())
129
+ .map((t) => ({ tool: t.name, chars: clientVisibleLength(t) }))
130
+ .filter((m) => m.chars > TOOL_DESCRIPTION_CAP)
131
+ .sort((a, b) => b.chars - a.chars);
132
+ // The message names the tool and its length, because "a description is too
133
+ // long" sends the next reader back to measuring them by hand.
134
+ expect(
135
+ over,
136
+ over.map((m) => `${m.tool}: ${m.chars} characters (cap ${TOOL_DESCRIPTION_CAP})`).join('\n'),
137
+ ).toEqual([]);
138
+ });
139
+
140
+ it(`leaves the tool's own first sentence inside the shorter ${CLIENT_SHORT_CUT}-character cut`, async () => {
141
+ // The cap does not answer the ~500-character cut; the ORDER of the text
142
+ // does, and this is where that claim is checked rather than asserted in a
143
+ // comment. A client that stops at 500 must still have the sentence saying
144
+ // what the tool does — what it loses is the pointer tail, and the file the
145
+ // pointer names is stated in the handshake instructions and in the guide
146
+ // anyway. Lowering the cap to 500 would not buy this; only order does.
147
+ const late = (await hexisTools())
148
+ .map((t) => ({ tool: t.name, endsAt: firstSentenceEnd(t) }))
149
+ .filter((m) => m.endsAt > CLIENT_SHORT_CUT)
150
+ .sort((a, b) => b.endsAt - a.endsAt);
151
+ expect(
152
+ late,
153
+ late
154
+ .map((m) => `${m.tool}: first sentence ends at ${m.endsAt} (cut ${CLIENT_SHORT_CUT}) — lead with what it does`)
155
+ .join('\n'),
156
+ ).toEqual([]);
157
+ });
158
+
159
+ it('holds the cap on a deployment that renamed its guide, not only under the default', async () => {
160
+ // The pointer ends every file tool's description and its length moves with
161
+ // a deployment setting, so a cap checked only against the nine characters
162
+ // of `AGENTS.md` guarantees nothing about the catalog a renamed deployment
163
+ // serves. Two things make it hold: the pointer is bounded by construction
164
+ // (past `POINTER_GUIDE_NAME_BUDGET` the guide is named by its role), and
165
+ // `clientVisibleLength` charges the worst case rather than this layout's.
166
+ const tools = await hexisTools();
167
+ const atWorst = tools
168
+ .map((t) => ({ tool: t.name, chars: clientVisibleLength(t) }))
169
+ .filter((m) => m.chars > TOOL_DESCRIPTION_CAP);
170
+ expect(atWorst, atWorst.map((m) => `${m.tool}: ${m.chars}`).join('\n')).toEqual([]);
171
+
172
+ // And measured literally, under the longest guide name a deployment can
173
+ // actually configure: every description still fits.
174
+ const longest = `${'x'.repeat(252)}.md`;
175
+ const pointerHere = sharedRulesPointer(testKbContext().layout);
176
+ const pointerThere = sharedRulesPointer({ ...testKbContext().layout, agentsFile: longest });
177
+ expect(pointerThere.length).toBeLessThanOrEqual(SHARED_RULES_POINTER_MAX);
178
+ for (const tool of tools) {
179
+ if (!tool.description?.endsWith(pointerHere)) continue;
180
+ const asRenamed = tool.description.slice(0, -pointerHere.length) + pointerThere;
181
+ const chars = clientVisibleLength({ name: tool.name, description: asRenamed });
182
+ expect(chars, `${tool.name} on a renamed deployment`).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
183
+ }
184
+ });
185
+
186
+ it('measures a prefixed tool with the prefix a client sees, not without it', async () => {
187
+ // The four knowledge-base tools carry the deployment's tool prefix ahead of
188
+ // their description on the MCP surface — up to `TOOL_PREFIX_CAP` characters
189
+ // the admin writes. A cap applied to the bare description would pass while
190
+ // the catalog the agent reads was over it by 300.
191
+ const read = (await hexisTools()).find((t) => t.name === 'read_file');
192
+ expect(read).toBeDefined();
193
+ // Its own text, with the pointer charged at its worst case rather than at
194
+ // this layout's, plus the prefix at ITS cap and the blank line between.
195
+ const pointer = sharedRulesPointer();
196
+ const ownAtWorstPointer = read!.description!.length - pointer.length + SHARED_RULES_POINTER_MAX;
197
+ expect(clientVisibleLength(read!)).toBe(ownAtWorstPointer + TOOL_PREFIX_CAP + 2);
198
+ expect(ownAtWorstPointer + TOOL_PREFIX_CAP + 2).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
199
+ });
200
+
201
+ it('fails, naming the tool, when a paragraph takes a description over the cap', () => {
202
+ const padded = { name: 'write_file', description: 'x'.repeat(TOOL_DESCRIPTION_CAP + 1) } as UtcpTool;
203
+ expect(clientVisibleLength(padded)).toBeGreaterThan(TOOL_DESCRIPTION_CAP);
204
+ // A tool with no description at all is not over the cap.
205
+ expect(clientVisibleLength({ name: 'nothing' } as UtcpTool)).toBe(0);
206
+ // Unless it is a PREFIXED one: the prefix is sent on its own then (no
207
+ // description, so no blank line either), and that text is what the client
208
+ // was handed. Measuring it as nothing would hide the only thing it got.
209
+ expect(clientVisibleLength({ name: 'read_file' } as UtcpTool)).toBe(TOOL_PREFIX_CAP);
210
+ });
211
+
212
+ it('measures the catalog, which is what a client lists — and says what is outside it', async () => {
213
+ // The cap is about `tools/list`: a client cuts what it was sent. The ONE
214
+ // long description this repository still writes is the in-process Mastra
215
+ // `call_tool_chain`, which no client lists — the in-process agent is handed
216
+ // it directly — and which OPENS with `@utcp/code-mode`'s own 2,500-character
217
+ // usage guide, text this repository does not own. It is left as it is, on
218
+ // purpose, and this assertion is what keeps that a stated fact rather than
219
+ // an oversight: if it ever reaches the catalog, the cap test above measures
220
+ // it like everything else.
221
+ const { createCallToolChainTool } = await import('../../code-mode/code-mode.tool.js');
222
+ const mastraTool = createCallToolChainTool(unused(), unused(), EXTERNAL_KB_MANUAL_NAME) as unknown as {
223
+ description: string;
224
+ };
225
+ expect(mastraTool.description).toContain('UTCP CodeMode Tool Usage Guide');
226
+ expect(mastraTool.description.length).toBeGreaterThan(TOOL_DESCRIPTION_CAP);
227
+ expect((await hexisTools()).some((t) => t.description === mastraTool.description)).toBe(false);
228
+ });
229
+ });
230
+
231
+ describe('every file tool ends with the pointer and carries no shared paragraph', () => {
232
+ /** The tools that used to carry the shared paragraphs — every `mount`ed one. */
233
+ const FILE_TOOLS = [
234
+ 'read_file',
235
+ 'list_files',
236
+ 'file_stat',
237
+ 'grep',
238
+ 'write_file',
239
+ 'write_files',
240
+ 'edit_file',
241
+ 'delete_file',
242
+ 'delete_folder',
243
+ 'mkdir',
244
+ 'move_file',
245
+ 'copy_file',
246
+ 'unzip',
247
+ 'execute_command',
248
+ 'apply_file_upload',
249
+ ];
250
+
251
+ it('ends each description with the one sentence naming the shared rules', async () => {
252
+ const tools = await hexisTools();
253
+ const pointer = sharedRulesPointer(testKbContext().layout);
254
+ for (const name of FILE_TOOLS) {
255
+ const def = tools.find((t) => t.name === name);
256
+ expect(def, name).toBeDefined();
257
+ expect(def!.description!.endsWith(pointer), `${name} must end with: ${pointer}`).toBe(true);
258
+ // Ends with a full sentence, so nothing reads as cut off mid-thought.
259
+ expect(def!.description!.trimEnd().endsWith('.'), name).toBe(true);
260
+ }
261
+ });
262
+
263
+ it('no longer repeats a shared paragraph inside a description', async () => {
264
+ const tools = await hexisTools();
265
+ // One recognisable fragment per rule that moved out. Searched across EVERY
266
+ // Hexis description, not only the file tools: a rule that came back by
267
+ // being pasted into a neighbouring tool is the same regression.
268
+ const moved = [
269
+ 'Content rule (the same on every file tool)',
270
+ 'they refuse documents, images, archives and other binary files',
271
+ '`mode` decides what may happen at a path and DEFAULTS TO',
272
+ 'Images: keep them in an `assets/` folder',
273
+ 'Escape sequences: some clients decode them in arguments',
274
+ 'If this is refused for permissions',
275
+ 'Before your first read or change in a workspace',
276
+ 'Do NOT set `confirm: true` on your first call',
277
+ ];
278
+ for (const fragment of moved) {
279
+ const carriers = tools.filter((t) => (t.description ?? '').includes(fragment)).map((t) => t.name);
280
+ expect(carriers, `"${fragment}" is a shared rule and belongs in the shared places only`).toEqual([]);
281
+ }
282
+ });
283
+ });
284
+
285
+ describe('the shared rules describe the tools they name', () => {
286
+ it('ends the chain description with the pointer too, under the cap', async () => {
287
+ // What a chain does with a failure, a large result or an image is true of
288
+ // every call, so it is stated in the shared rules — and the clients that
289
+ // drop the handshake `instructions` see only descriptions, so the chain
290
+ // gets the same pointer every file tool ends with. Composed at the mount,
291
+ // because `mcp-core` may not spell a guide name that is a deployment
292
+ // setting.
293
+ const pointer = sharedRulesPointer(testKbContext().layout);
294
+ const served = (await hexisTools()).find((t) => t.name === CALL_TOOL_CHAIN_NAME)!;
295
+ expect(served.description!.endsWith(pointer)).toBe(true);
296
+ expect(clientVisibleLength(served)).toBeLessThanOrEqual(TOOL_DESCRIPTION_CAP);
297
+ // The other two describe the registry, not what a call does: no pointer.
298
+ for (const tool of servedMetaTools([]).filter((t) => t.name !== CALL_TOOL_CHAIN_NAME)) {
299
+ expect(tool.description).not.toContain(pointer);
300
+ }
301
+ });
302
+
303
+ it('states what a chain does in the shared rules, and not a second time on the chain', async () => {
304
+ // The pointer is only honest if the rules it points at are THERE. These
305
+ // two were paragraphs of the chain's description; they moved, whole, and a
306
+ // description that kept them as well would be the long one a client cuts.
307
+ const rule = sharedFileRules(testKbContext().layout).find((r) => r.id === 'tool-chain')!;
308
+ expect(rule.body).toContain(CHAIN_FAILURES_RULE);
309
+ expect(rule.body).toContain(CHAIN_LARGE_RESULTS_RULE);
310
+ const served = (await hexisTools()).find((t) => t.name === CALL_TOOL_CHAIN_NAME)!;
311
+ expect(served.description).not.toContain(CHAIN_FAILURES_RULE);
312
+ expect(served.description).not.toContain(CHAIN_LARGE_RESULTS_RULE);
313
+ // What a chained read does to an image was already one of the shared rules.
314
+ const content = sharedFileRules(testKbContext().layout).find((r) => r.id === 'content-kinds')!;
315
+ expect(content.body).toContain('image_omitted');
316
+ });
317
+
318
+ it('keeps the rules on the chain itself where no shared rules are served', () => {
319
+ // The standalone bridge proxies a deployment whose guide it cannot name, so
320
+ // it passes no pointer — and an agent there must still be told what a chain
321
+ // that timed out, or answered too much, or read an image, does.
322
+ const [chain] = codeModeMetaTools('hexis', []).filter((t) => t.name === CALL_TOOL_CHAIN_NAME);
323
+ expect(chain.description).toContain(CHAIN_FAILURES_RULE);
324
+ expect(chain.description).toContain(CHAIN_LARGE_RESULTS_RULE);
325
+ expect(chain.description).toContain('image_omitted');
326
+ });
327
+
328
+ it('names a dry run only on the tools that take one', async () => {
329
+ // A rule is worse than no rule when it promises an argument the tool
330
+ // rejects: `delete_file` has no `dryRun`, so an agent told to preflight a
331
+ // single-file delete gets a validation error on the safe call and learns to
332
+ // skip it. Read off the schemas rather than asserted by hand.
333
+ const rule = sharedFileRules(testKbContext().layout).find((r) => r.id === 'dry-run-confirm')!;
334
+ const tools = await hexisTools();
335
+ // `toolDef` wraps a tool's own inputs under `body`, which is the schema a
336
+ // client validates against — so that is where the argument either is or is not.
337
+ const takesDryRun = (name: string): boolean => {
338
+ const inputs = tools.find((t) => t.name === name)?.inputs as
339
+ | { properties?: { body?: { properties?: Record<string, unknown> } } }
340
+ | undefined;
341
+ return inputs?.properties?.body?.properties?.dryRun !== undefined;
342
+ };
343
+ expect(takesDryRun('move_file')).toBe(true);
344
+ expect(takesDryRun('delete_folder')).toBe(true);
345
+ expect(takesDryRun('delete_file')).toBe(false);
346
+ // Every tool that takes one, read off the catalog rather than listed here —
347
+ // a hand-written list is what let `copy_file` gain a `dryRun` on dev while
348
+ // the rule still named two tools, so an agent reading the guide was told
349
+ // the copy had no preflight it could run.
350
+ const withDryRun = tools.filter((t) => takesDryRun(t.name)).map((t) => t.name);
351
+ expect(withDryRun.length, 'no tool takes a dryRun — the rule would be vacuous').toBeGreaterThan(1);
352
+ for (const name of withDryRun) {
353
+ expect(rule.body, name).toContain(name);
354
+ }
355
+ // Named, but as the tool that has none — never as one that takes one.
356
+ expect(rule.body).toContain('delete_file takes neither');
357
+ expect(rule.body).not.toContain('move_file, delete_file and delete_folder take');
358
+ });
359
+
360
+ it('gives each platform file the depth it actually counts at', () => {
361
+ const rule = sharedFileRules(testKbContext().layout).find((r) => r.id === 'managed-items')!;
362
+ const { anyDepth, rootOnly } = platformFilesByDepth(testKbContext().layout);
363
+ // The split is the half of the rule a list of names leaves out, and
364
+ // `isPlatformFile` is the predicate the prose has to match.
365
+ expect(rule.body).toContain(`${anyDepth.map((n) => `\`${n}\``).join(' or ')} in any folder`);
366
+ expect(rule.body).toContain(`${rootOnly.map((n) => `\`${n}\``).join(' or ')} at the repository root`);
367
+ for (const name of anyDepth) expect(isPlatformFile(`Deep/Folder/${name}`, testKbContext().layout), name).toBe(true);
368
+ for (const name of rootOnly) expect(isPlatformFile(`Deep/Folder/${name}`, testKbContext().layout), name).toBe(false);
369
+ });
370
+
371
+ it('says what unzip extracts, rather than that it takes any bytes', () => {
372
+ const rule = sharedFileRules(testKbContext().layout).find((r) => r.id === 'content-kinds')!;
373
+ // `unzip` refuses anything but a `.zip` (`workspace.service.ts`: "Only .zip
374
+ // files can be extracted"), so the byte-tool clause must not sweep it in.
375
+ expect(rule.body).toContain('unzip extracts the entries of a `.zip`');
376
+ expect(rule.body).not.toContain('and unzip act on bytes of any kind');
377
+ });
378
+ });
@@ -0,0 +1,111 @@
1
+ /**
2
+ * How long a tool description may be, and how to measure one.
3
+ *
4
+ * Clients cut a long description, and they cut it from the END — which is
5
+ * where the text specific to the tool sits, after whatever shared preamble it
6
+ * carried. Agents reported `file_stat`, `read_file`, `write_file` and
7
+ * `write_files` arriving ending in "[truncated]". The rules those descriptions
8
+ * shared now live in one place (see `agent-instructions/shared-file-rules.ts`)
9
+ * and each description ends with one sentence pointing there, which is what
10
+ * makes the cap below reachable rather than aspirational.
11
+ */
12
+
13
+ import { TOOL_PREFIX_CAP } from '@bevel-software/platform-shared';
14
+ import { PREFIXED_TOOLS } from '../agent-instructions/compose.js';
15
+ import { SHARED_RULES_POINTER_MAX, sharedRulesPointer } from '../agent-instructions/shared-file-rules.js';
16
+ import type { UtcpTool } from './tool.contract.js';
17
+
18
+ /**
19
+ * The ceiling on what a client is handed for one tool, enforced by
20
+ * `__tests__/tool-description-length.test.ts`.
21
+ *
22
+ * DERIVED, not published: the clients that cut descriptions do not say where.
23
+ * Two cuts were observed, and they are different problems. claude.ai cuts
24
+ * around 500 characters — NOT what this cap answers, and not something a cap
25
+ * could answer: no useful description of `move_file` fits in 500. What answers
26
+ * that one is the ORDER of the text, which is why the deployment's purpose line
27
+ * is prepended rather than appended (see `prefixToolDescription`) and why every
28
+ * description now leads with what the tool does and ends with the pointer: a
29
+ * cut at 500 then takes the pointer and leaves the tool. The other cut is the
30
+ * four-figure one agents reported on `file_stat`, `read_file`, `write_file` and
31
+ * `write_files`, and 1,200 sits below it with room to spare.
32
+ *
33
+ * So the cap is a ceiling on growth rather than a guarantee of survival, and it
34
+ * is a number the reviewer may move: the point of pinning it is that moving it
35
+ * is a decision someone takes, rather than a paragraph someone appends.
36
+ */
37
+ export const TOOL_DESCRIPTION_CAP = 1_200;
38
+
39
+ /**
40
+ * The OTHER cut — the ~500 characters claude.ai allows — as a number the tests
41
+ * can hold something to.
42
+ *
43
+ * It is deliberately not the value of {@link TOOL_DESCRIPTION_CAP}, and the
44
+ * difference is the whole point: lowering the cap to 500 would not make these
45
+ * descriptions survive that client, it would only move the loss from the client
46
+ * to the source, because no useful description of `move_file` or `file_stat`
47
+ * fits in 500 characters and shortening them to fit means dropping facts an
48
+ * agent needs.
49
+ *
50
+ * What survives a cut here is decided by ORDER instead, and order is testable:
51
+ * the purpose prefix goes first, the tool's own opening sentence next, the
52
+ * pointer to the shared rules last. So a cut at 500 takes the pointer — which
53
+ * costs the agent the name of a file the handshake instructions and the guide
54
+ * both state anyway — and leaves the sentence saying what the tool does.
55
+ * {@link firstSentenceEnd} measures where that sentence ends, and the suite
56
+ * pins it under this number for every tool, so the claim above is a check
57
+ * rather than a comment.
58
+ */
59
+ export const CLIENT_SHORT_CUT = 500;
60
+
61
+ /**
62
+ * Where the tool's OWN opening sentence ends in the text a client is handed:
63
+ * the purpose prefix counted at its cap, as in {@link clientVisibleLength}, and
64
+ * the pointer not counted at all, since it is the part a short cut is meant to
65
+ * take.
66
+ *
67
+ * A description with no sentence-ending punctuation counts whole — the honest
68
+ * answer for text that never finishes a sentence.
69
+ */
70
+ export function firstSentenceEnd(tool: Pick<UtcpTool, 'name' | 'description'>): number {
71
+ const prefix = PREFIXED_TOOLS.has(tool.name) ? TOOL_PREFIX_CAP + 2 : 0;
72
+ const description = tool.description ?? '';
73
+ if (description === '') return prefix;
74
+ const pointer = sharedRulesPointer();
75
+ const own = description.endsWith(pointer) ? description.slice(0, -pointer.length) : description;
76
+ const firstSentence = own.match(/^[\s\S]*?[.!?](?=\s|$)/)?.[0] ?? own;
77
+ return prefix + firstSentence.length;
78
+ }
79
+
80
+ /**
81
+ * The length of the description as a CLIENT receives it — which for the four
82
+ * knowledge-base tools includes the deployment's purpose prefix, since the MCP
83
+ * surface prepends it (`prefixToolDescription`) and the client cuts the result.
84
+ * Measured at the prefix's CAP rather than at whatever the current admin wrote:
85
+ * the cap is what an admin may grow their text to without being told, so a
86
+ * description that only fits beside a short prefix does not really fit.
87
+ *
88
+ * The pointer sentence is measured the same way, for the same reason. It ends
89
+ * every file tool's description and its length moves with a DEPLOYMENT SETTING
90
+ * — the guide's file name — so a description measured beside the nine
91
+ * characters of `AGENTS.md` would pass here and arrive cut on a deployment
92
+ * that renamed its guide. Whatever pointer a description actually carries is
93
+ * discounted and charged at {@link SHARED_RULES_POINTER_MAX} instead.
94
+ */
95
+ export function clientVisibleLength(tool: Pick<UtcpTool, 'name' | 'description'>): number {
96
+ const own = tool.description?.length ?? 0;
97
+ // A prefixed tool with no description of its own is still handed the prefix,
98
+ // and nothing else — `prefixToolDescription` sends the prefix alone, with no
99
+ // blank line after it. Measuring that as zero would under-report the only
100
+ // text the client got.
101
+ if (own === 0) return PREFIXED_TOOLS.has(tool.name) ? TOOL_PREFIX_CAP : 0;
102
+ // The pointer at its worst case rather than at this layout's: swap the one
103
+ // it carries for the longest it could be. A description that does not end
104
+ // with it (`start_session`, the proxied tools) is charged nothing.
105
+ const pointer = sharedRulesPointer();
106
+ const atWorstPointer = tool.description!.endsWith(pointer)
107
+ ? own - pointer.length + SHARED_RULES_POINTER_MAX
108
+ : own;
109
+ // `+ 2` for the blank line `prefixToolDescription` puts between the two.
110
+ return PREFIXED_TOOLS.has(tool.name) ? atWorstPointer + TOOL_PREFIX_CAP + 2 : atWorstPointer;
111
+ }
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { SQL } from 'drizzle-orm';
3
+ import { PgDialect } from 'drizzle-orm/pg-core';
4
+ import { PendingCommitsService } from '../pending-commits.service.js';
5
+ import type { Database } from '../../database/connection.js';
6
+
7
+ /**
8
+ * Commits still queued for a branch when the knowledge-base repository is
9
+ * REPLACED.
10
+ *
11
+ * A release enqueues the bytes and only then drops its lock, so the moment the
12
+ * locks on a branch are dropped there may be rows already waiting — written
13
+ * against a working copy of the repository that is gone. Left `pending`, the
14
+ * worker picks them up after the re-clone and commits them onto a same-named
15
+ * branch of a DIFFERENT repository: somebody else's file, overwritten by a
16
+ * change nobody made to it.
17
+ *
18
+ * So they are escalated, never dropped: the bytes stay in the row, the reason
19
+ * is on it, and a person decides.
20
+ */
21
+ function makeFakeDb(moved: Array<{ id: string }>) {
22
+ const sets: Record<string, unknown>[] = [];
23
+ const db = {
24
+ update: vi.fn(() => ({
25
+ set: (values: Record<string, unknown>) => {
26
+ sets.push(values);
27
+ return { where: () => ({ returning: async () => moved }) };
28
+ },
29
+ })),
30
+ // Queued bytes are never thrown away over this.
31
+ delete: () => {
32
+ throw new Error('a queued commit must never be deleted');
33
+ },
34
+ } as unknown as Database;
35
+ return { db, sets };
36
+ }
37
+
38
+ describe('PendingCommitsService.markNeedsAttentionOnBranch', () => {
39
+ it('escalates every queued commit on the branch, with the reason, and answers how many', async () => {
40
+ const { db, sets } = makeFakeDb([{ id: 'a' }, { id: 'b' }]);
41
+ const svc = new PendingCommitsService(db);
42
+
43
+ await expect(
44
+ svc.markNeedsAttentionOnBranch('alice/draft', 'The repository was replaced.'),
45
+ ).resolves.toBe(2);
46
+
47
+ expect(sets).toHaveLength(1);
48
+ // `needs_attention` is the status the admin surface reads — the row is out
49
+ // of the worker's reach and in front of a person.
50
+ expect(sets[0]?.status).toBe('needs_attention');
51
+ expect(sets[0]?.lastError).toBe('The repository was replaced.');
52
+ expect(sets[0]?.lastAttemptedAt).toBeInstanceOf(Date);
53
+ });
54
+
55
+ it('answers zero when nothing was queued for that branch', async () => {
56
+ const { db } = makeFakeDb([]);
57
+ const svc = new PendingCommitsService(db);
58
+ await expect(svc.markNeedsAttentionOnBranch('quiet/branch', 'whatever')).resolves.toBe(0);
59
+ });
60
+ });
61
+
62
+ /**
63
+ * The same, keyed on the WORKING COPY that was set aside. Closing change
64
+ * requests covers their branches and nothing else; a commit queued on the
65
+ * default branch was written against the repository that was left all the
66
+ * same, and the path it names is about to hold a clone of another one.
67
+ */
68
+ describe('PendingCommitsService.markNeedsAttentionInWorkspace', () => {
69
+ it('escalates every queued commit for the working copy, with the reason, and answers how many', async () => {
70
+ const { db, sets } = makeFakeDb([{ id: 'a' }, { id: 'b' }, { id: 'c' }]);
71
+ const svc = new PendingCommitsService(db);
72
+
73
+ await expect(svc.markNeedsAttentionInWorkspace('main', 'The repository was replaced.')).resolves.toBe(3);
74
+
75
+ expect(sets).toHaveLength(1);
76
+ expect(sets[0]?.status).toBe('needs_attention');
77
+ expect(sets[0]?.lastError).toBe('The repository was replaced.');
78
+ expect(sets[0]?.lastAttemptedAt).toBeInstanceOf(Date);
79
+ });
80
+
81
+ /**
82
+ * Rows are stored under the encoded id, whichever spelling the caller
83
+ * enqueued with. The predicate is rendered and read, since the stub does
84
+ * not evaluate it: the id it compares must be the stored spelling, and it
85
+ * must match only the two statuses a worker could still write, `pending`
86
+ * and `running`.
87
+ */
88
+ it('looks for the working copy under the id the queue stores, and only for commits still queued', async () => {
89
+ const wheres: SQL[] = [];
90
+ const db = {
91
+ update: () => ({
92
+ set: () => ({
93
+ where: (predicate: SQL) => {
94
+ wheres.push(predicate);
95
+ return { returning: async () => [] };
96
+ },
97
+ }),
98
+ }),
99
+ } as unknown as Database;
100
+ await new PendingCommitsService(db).markNeedsAttentionInWorkspace('alice/feature', 'whatever');
101
+
102
+ const { sql: text, params } = new PgDialect().sqlToQuery(wheres[0]!);
103
+ expect(text).toContain('"workspace_id" =');
104
+ expect(params).toEqual(['alice%2Ffeature', 'pending', 'running']);
105
+ });
106
+
107
+ it('answers zero when nothing was queued for it', async () => {
108
+ const { db } = makeFakeDb([]);
109
+ await expect(new PendingCommitsService(db).markNeedsAttentionInWorkspace('main', 'whatever')).resolves.toBe(0);
110
+ });
111
+ });
@@ -8,6 +8,10 @@ import type { AuthService } from '../../auth/auth.service.js';
8
8
  import type { WorkspaceService } from '../../workspace/workspace.service.js';
9
9
  import type { WorkflowEventBus } from '../event-bus.js';
10
10
  import { createWorkflowRoutes } from '../workflow.routes.js';
11
+ import {
12
+ VersionNotOnBranchError,
13
+ VERSION_NOT_ON_BRANCH_MESSAGE,
14
+ } from '../../../shared/domain-errors.js';
11
15
 
12
16
  // A file's history is its content with a time axis: the same default-deny
13
17
  // read model that hides a file must hide its commit list, its diffs, and its
@@ -129,6 +133,27 @@ describe('history routes enforce the read model', () => {
129
133
  expect((await get(`/compare-file?from=a&to=b&path=${OPEN}`)).status).toBe(403);
130
134
  });
131
135
 
136
+ it.each([
137
+ ['/show-file?sha=abc1234&path=%s', 'showFileAtChange'],
138
+ ['/file-at-change?sha=abc1234&path=%s', 'fileAtChange'],
139
+ ] as const)(
140
+ '%s: a save off this branch is refused with the history message',
141
+ async (route, method) => {
142
+ // The service raises it (the git layer knows the branch); the route has
143
+ // to pass the typed refusal through with its own status and sentence,
144
+ // because a reader who is told "not found" about a file that is plainly
145
+ // there learns the wrong thing. The bytes route carries the same rule,
146
+ // so neither can be read around through the other.
147
+ h = await makeHarness();
148
+ h.workflow[method].mockRejectedValueOnce(new VersionNotOnBranchError());
149
+ const res = await get(route.replace('%s', OPEN));
150
+ expect(res.status).toBe(404);
151
+ expect(((await res.json()) as { error: string }).error).toBe(
152
+ VERSION_NOT_ON_BRANCH_MESSAGE,
153
+ );
154
+ },
155
+ );
156
+
132
157
  it('an access-model error fails closed, not open', async () => {
133
158
  h = await makeHarness();
134
159
  h.canRead.mockRejectedValueOnce(new Error('access tree unreadable'));