@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,335 @@
1
+ import { describe, expect, it, vi, beforeAll, afterAll } from 'vitest';
2
+ import '@utcp/direct-call';
3
+ import { addFunctionToUtcpDirectCall } from '@utcp/direct-call';
4
+ import { CodeModeUtcpClient } from '@utcp/code-mode';
5
+ import { codeModeMetaTools, dispatchMetaTool } from '@bevel-software/platform-mcp-core';
6
+ import { createCallToolChainTool } from '../code-mode.tool.js';
7
+
8
+ /**
9
+ * Every Scenario of the Specification, against a REAL `isolated-vm` isolate.
10
+ *
11
+ * mcp-core pins the runtime's pieces one by one, which is where the arithmetic
12
+ * belongs. What it cannot show is that a chain can actually REACH `atob`, that
13
+ * `while(true){}` really is answered rather than dropped, and that the
14
+ * connection survives it — all three are properties of the isolate and of the
15
+ * runner's own resolved shape, and a mock of `callToolChain` would be asserting
16
+ * our belief about `@utcp/code-mode` rather than its behaviour. So this file
17
+ * registers a live manual and runs real chains through the shipping tool.
18
+ */
19
+
20
+ const NAMESPACE = 'KNOWLEDGE_BASE';
21
+ let client: CodeModeUtcpClient;
22
+
23
+ /**
24
+ * The manual is shaped exactly as `toolDef` shapes a real knowledge-base tool:
25
+ * flat arguments wrapped under one required `body`, `branch` required inside it
26
+ * with no `default` and no `enum`, and a handler that REFUSES a call without it
27
+ * the way the route does. That refusal is the point — it is what turned a
28
+ * plausible-looking example into `400 … \`branch\` is required`, and a fixture
29
+ * that accepted anything would have let the example through again.
30
+ */
31
+ function kbTool(name: string, required: string[], callable: string) {
32
+ const properties: Record<string, unknown> = {
33
+ branch: { type: 'string', minLength: 1, description: 'The branch this operates on.' },
34
+ path: { type: 'string', description: 'The path to read.' },
35
+ };
36
+ return {
37
+ name,
38
+ description: `A live ${name}, so the namespace is a real object.`,
39
+ inputs: {
40
+ type: 'object',
41
+ properties: {
42
+ body:
43
+ required.length === 0
44
+ ? { type: 'object', properties: {}, additionalProperties: false }
45
+ : {
46
+ type: 'object',
47
+ properties: Object.fromEntries(required.map((k) => [k, properties[k]])),
48
+ required,
49
+ additionalProperties: false,
50
+ },
51
+ },
52
+ required: ['body'],
53
+ additionalProperties: false,
54
+ },
55
+ outputs: { type: 'object', properties: {} },
56
+ tool_call_template: { call_template_type: 'direct-call', callable_name: callable },
57
+ };
58
+ }
59
+
60
+ /**
61
+ * The real `read_file` handler, as a named installer rather than an inline
62
+ * closure, because a test that swaps it has to put THIS back. Restoring a
63
+ * permissive stand-in instead silently disarmed the branch check for every
64
+ * test that ran afterwards — which is how a test for the missing-argument
65
+ * failure passed while asserting the opposite.
66
+ *
67
+ * Note the unwrapping: the direct-call protocol hands the callable the inner
68
+ * `body` object, so what arrives is `{ branch, path }`, not `{ body: … }`.
69
+ */
70
+ function installReadFile(): void {
71
+ addFunctionToUtcpDirectCall('e2e_read_file', async (args: unknown) => {
72
+ const given = (args ?? {}) as Record<string, unknown>;
73
+ const body = (given.body as Record<string, unknown> | undefined) ?? given;
74
+ // The server's own refusal, reproduced: identity never implies a
75
+ // workspace, so `branch` always comes from the argument.
76
+ if (!body.branch) throw new Error('HTTP 400: `branch` is required');
77
+ if (!body.path) throw new Error('HTTP 400: `path` is required');
78
+ return { content: 'live' };
79
+ });
80
+ }
81
+
82
+ beforeAll(async () => {
83
+ addFunctionToUtcpDirectCall('e2e_manual', async () => ({
84
+ utcp_version: '1.0.0',
85
+ manual_version: '1.0.0',
86
+ tools: [
87
+ kbTool('read_file', ['branch', 'path'], 'e2e_read_file'),
88
+ kbTool('start_session', [], 'e2e_start_session'),
89
+ ],
90
+ }));
91
+ installReadFile();
92
+ addFunctionToUtcpDirectCall('e2e_start_session', async () => ({ sessionId: 's-1' }));
93
+ client = await CodeModeUtcpClient.create(process.cwd(), null);
94
+ const registered = await client.registerManual({
95
+ name: NAMESPACE,
96
+ call_template_type: 'direct-call',
97
+ callable_name: 'e2e_manual',
98
+ } as never);
99
+ expect(registered.success).toBe(true);
100
+ }, 60_000);
101
+
102
+ afterAll(async () => {
103
+ await client?.close();
104
+ });
105
+
106
+ type ChainResult = {
107
+ success: boolean;
108
+ result?: unknown;
109
+ error?: string;
110
+ logs?: string[];
111
+ };
112
+
113
+ /** This client's catalog, in the shape the description builder takes. */
114
+ async function catalog(): Promise<{ utcpName: string; inputSchema: unknown }[]> {
115
+ return (await client.config.tool_repository.getTools()).map((t) => ({
116
+ utcpName: t.name,
117
+ inputSchema: t.inputs,
118
+ }));
119
+ }
120
+
121
+ /** The shipping Mastra tool, run for real against the LIVE catalog. */
122
+ async function chain(code: string, timeout?: number): Promise<ChainResult> {
123
+ const tool = createCallToolChainTool(
124
+ client,
125
+ { write: vi.fn() } as never,
126
+ NAMESPACE,
127
+ await catalog(),
128
+ ) as unknown as {
129
+ execute: (input: { code: string; timeout?: number }) => Promise<ChainResult>;
130
+ };
131
+ return tool.execute({ code, ...(timeout === undefined ? {} : { timeout }) });
132
+ }
133
+
134
+ /** The same chain through the MCP surfaces' dispatcher. */
135
+ async function mcpChain(code: string, timeout?: number): Promise<{ isError?: boolean; text: string }> {
136
+ const res = await dispatchMetaTool(client, 'call_tool_chain', {
137
+ code,
138
+ ...(timeout === undefined ? {} : { timeout }),
139
+ });
140
+ return { isError: res.isError as boolean | undefined, text: (res.content[0] as { text: string }).text };
141
+ }
142
+
143
+ describe('the chain runtime, in a real isolate', () => {
144
+ it('has atob, and it decodes', async () => {
145
+ expect(await chain("return atob('SGVsbG8=')")).toMatchObject({ success: true, result: 'Hello' });
146
+ });
147
+
148
+ it('round-trips UTF-8 text through TextEncoder and TextDecoder', async () => {
149
+ const out = await chain("return new TextDecoder().decode(new TextEncoder().encode('über'))");
150
+ expect(out).toMatchObject({ success: true, result: 'über' });
151
+ });
152
+
153
+ it('has all four globals as functions', async () => {
154
+ const out = await chain(
155
+ 'return [typeof atob, typeof btoa, typeof TextEncoder, typeof TextDecoder]',
156
+ );
157
+ expect(out.result).toEqual(['function', 'function', 'function', 'function']);
158
+ });
159
+
160
+ it('encodes the bytes a browser encodes, and base64s them', async () => {
161
+ const out = await chain(
162
+ "const b = new TextEncoder().encode('über'); return [Array.from(b), btoa(String.fromCharCode.apply(null, Array.from(b)))]",
163
+ );
164
+ expect(out.result).toEqual([[195, 188, 98, 101, 114], 'w7xiZXI=']);
165
+ });
166
+
167
+ it('decodes base64 back into UTF-8 text', async () => {
168
+ const out = await chain(
169
+ "const s = atob('w7xiZXI='); const b = new Uint8Array(s.length); for (let i = 0; i < s.length; i++) b[i] = s.charCodeAt(i); return new TextDecoder().decode(b)",
170
+ );
171
+ expect(out).toMatchObject({ success: true, result: 'über' });
172
+ });
173
+
174
+ it('offers them on the MCP surfaces too', async () => {
175
+ const out = await mcpChain("return atob('SGVsbG8=')");
176
+ expect(out.isError).toBeFalsy();
177
+ expect(JSON.parse(out.text)).toMatchObject({ success: true, result: 'Hello' });
178
+ });
179
+ });
180
+
181
+ describe('a chain that outlives its timeout, in a real isolate', () => {
182
+ it('is answered with a timeout error, and the connection stays usable', async () => {
183
+ const out = await chain('while(true){}', 1_000);
184
+ expect(out.success).toBe(false);
185
+ expect(out.error).toContain('timed out after 1000 ms');
186
+ expect(out.error).toContain('`timeout`');
187
+ expect(out.error).toContain('120000');
188
+ // The Scenario's second half: the NEXT call on the same connection works.
189
+ expect(await chain("return KNOWLEDGE_BASE.read_file({ body: { branch: 'main', path: 'x' } })")).toMatchObject({
190
+ success: true,
191
+ result: { content: 'live' },
192
+ });
193
+ }, 30_000);
194
+
195
+ it('is answered the same way on the MCP surfaces, which then serve the next call', async () => {
196
+ const timedOut = await mcpChain('while(true){}', 1_000);
197
+ expect(timedOut.isError).toBe(true);
198
+ expect(timedOut.text).toContain('timed out after 1000 ms');
199
+ expect(timedOut.text).toContain('120000');
200
+ const next = await mcpChain("return KNOWLEDGE_BASE.read_file({ body: { branch: 'main', path: 'x' } })");
201
+ expect(next.isError).toBeFalsy();
202
+ expect(JSON.parse(next.text)).toMatchObject({ success: true });
203
+ // And a plain tool listing still answers, so nothing about the session died.
204
+ const listed = await dispatchMetaTool(client, 'list_tools', {});
205
+ expect((listed.content[0] as { text: string }).text).toContain('KNOWLEDGE_BASE.read_file');
206
+ }, 30_000);
207
+ });
208
+
209
+ describe('a chain that fails some other way, in a real isolate', () => {
210
+ it('is told which namespaces exist when it names one that does not', async () => {
211
+ const out = await chain("return WRONG.read_file({ path: 'x' })");
212
+ expect(out.success).toBe(false);
213
+ expect(out.error).toContain('WRONG is not defined');
214
+ expect(out.error).toContain('KNOWLEDGE_BASE');
215
+ expect(out.error).toContain('list_tools');
216
+ });
217
+
218
+ it('is answered with its own reason when the chain throws', async () => {
219
+ const out = await chain("throw new Error('the chain said no')");
220
+ expect(out.success).toBe(false);
221
+ expect(out.error).toContain('the chain said no');
222
+ });
223
+
224
+ it('is answered with the tool\'s reason when a tool inside it throws', async () => {
225
+ addFunctionToUtcpDirectCall('e2e_read_file', async () => {
226
+ throw new Error("You don't have read access to \"Secret.md\"");
227
+ });
228
+ try {
229
+ const out = await chain("return KNOWLEDGE_BASE.read_file({ body: { branch: 'main', path: 'Secret.md' } })");
230
+ expect(out.success).toBe(false);
231
+ expect(out.error).toContain('read access');
232
+ } finally {
233
+ // The REAL handler back, branch check included — see `installReadFile`.
234
+ installReadFile();
235
+ }
236
+ });
237
+
238
+ it('is answered with the compiler\'s own complaint rather than an empty success', async () => {
239
+ const out = await chain('return (((');
240
+ expect(out.success).toBe(false);
241
+ expect(out.error).toMatch(/Unexpected token/);
242
+ });
243
+
244
+ it('reports a failure against the chain\'s OWN line numbers, the prelude notwithstanding', async () => {
245
+ // The single-line prelude earns its keep here: the stack must name the
246
+ // line of the agent's code, not a line of a runtime it cannot see.
247
+ const out = await chain('const a = 1;\nconst b = 2;\nreturn WRONG.go();');
248
+ expect(out.success).toBe(false);
249
+ const noPrelude = await client.callToolChain('const a = 1;\nconst b = 2;\nreturn WRONG.go();', 10_000);
250
+ const line = (s: string) => /<isolated-vm>:(\d+):/.exec(s)?.[1];
251
+ expect(line(noPrelude.logs.join('\n'))).toBeDefined();
252
+ // Same reported line with and without the prelude in front of the chain.
253
+ const withPrelude = await client.callToolChain(
254
+ (await import('@bevel-software/platform-mcp-core')).withChainRuntime(
255
+ 'const a = 1;\nconst b = 2;\nreturn WRONG.go();',
256
+ ),
257
+ 10_000,
258
+ );
259
+ expect(line(withPrelude.logs.join('\n'))).toBe(line(noPrelude.logs.join('\n')));
260
+ }, 30_000);
261
+ });
262
+
263
+ describe('a chain that succeeds, in a real isolate', () => {
264
+ it('still calls its tools and returns its value', async () => {
265
+ const out = await chain(
266
+ "const a = KNOWLEDGE_BASE.read_file({ body: { branch: 'main', path: 'x' } }); return { seen: a.content, b64: btoa(a.content) };",
267
+ );
268
+ expect(out).toMatchObject({ success: true, result: { seen: 'live', b64: 'bGl2ZQ==' } });
269
+ });
270
+
271
+ it('can still return null without being read as a failure', async () => {
272
+ expect(await chain('return null')).toMatchObject({ success: true, result: null });
273
+ });
274
+ });
275
+
276
+ /**
277
+ * The acceptance criterion itself: an agent that copies the example call out of
278
+ * the description gets a WORKING call. Run verbatim, in a real isolate, against
279
+ * a manual that refuses a call without `branch` exactly as the route does.
280
+ *
281
+ * This is the test whose absence shipped the bug. The unit tests checked that
282
+ * the example's NAME came from the catalog; nothing executed it, so nobody
283
+ * noticed the arguments beside the name were fixed text omitting a required
284
+ * one. Here the example is extracted from the generated description and run.
285
+ */
286
+ describe('the example call in the description, copied verbatim', () => {
287
+ async function printedExample(): Promise<string> {
288
+ const tool = createCallToolChainTool(
289
+ client,
290
+ { write: vi.fn() } as never,
291
+ NAMESPACE,
292
+ await catalog(),
293
+ ) as unknown as { description: string };
294
+ const printed = /works exactly as written: `return ([^`]+);`/.exec(tool.description);
295
+ expect(printed, 'the description printed no example call').not.toBeNull();
296
+ return printed![1]!;
297
+ }
298
+
299
+ it('runs, and returns a value rather than an error', async () => {
300
+ const example = await printedExample();
301
+ const out = await chain(`return ${example};`);
302
+ expect(out.success).toBe(true);
303
+ expect(out.error).toBeUndefined();
304
+ expect(out.result).not.toBeNull();
305
+ });
306
+
307
+ it('runs on the MCP surfaces too, from the description THEY serve', async () => {
308
+ const chainTool = codeModeMetaTools(NAMESPACE, await catalog()).find(
309
+ (t) => t.name === 'call_tool_chain',
310
+ )!;
311
+ const printed = /works exactly as written: `return ([^`]+);`/.exec(chainTool.description!);
312
+ expect(printed).not.toBeNull();
313
+ const out = await mcpChain(`return ${printed![1]!};`);
314
+ expect(out.isError).toBeFalsy();
315
+ expect(JSON.parse(out.text)).toMatchObject({ success: true });
316
+ });
317
+
318
+ /**
319
+ * The fixture has teeth. If a missing `branch` were quietly accepted, the
320
+ * test above would pass for the wrong reason and the regression would be
321
+ * invisible again — so prove the refusal is real.
322
+ */
323
+ it('fails when a required argument is dropped, which is what the old example did', async () => {
324
+ const out = await chain("return KNOWLEDGE_BASE.read_file({ body: { path: 'x' } })");
325
+ expect(out.success).toBe(false);
326
+ expect(out.error).toContain('`branch` is required');
327
+ });
328
+
329
+ it('names a tool the catalog actually has, in the form the runtime binds', async () => {
330
+ const example = await printedExample();
331
+ const name = /^([\w.$]+)\(/.exec(example)![1]!;
332
+ const bound = await chain(`return typeof ${name};`);
333
+ expect(bound).toMatchObject({ success: true, result: 'function' });
334
+ });
335
+ });
@@ -86,7 +86,7 @@ describe('call_tool_chain image scrub', () => {
86
86
  callToolChain: vi.fn(async () => ({ result: { pic: sentinel, ok: true }, logs: [] as string[] })),
87
87
  } as unknown as CodeModeUtcpClient;
88
88
  const spill = { write: vi.fn(async () => ({ ref: '__tool_chain_spill__/x.json', bytes: 1 })) };
89
- const tool = createCallToolChainTool(chainClient, spill as never) as unknown as {
89
+ const tool = createCallToolChainTool(chainClient, spill as never, 'KNOWLEDGE_BASE') as unknown as {
90
90
  execute: (input: { code: string }) => Promise<unknown>;
91
91
  };
92
92
  const out = JSON.stringify(await tool.execute({ code: 'return 1' }));
@@ -98,6 +98,50 @@ describe('call_tool_chain image scrub', () => {
98
98
  });
99
99
  });
100
100
 
101
+ /**
102
+ * `call_tool_chain` promises a STRUCTURED answer for every outcome. The work
103
+ * past the runner is still fallible — the spill store writes to disk, and
104
+ * `JSON.stringify` can meet a value it cannot serialize — and an exception
105
+ * escaping `execute` reaches the agent as an unhandled tool failure, which is
106
+ * the dropped call this ticket is about.
107
+ */
108
+ describe('call_tool_chain when the result cannot be delivered', () => {
109
+ async function runWith(spill: { write: (json: string) => Promise<{ ref: string; bytes: number }> }, result: unknown) {
110
+ const { createCallToolChainTool } = await import('../code-mode.tool.js');
111
+ const chainClient = {
112
+ callToolChain: vi.fn(async () => ({ result, logs: ['[LOG] halfway'] as string[] })),
113
+ } as unknown as CodeModeUtcpClient;
114
+ const tool = createCallToolChainTool(chainClient, spill as never, 'KNOWLEDGE_BASE') as unknown as {
115
+ execute: (input: { code: string; max_output_size?: number }) => Promise<{
116
+ success: boolean;
117
+ error?: string;
118
+ logs?: string[];
119
+ }>;
120
+ };
121
+ return tool.execute({ code: 'return 1', max_output_size: 1_000 });
122
+ }
123
+
124
+ it('answers with the reason when the spill store cannot write', async () => {
125
+ const out = await runWith(
126
+ { write: vi.fn(async () => Promise.reject(new Error('ENOSPC: no space left on device'))) },
127
+ 'x'.repeat(2_000),
128
+ );
129
+ expect(out.success).toBe(false);
130
+ expect(out.error).toContain('ENOSPC');
131
+ // The chain itself ran, so its logs are the only trace of the work left.
132
+ expect(out.logs).toEqual(['[LOG] halfway']);
133
+ });
134
+
135
+ it('answers with the reason when the chain\'s value cannot be serialized', async () => {
136
+ // A BigInt, not a cycle: `omitImagePayloads` already replaces a cycle with
137
+ // `"[Circular]"`, so a cyclic result serializes fine and is not this case.
138
+ const out = await runWith({ write: vi.fn(async () => ({ ref: 'r', bytes: 1 })) }, { n: 1n });
139
+ expect(out.success).toBe(false);
140
+ expect(out.error).toMatch(/could not be returned/);
141
+ expect(out.error).toMatch(/BigInt/);
142
+ });
143
+ });
144
+
101
145
  /**
102
146
  * `merge_change_request` is retired from every agent tool set, the in-app
103
147
  * chat's included. A chain still calling it gets who merges now, not the
@@ -108,7 +152,7 @@ describe('call_tool_chain and a retired tool', () => {
108
152
 
109
153
  async function runWith(chainClient: CodeModeUtcpClient, code: string) {
110
154
  const { createCallToolChainTool } = await import('../code-mode.tool.js');
111
- const tool = createCallToolChainTool(chainClient, { write: vi.fn() } as never) as unknown as {
155
+ const tool = createCallToolChainTool(chainClient, { write: vi.fn() } as never, 'KNOWLEDGE_BASE') as unknown as {
112
156
  execute: (input: { code: string }) => Promise<{ success: boolean; error?: string; result?: unknown }>;
113
157
  };
114
158
  return tool.execute({ code });
@@ -2,20 +2,53 @@ import '@utcp/direct-call';
2
2
  import { createTool } from '@mastra/core/tools';
3
3
  import { z } from 'zod';
4
4
  import { CodeModeUtcpClient } from '@utcp/code-mode';
5
- import { omitImagePayloads, retiredToolInFailure, retiredToolChainFailure } from '@bevel-software/platform-mcp-core';
5
+ import {
6
+ omitImagePayloads,
7
+ retiredToolInFailure,
8
+ CHAIN_TIMEOUT_DEFAULT_MS,
9
+ CHAIN_TIMEOUT_MAX_MS,
10
+ CHAIN_TIMEOUT_MIN_MS,
11
+ chainExample,
12
+ type ChainExampleTool,
13
+ runToolChain,
14
+ } from '@bevel-software/platform-mcp-core';
6
15
  import type { SpillStore } from '../workspace/spill-store.js';
7
16
  import { utcpNameToTsInterfaceName, findToolByName, AmbiguousToolNameError } from './code-mode-names.js';
8
17
 
18
+ /**
19
+ * The in-process agent's three code-mode tools.
20
+ *
21
+ * `namespace` is the UTCP manual the knowledge-base tools are registered under
22
+ * for THIS client, and every example in the descriptions is written against it.
23
+ * It is a parameter rather than fixed text because it is not the same name on
24
+ * every surface — the hosted endpoint registers `KNOWLEDGE_BASE`, the local MCP
25
+ * server registers the whole deployment as `hexis` — and a description naming
26
+ * the other one taught the agent a namespace the runtime had no binding for.
27
+ *
28
+ * `tools` is this client's catalog — names AND input schemas — and is used only
29
+ * to write an EXAMPLE call that actually runs. Both halves are needed: the
30
+ * namespace does not say how deeply a tool's name is nested, and the name does
31
+ * not say which of its arguments are required (see `chainExample`). Passing
32
+ * nothing prints no concrete example, which is the honest default for a caller
33
+ * that has not handed over its catalog.
34
+ */
9
35
  export function createCallToolChainTool(
10
36
  client: CodeModeUtcpClient,
11
37
  spillStore: SpillStore,
38
+ namespace: string,
39
+ tools: readonly ChainExampleTool[] = [],
12
40
  ) {
41
+ const { namespace: ns, call } = chainExample(namespace, tools);
42
+ // Printed only when the catalog determines every required argument, so a
43
+ // copied example never 400s on a missing one; see `chainExample`.
44
+ const worked = call ? ` A call that works exactly as written: \`return ${call};\`.` : '';
13
45
  return createTool({
14
46
  id: 'call_tool_chain',
15
47
  description: [
16
48
  CodeModeUtcpClient.AGENT_PROMPT_TEMPLATE,
17
- 'Execute JavaScript code with direct access to all registered UTCP tools as hierarchical functions (e.g. `manual.tool(args)`, synchronous, no await). The runtime is plain JavaScript — no type annotations or other TypeScript-only syntax. Return the final value with `return`. Use `list_tools` and `tools_info` first to discover available tools and their interfaces.',
49
+ `Execute JavaScript code with direct access to all registered UTCP tools as hierarchical functions — call them as \`${ns}.<tool>({ body: { ...args } })\`, synchronous, no await.${worked} Every argument a tool declares REQUIRED must be present — for the knowledge-base tools that includes \`branch\`. The runtime is plain JavaScript — no type annotations or other TypeScript-only syntax — plus \`atob\`, \`btoa\`, \`TextEncoder\` and \`TextDecoder\` for base64 and UTF-8 bytes, as in a browser; there is no \`Buffer\`, no \`fetch\` and no \`require\`. Return the final value with \`return\`. Use \`list_tools\` and \`tools_info\` first to discover available tools and their interfaces.`,
18
50
  'Error handling inside the chain: a failing tool call THROWS, and the thrown error\'s `.message` holds the server\'s actual reason (e.g. a 403 with the explanation, not just a status code). If you catch it, surface `err.message` (and `err.status` / `err.data` when present) — NEVER `return { error: err }` or otherwise return the raw Error object, because an Error serializes to `{}` (its `message` is non-enumerable) and the reason is lost. If you don\'t need to handle it, just let it throw — the runtime already reports `err.message` back to you.',
51
+ `Failures are answered, never dropped: a chain that throws comes back with \`success: false\` and the reason, and one that outlives \`timeout\` (default ${CHAIN_TIMEOUT_DEFAULT_MS} ms, maximum ${CHAIN_TIMEOUT_MAX_MS} ms) comes back saying it timed out — raise \`timeout\` or split the work and run it again. Either way your next tool call works as usual.`,
19
52
  'Large return values: if the returned value exceeds `max_output_size`, the full JSON is auto-spilled to a shared spill store (outside any workspace, never committed) and the response contains only a `__tool_chain_spill__/…` ref + a truncated marker. You can read the spill back with the regular `read_file` tool — pass that ref as `path` (its `branch` is ignored) plus `offset` / `limit` to slice it, never read a multi-MB file in full. Order of preference: (1) re-run `call_tool_chain` with a follow-up code chain that filters/maps the data inline and returns just what you need; (2) narrow the API call — shorter `fields`, tighter date window, lower `limit`; (3) last resort — `read_file` against the spill ref with `offset` / `limit`. The spill is read-only context only; do NOT use it as a way to persist KB content — for KB writes use the regular `write_file` / `edit_file` tools, which go through the lock/commit pipeline.',
20
53
  ].join('\n\n'),
21
54
  inputSchema: z.object({
@@ -26,11 +59,11 @@ export function createCallToolChainTool(
26
59
  timeout: z
27
60
  .number()
28
61
  .int()
29
- .min(1_000)
30
- .max(120_000)
62
+ .min(CHAIN_TIMEOUT_MIN_MS)
63
+ .max(CHAIN_TIMEOUT_MAX_MS)
31
64
  .optional()
32
- .default(30_000)
33
- .describe('Timeout in milliseconds (default: 30000).'),
65
+ .default(CHAIN_TIMEOUT_DEFAULT_MS)
66
+ .describe(`Timeout in milliseconds (default: ${CHAIN_TIMEOUT_DEFAULT_MS}, max: ${CHAIN_TIMEOUT_MAX_MS}).`),
34
67
  max_output_size: z
35
68
  .number()
36
69
  .int()
@@ -41,15 +74,35 @@ export function createCallToolChainTool(
41
74
  .describe('Max size of the stringified result in characters (default: 200000, max: 1000000). If exceeded, the full result is spilled to the shared spill store and only a `__tool_chain_spill__/…` ref is returned.'),
42
75
  }),
43
76
  execute: async (input) => {
44
- const timeout = input.timeout ?? 30_000;
77
+ const timeout = input.timeout ?? CHAIN_TIMEOUT_DEFAULT_MS;
45
78
  const maxOutputSize = input.max_output_size ?? 200_000;
79
+ // The shared runner always answers: the chain's browser globals are in
80
+ // place, and a timeout, an exhausted heap and an unknown namespace each
81
+ // come back as the sentence that says so rather than as a success with a
82
+ // null result (which is what the runner's own resolved shape looks like).
83
+ const outcome = await runToolChain(client, input.code, timeout);
84
+ if (!outcome.ok) {
85
+ // A chain that failed while calling a REMOVED tool gets the reason it
86
+ // was removed (who does it now, and where), not "is not a function".
87
+ // The failure is the signal, not the chain's source: a chain that only
88
+ // mentions the name and died of something else keeps its own error.
89
+ const message = retiredToolInFailure(outcome.error) ?? outcome.error;
90
+ return {
91
+ success: false,
92
+ error: message,
93
+ ...(outcome.logs.length ? { logs: outcome.logs } : {}),
94
+ ...(outcome.status !== undefined ? { status: outcome.status } : {}),
95
+ ...(outcome.data !== undefined ? { data: outcome.data } : {}),
96
+ };
97
+ }
98
+ const { result: rawResult, logs } = outcome;
99
+ // Everything past the outcome is still fallible — `omitImagePayloads`
100
+ // walks a value the chain built, `JSON.stringify` can meet a cycle or a
101
+ // BigInt, and the spill store writes to disk. This tool promises a
102
+ // STRUCTURED failure for every outcome, so a disk or serialization error
103
+ // must come back as `success: false` with its reason rather than as an
104
+ // unhandled tool failure the agent sees as a dropped call.
46
105
  try {
47
- const { result: rawResult, logs } = await client.callToolChain(input.code, timeout);
48
- // The runner reports a failed chain in `logs` rather than throwing, so
49
- // a chain that died calling a removed tool is recognised here: the agent
50
- // gets who does it now, not "is not a function".
51
- const retired = retiredToolChainFailure({ result: rawResult, logs });
52
- if (retired) return { success: false, error: retired, logs };
53
106
  // Same policy as the MCP surfaces' `call_tool_chain` (see
54
107
  // `omitImagePayloads`): a chain result is stringified JSON, so an image
55
108
  // read inside it comes back as an omitted-image note instead of a
@@ -68,36 +121,30 @@ export function createCallToolChainTool(
68
121
  result_bytes: bytes,
69
122
  message: `Combined result+logs payload was ${fullJson.length} characters (exceeded max_output_size of ${maxOutputSize}). Full JSON (both \`result\` and \`logs\`) saved to the shared spill store as \`${ref}\` (outside any workspace, uncommitted). Read it back with \`read_file\` — pass that ref as \`path\` (\`branch\` is ignored) plus \`offset\` / \`limit\` for a slice — or, usually better, narrow the next \`call_tool_chain\` call (smaller fields list, tighter date window, lower limit) so the result fits inline.`,
70
123
  };
71
- } catch (e) {
72
- // A failed tool call inside the chain surfaces here. When the UTCP http
73
- // transport carries the server's status + body on the thrown error
74
- // (so non-2xx tool errors aren't reduced to a bare status code), pass
75
- // them through to the agent — `e.message` already holds the server's
76
- // reason, and `status` / `data` give it the structured detail.
77
- const raw = e instanceof Error ? e.message : String(e);
78
- // A chain that failed while calling a removed tool gets the reason it
79
- // was removed (who does it now, and where), not "is not a function".
80
- // The thrown failure is the signal, not the chain's source: a chain
81
- // that only mentions the name and died of something else keeps its own
82
- // error, instead of hiding it behind a migration notice.
83
- const message = retiredToolInFailure(raw) ?? raw;
84
- const status = (e as { status?: unknown })?.status;
85
- const data = (e as { data?: unknown })?.data;
124
+ } catch (err) {
125
+ // The chain itself SUCCEEDED and only its delivery failed, so the logs
126
+ // ride along: they are the only trace of the work left, and an agent
127
+ // deciding whether to re-run a long chain needs them.
86
128
  return {
87
129
  success: false,
88
- error: message,
89
- ...(typeof status === 'number' ? { status } : {}),
90
- ...(data !== undefined ? { data } : {}),
130
+ error: `The chain ran, but its result could not be returned: ${err instanceof Error ? err.message : String(err)}. Re-run a narrower chain that returns only what you need.`,
131
+ ...(logs.length ? { logs } : {}),
91
132
  };
92
133
  }
93
134
  },
94
135
  });
95
136
  }
96
137
 
97
- export function createListToolsTool(client: CodeModeUtcpClient) {
138
+ export function createListToolsTool(
139
+ client: CodeModeUtcpClient,
140
+ namespace: string,
141
+ tools: readonly ChainExampleTool[] = [],
142
+ ) {
143
+ const { name } = chainExample(namespace, tools);
98
144
  return createTool({
99
145
  id: 'list_tools',
100
- description: 'Returns a list of all UTCP tool names currently registered, in their TypeScript-accessible form (e.g. `manual.tool`).',
146
+ // A name the catalog really has, or none: see `ChainExample.name`.
147
+ description: `Returns a list of all UTCP tool names currently registered, in their TypeScript-accessible form${name ? ` (e.g. \`${name}\`)` : ''}.`,
101
148
  inputSchema: z.object({}),
102
149
  execute: async () => {
103
150
  const tools = await client.config.tool_repository.getTools();
@@ -105,7 +152,6 @@ export function createListToolsTool(client: CodeModeUtcpClient) {
105
152
  },
106
153
  });
107
154
  }
108
-
109
155
  export function createToolsInfoTool(client: CodeModeUtcpClient) {
110
156
  return createTool({
111
157
  id: 'tools_info',
@@ -1,3 +1,4 @@
1
+ import { randomBytes } from 'node:crypto';
1
2
  import { describe, it, expect, afterEach } from 'vitest';
2
3
  import {
3
4
  DEFAULT_DB_SCHEMA,
@@ -88,6 +89,17 @@ describe('getDb', () => {
88
89
  expect(getDb(URL, { schema: DEFAULT_DB_SCHEMA })).toBe(plain);
89
90
  });
90
91
 
92
+ it('hands the cached pool to a caller holding the same key spelled otherwise, and refuses another key', () => {
93
+ const raw = randomBytes(32);
94
+ const first = getDb(URL, { schema: 'keyed', piiKey: raw.toString('base64') });
95
+ opened.push(first);
96
+ // The same 32 bytes as hex: one key, so one pool.
97
+ expect(getDb(URL, { schema: 'keyed', piiKey: raw.toString('hex') })).toBe(first);
98
+ expect(() => getDb(URL, { schema: 'keyed', piiKey: randomBytes(32).toString('base64') })).toThrow(
99
+ /already open with a different personal-data key/,
100
+ );
101
+ });
102
+
91
103
  it('opens a fresh pool after the cached one was closed', async () => {
92
104
  const first = getDb(URL, { schema: 'closing' });
93
105
  await closeDb(first);