@bevel-software/platform-core-backend 0.11.1 → 0.12.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 (443) hide show
  1. package/THIRD-PARTY-NOTICES.md +1163 -425
  2. package/dist/core/create-core-server.js +1 -1
  3. package/dist/core/create-core-server.js.map +1 -1
  4. package/dist/core/create-core-services.d.ts +3 -1
  5. package/dist/core/create-core-services.d.ts.map +1 -1
  6. package/dist/core/create-core-services.js +7 -2
  7. package/dist/core/create-core-services.js.map +1 -1
  8. package/dist/core-config.d.ts +7 -0
  9. package/dist/core-config.d.ts.map +1 -1
  10. package/dist/core-config.js +9 -0
  11. package/dist/core-config.js.map +1 -1
  12. package/dist/modules/access/access-control.service.d.ts +1 -253
  13. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  14. package/dist/modules/access/access-control.service.js +3 -632
  15. package/dist/modules/access/access-control.service.js.map +1 -1
  16. package/dist/modules/access/access-declarations.d.ts +2 -2
  17. package/dist/modules/access/access-declarations.d.ts.map +1 -1
  18. package/dist/modules/access/access-declarations.js +2 -2
  19. package/dist/modules/access/access-declarations.js.map +1 -1
  20. package/dist/modules/access/access-mutation.service.d.ts +3 -3
  21. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  22. package/dist/modules/access/access-mutation.service.js +3 -3
  23. package/dist/modules/access/access-mutation.service.js.map +1 -1
  24. package/dist/modules/access/access.routes.js +4 -4
  25. package/dist/modules/access/access.routes.js.map +1 -1
  26. package/dist/modules/access/admin-locked-commit.d.ts +2 -2
  27. package/dist/modules/access/admin-locked-commit.d.ts.map +1 -1
  28. package/dist/modules/access/admin-locked-commit.js +2 -2
  29. package/dist/modules/access/admin-locked-commit.js.map +1 -1
  30. package/dist/modules/access/admin-route-helpers.js +1 -1
  31. package/dist/modules/access/admin-route-helpers.js.map +1 -1
  32. package/dist/modules/access/capability-registry.js +1 -1
  33. package/dist/modules/access/capability-registry.js.map +1 -1
  34. package/dist/modules/access/creator-access.d.ts +1 -45
  35. package/dist/modules/access/creator-access.d.ts.map +1 -1
  36. package/dist/modules/access/creator-access.js +4 -24
  37. package/dist/modules/access/creator-access.js.map +1 -1
  38. package/dist/modules/access/groups-admin.routes.js +1 -1
  39. package/dist/modules/access/groups-admin.routes.js.map +1 -1
  40. package/dist/modules/access/groups-admin.service.d.ts +1 -1
  41. package/dist/modules/access/groups-admin.service.d.ts.map +1 -1
  42. package/dist/modules/access/groups-admin.service.js +6 -5
  43. package/dist/modules/access/groups-admin.service.js.map +1 -1
  44. package/dist/modules/access/groups-edit.js +2 -2
  45. package/dist/modules/access/groups-edit.js.map +1 -1
  46. package/dist/modules/access/reference-scan.js +1 -1
  47. package/dist/modules/access/reference-scan.js.map +1 -1
  48. package/dist/modules/access/roles-admin.service.d.ts +1 -1
  49. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  50. package/dist/modules/access/roles-admin.service.js +8 -7
  51. package/dist/modules/access/roles-admin.service.js.map +1 -1
  52. package/dist/modules/access/roles-edit.js +1 -1
  53. package/dist/modules/access/roles-edit.js.map +1 -1
  54. package/dist/modules/access/synced-groups-committer.js +4 -4
  55. package/dist/modules/access/synced-groups-committer.js.map +1 -1
  56. package/dist/modules/access/synced-groups-writer.js +2 -2
  57. package/dist/modules/access/synced-groups-writer.js.map +1 -1
  58. package/dist/modules/access-model/access-errors.d.ts +34 -0
  59. package/dist/modules/access-model/access-errors.d.ts.map +1 -0
  60. package/dist/modules/access-model/access-errors.js +40 -0
  61. package/dist/modules/access-model/access-errors.js.map +1 -0
  62. package/dist/modules/access-model/access-grammar.d.ts +266 -0
  63. package/dist/modules/access-model/access-grammar.d.ts.map +1 -0
  64. package/dist/modules/access-model/access-grammar.js +644 -0
  65. package/dist/modules/access-model/access-grammar.js.map +1 -0
  66. package/dist/modules/access-model/access-splice.d.ts +140 -0
  67. package/dist/modules/access-model/access-splice.d.ts.map +1 -0
  68. package/dist/modules/access-model/access-splice.js +389 -0
  69. package/dist/modules/access-model/access-splice.js.map +1 -0
  70. package/dist/modules/access-model/creator.d.ts +54 -0
  71. package/dist/modules/access-model/creator.d.ts.map +1 -0
  72. package/dist/modules/access-model/creator.js +30 -0
  73. package/dist/modules/access-model/creator.js.map +1 -0
  74. package/dist/modules/access-model/group-files.d.ts +83 -0
  75. package/dist/modules/access-model/group-files.d.ts.map +1 -0
  76. package/dist/modules/access-model/group-files.js +167 -0
  77. package/dist/modules/access-model/group-files.js.map +1 -0
  78. package/dist/modules/access-model/kb-read-filter.d.ts +41 -0
  79. package/dist/modules/access-model/kb-read-filter.d.ts.map +1 -0
  80. package/dist/modules/access-model/kb-read-filter.js +60 -0
  81. package/dist/modules/access-model/kb-read-filter.js.map +1 -0
  82. package/dist/modules/access-model/render-roles-yaml.d.ts +22 -0
  83. package/dist/modules/access-model/render-roles-yaml.d.ts.map +1 -0
  84. package/dist/modules/access-model/render-roles-yaml.js +56 -0
  85. package/dist/modules/access-model/render-roles-yaml.js.map +1 -0
  86. package/dist/modules/access-model/roles-yaml-guard.d.ts +56 -0
  87. package/dist/modules/access-model/roles-yaml-guard.d.ts.map +1 -0
  88. package/dist/modules/access-model/roles-yaml-guard.js +79 -0
  89. package/dist/modules/access-model/roles-yaml-guard.js.map +1 -0
  90. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  91. package/dist/modules/admin/admin-access.service.js +1 -1
  92. package/dist/modules/admin/admin-access.service.js.map +1 -1
  93. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  94. package/dist/modules/code-mode/code-mode.tool.js +7 -1
  95. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  96. package/dist/modules/diff/diff.routes.js +4 -4
  97. package/dist/modules/diff/diff.routes.js.map +1 -1
  98. package/dist/modules/diff/diff.service.d.ts +1 -1
  99. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  100. package/dist/modules/kb-fs/branch-name.d.ts +10 -0
  101. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -0
  102. package/dist/modules/kb-fs/branch-name.js +76 -0
  103. package/dist/modules/kb-fs/branch-name.js.map +1 -0
  104. package/dist/modules/kb-fs/clone-config.d.ts +61 -0
  105. package/dist/modules/kb-fs/clone-config.d.ts.map +1 -0
  106. package/dist/modules/kb-fs/clone-config.js +69 -0
  107. package/dist/modules/kb-fs/clone-config.js.map +1 -0
  108. package/dist/modules/kb-fs/file-change-notifier.d.ts +38 -0
  109. package/dist/modules/kb-fs/file-change-notifier.d.ts.map +1 -0
  110. package/dist/modules/kb-fs/file-change-notifier.js +22 -0
  111. package/dist/modules/kb-fs/file-change-notifier.js.map +1 -0
  112. package/dist/modules/kb-fs/locking-filesystem.d.ts +137 -0
  113. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -0
  114. package/dist/modules/kb-fs/locking-filesystem.js +553 -0
  115. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -0
  116. package/dist/modules/kb-fs/mutex.d.ts +11 -0
  117. package/dist/modules/kb-fs/mutex.d.ts.map +1 -0
  118. package/dist/modules/kb-fs/mutex.js +23 -0
  119. package/dist/modules/kb-fs/mutex.js.map +1 -0
  120. package/dist/modules/kb-fs/read-only-filesystem.d.ts +24 -0
  121. package/dist/modules/kb-fs/read-only-filesystem.d.ts.map +1 -0
  122. package/dist/modules/kb-fs/read-only-filesystem.js +39 -0
  123. package/dist/modules/kb-fs/read-only-filesystem.js.map +1 -0
  124. package/dist/modules/plugins/join-proposals.d.ts +1 -1
  125. package/dist/modules/plugins/join-proposals.d.ts.map +1 -1
  126. package/dist/modules/plugins/join-proposals.js +1 -1
  127. package/dist/modules/plugins/join-proposals.js.map +1 -1
  128. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  129. package/dist/modules/plugins/join-requests.service.js +1 -1
  130. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  131. package/dist/modules/plugins/plugin-provision.service.js +3 -3
  132. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  133. package/dist/modules/plugins/plugins.routes.js +2 -2
  134. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  135. package/dist/modules/plugins/plugins.service.js +1 -1
  136. package/dist/modules/plugins/plugins.service.js.map +1 -1
  137. package/dist/modules/secrets-vault/secrets-vault.routes.js +1 -1
  138. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  139. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  140. package/dist/modules/skills/pending-skills.service.js +1 -1
  141. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  142. package/dist/modules/skills/skills.service.js +1 -1
  143. package/dist/modules/skills/skills.service.js.map +1 -1
  144. package/dist/modules/tool-helpers/tool-context.d.ts +2 -2
  145. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  146. package/dist/modules/tool-helpers/tool-context.js +4 -4
  147. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  148. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +1 -1
  149. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  150. package/dist/modules/tool-manuals/mcp-server-edit.service.js +1 -1
  151. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  152. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +1 -1
  153. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  154. package/dist/modules/tool-manuals/tool-manuals.routes.js +1 -1
  155. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  156. package/dist/modules/tool-manuals/tool-manuals.service.js +1 -1
  157. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  158. package/dist/modules/tool-manuals/tool-manuals.tools.js +1 -1
  159. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  160. package/dist/modules/workflow/agent-tools/workflow.tools.js +1 -1
  161. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  162. package/dist/modules/workflow/file-lock.service.js +1 -1
  163. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  164. package/dist/modules/workflow/git/git.service.d.ts +2 -2
  165. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  166. package/dist/modules/workflow/git/git.service.js +5 -5
  167. package/dist/modules/workflow/git/git.service.js.map +1 -1
  168. package/dist/modules/workflow/git/pull-request.service.js +1 -1
  169. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  170. package/dist/modules/workflow/review-workflow/review-workflow.service.js +2 -2
  171. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  172. package/dist/modules/workflow/session-ontology.service.js +1 -1
  173. package/dist/modules/workflow/session-ontology.service.js.map +1 -1
  174. package/dist/modules/workflow/workflow.routes.js +2 -2
  175. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  176. package/dist/modules/workflow/workflow.service.d.ts +1 -1
  177. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  178. package/dist/modules/workflow/workflow.service.js +4 -4
  179. package/dist/modules/workflow/workflow.service.js.map +1 -1
  180. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +71 -0
  181. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -0
  182. package/dist/modules/workspace/file-readers/doc-extract.service.js +90 -0
  183. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -0
  184. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts +55 -0
  185. package/dist/modules/workspace/file-readers/doc-extract.types.d.ts.map +1 -0
  186. package/dist/modules/workspace/file-readers/doc-extract.types.js +34 -0
  187. package/dist/modules/workspace/file-readers/doc-extract.types.js.map +1 -0
  188. package/dist/modules/workspace/file-readers/document-reader.d.ts +32 -0
  189. package/dist/modules/workspace/file-readers/document-reader.d.ts.map +1 -0
  190. package/dist/modules/workspace/file-readers/document-reader.js +59 -0
  191. package/dist/modules/workspace/file-readers/document-reader.js.map +1 -0
  192. package/dist/modules/workspace/file-readers/email-reader.d.ts +15 -0
  193. package/dist/modules/workspace/file-readers/email-reader.d.ts.map +1 -0
  194. package/dist/modules/workspace/file-readers/email-reader.js +19 -0
  195. package/dist/modules/workspace/file-readers/email-reader.js.map +1 -0
  196. package/dist/modules/workspace/file-readers/email-text.d.ts +51 -0
  197. package/dist/modules/workspace/file-readers/email-text.d.ts.map +1 -0
  198. package/dist/modules/workspace/file-readers/email-text.js +151 -0
  199. package/dist/modules/workspace/file-readers/email-text.js.map +1 -0
  200. package/dist/modules/workspace/file-readers/extract-docx.d.ts +13 -0
  201. package/dist/modules/workspace/file-readers/extract-docx.d.ts.map +1 -0
  202. package/dist/modules/workspace/file-readers/extract-docx.js +67 -0
  203. package/dist/modules/workspace/file-readers/extract-docx.js.map +1 -0
  204. package/dist/modules/workspace/file-readers/extract-eml.d.ts +18 -0
  205. package/dist/modules/workspace/file-readers/extract-eml.d.ts.map +1 -0
  206. package/dist/modules/workspace/file-readers/extract-eml.js +87 -0
  207. package/dist/modules/workspace/file-readers/extract-eml.js.map +1 -0
  208. package/dist/modules/workspace/file-readers/extract-msg.d.ts +17 -0
  209. package/dist/modules/workspace/file-readers/extract-msg.d.ts.map +1 -0
  210. package/dist/modules/workspace/file-readers/extract-msg.js +121 -0
  211. package/dist/modules/workspace/file-readers/extract-msg.js.map +1 -0
  212. package/dist/modules/workspace/file-readers/extract-odp.d.ts +13 -0
  213. package/dist/modules/workspace/file-readers/extract-odp.d.ts.map +1 -0
  214. package/dist/modules/workspace/file-readers/extract-odp.js +60 -0
  215. package/dist/modules/workspace/file-readers/extract-odp.js.map +1 -0
  216. package/dist/modules/workspace/file-readers/extract-ods.d.ts +10 -0
  217. package/dist/modules/workspace/file-readers/extract-ods.d.ts.map +1 -0
  218. package/dist/modules/workspace/file-readers/extract-ods.js +173 -0
  219. package/dist/modules/workspace/file-readers/extract-ods.js.map +1 -0
  220. package/dist/modules/workspace/file-readers/extract-odt.d.ts +17 -0
  221. package/dist/modules/workspace/file-readers/extract-odt.d.ts.map +1 -0
  222. package/dist/modules/workspace/file-readers/extract-odt.js +45 -0
  223. package/dist/modules/workspace/file-readers/extract-odt.js.map +1 -0
  224. package/dist/modules/workspace/file-readers/extract-pdf.d.ts +3 -0
  225. package/dist/modules/workspace/file-readers/extract-pdf.d.ts.map +1 -0
  226. package/dist/modules/workspace/file-readers/extract-pdf.js +176 -0
  227. package/dist/modules/workspace/file-readers/extract-pdf.js.map +1 -0
  228. package/dist/modules/workspace/file-readers/extract-pptx.d.ts +37 -0
  229. package/dist/modules/workspace/file-readers/extract-pptx.d.ts.map +1 -0
  230. package/dist/modules/workspace/file-readers/extract-pptx.js +288 -0
  231. package/dist/modules/workspace/file-readers/extract-pptx.js.map +1 -0
  232. package/dist/modules/workspace/file-readers/extract-xlsx.d.ts +10 -0
  233. package/dist/modules/workspace/file-readers/extract-xlsx.d.ts.map +1 -0
  234. package/dist/modules/workspace/file-readers/extract-xlsx.js +98 -0
  235. package/dist/modules/workspace/file-readers/extract-xlsx.js.map +1 -0
  236. package/dist/modules/workspace/file-readers/extraction-cache.d.ts +61 -0
  237. package/dist/modules/workspace/file-readers/extraction-cache.d.ts.map +1 -0
  238. package/dist/modules/workspace/file-readers/extraction-cache.js +135 -0
  239. package/dist/modules/workspace/file-readers/extraction-cache.js.map +1 -0
  240. package/dist/modules/workspace/file-readers/file-reader.d.ts +76 -0
  241. package/dist/modules/workspace/file-readers/file-reader.d.ts.map +1 -0
  242. package/dist/modules/workspace/file-readers/file-reader.js +55 -0
  243. package/dist/modules/workspace/file-readers/file-reader.js.map +1 -0
  244. package/dist/modules/workspace/file-readers/file-reader.registry.d.ts +13 -0
  245. package/dist/modules/workspace/file-readers/file-reader.registry.d.ts.map +1 -0
  246. package/dist/modules/workspace/file-readers/file-reader.registry.js +41 -0
  247. package/dist/modules/workspace/file-readers/file-reader.registry.js.map +1 -0
  248. package/dist/modules/workspace/file-readers/image-read.d.ts +35 -0
  249. package/dist/modules/workspace/file-readers/image-read.d.ts.map +1 -0
  250. package/dist/modules/workspace/file-readers/image-read.js +108 -0
  251. package/dist/modules/workspace/file-readers/image-read.js.map +1 -0
  252. package/dist/modules/workspace/file-readers/image-reader.d.ts +19 -0
  253. package/dist/modules/workspace/file-readers/image-reader.d.ts.map +1 -0
  254. package/dist/modules/workspace/file-readers/image-reader.js +30 -0
  255. package/dist/modules/workspace/file-readers/image-reader.js.map +1 -0
  256. package/dist/modules/workspace/file-readers/odf-text.d.ts +26 -0
  257. package/dist/modules/workspace/file-readers/odf-text.d.ts.map +1 -0
  258. package/dist/modules/workspace/file-readers/odf-text.js +116 -0
  259. package/dist/modules/workspace/file-readers/odf-text.js.map +1 -0
  260. package/dist/modules/workspace/file-readers/ooxml-text.d.ts +172 -0
  261. package/dist/modules/workspace/file-readers/ooxml-text.d.ts.map +1 -0
  262. package/dist/modules/workspace/file-readers/ooxml-text.js +439 -0
  263. package/dist/modules/workspace/file-readers/ooxml-text.js.map +1 -0
  264. package/dist/modules/workspace/file-readers/text-reader.d.ts +47 -0
  265. package/dist/modules/workspace/file-readers/text-reader.d.ts.map +1 -0
  266. package/dist/modules/workspace/file-readers/text-reader.js +117 -0
  267. package/dist/modules/workspace/file-readers/text-reader.js.map +1 -0
  268. package/dist/modules/workspace/startup/kb-startup-runner.js +1 -1
  269. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  270. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +1 -1
  271. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -1
  272. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +1 -1
  273. package/dist/modules/workspace/startup/steps/roles-yaml.step.js +2 -2
  274. package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -1
  275. package/dist/modules/workspace/startup/steps/seed-tree.js +1 -1
  276. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  277. package/dist/modules/workspace/workspace.routes.d.ts +1 -1
  278. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  279. package/dist/modules/workspace/workspace.routes.js +5 -4
  280. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  281. package/dist/modules/workspace/workspace.service.d.ts +2 -9
  282. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  283. package/dist/modules/workspace/workspace.service.js +14 -15
  284. package/dist/modules/workspace/workspace.service.js.map +1 -1
  285. package/dist/modules/workspace/workspace.tools.d.ts +2 -1
  286. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  287. package/dist/modules/workspace/workspace.tools.js +161 -18
  288. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  289. package/dist/shared/domain-errors.d.ts +202 -0
  290. package/dist/shared/domain-errors.d.ts.map +1 -0
  291. package/dist/shared/domain-errors.js +303 -0
  292. package/dist/shared/domain-errors.js.map +1 -0
  293. package/dist/shared/workspace-id.d.ts +27 -0
  294. package/dist/shared/workspace-id.d.ts.map +1 -0
  295. package/dist/shared/workspace-id.js +36 -0
  296. package/dist/shared/workspace-id.js.map +1 -0
  297. package/package.json +9 -4
  298. package/src/core/create-core-server.ts +1 -1
  299. package/src/core/create-core-services.ts +8 -2
  300. package/src/core-config.ts +9 -0
  301. package/src/modules/access/__tests__/access-control.service.test.ts +1 -1
  302. package/src/modules/access/__tests__/access-groups.test.ts +6 -5
  303. package/src/modules/access/__tests__/access-md-format.test.ts +3 -3
  304. package/src/modules/access/__tests__/access-mutation.service.test.ts +1 -1
  305. package/src/modules/access/__tests__/admin-locked-commit.test.ts +1 -1
  306. package/src/modules/access/__tests__/admin-route-helpers.test.ts +1 -1
  307. package/src/modules/access/__tests__/roles-admin.service.test.ts +2 -2
  308. package/src/modules/access/__tests__/roles-edit.test.ts +1 -1
  309. package/src/modules/access/__tests__/synced-groups-committer.test.ts +1 -1
  310. package/src/modules/access/__tests__/synced-groups-writer.test.ts +1 -1
  311. package/src/modules/access/access-control.service.ts +23 -768
  312. package/src/modules/access/access-declarations.ts +2 -2
  313. package/src/modules/access/access-mutation.service.ts +3 -3
  314. package/src/modules/access/access.routes.ts +5 -5
  315. package/src/modules/access/admin-locked-commit.ts +2 -2
  316. package/src/modules/access/admin-route-helpers.ts +1 -1
  317. package/src/modules/access/capability-registry.ts +1 -1
  318. package/src/modules/access/creator-access.ts +9 -72
  319. package/src/modules/access/groups-admin.routes.ts +1 -1
  320. package/src/modules/access/groups-admin.service.ts +6 -7
  321. package/src/modules/access/groups-edit.ts +2 -2
  322. package/src/modules/access/reference-scan.ts +1 -1
  323. package/src/modules/access/roles-admin.service.ts +8 -8
  324. package/src/modules/access/roles-edit.ts +1 -1
  325. package/src/modules/access/synced-groups-committer.ts +4 -4
  326. package/src/modules/access/synced-groups-writer.ts +2 -2
  327. package/src/modules/access-model/__tests__/access-grammar.test.ts +31 -0
  328. package/src/modules/{access → access-model}/__tests__/access-splice.test.ts +268 -268
  329. package/src/modules/{access → access-model}/access-errors.ts +1 -1
  330. package/src/modules/access-model/access-grammar.ts +778 -0
  331. package/src/modules/{access → access-model}/access-splice.ts +1 -1
  332. package/src/modules/access-model/creator.ts +79 -0
  333. package/src/modules/{access → access-model}/group-files.ts +1 -1
  334. package/src/modules/{access → access-model}/render-roles-yaml.ts +1 -1
  335. package/src/modules/{access → access-model}/roles-yaml-guard.ts +2 -2
  336. package/src/modules/admin/admin-access.service.ts +2 -1
  337. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +30 -0
  338. package/src/modules/code-mode/code-mode.tool.ts +7 -1
  339. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +2 -2
  340. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +3 -2
  341. package/src/modules/diff/__tests__/diff.service.test.ts +3 -2
  342. package/src/modules/diff/diff.routes.ts +4 -4
  343. package/src/modules/diff/diff.service.ts +1 -1
  344. package/src/modules/{workflow/git → kb-fs}/__tests__/branch-name.test.ts +1 -1
  345. package/src/modules/{workflow → kb-fs}/__tests__/locking-filesystem.test.ts +1 -1
  346. package/src/modules/{workflow/git → kb-fs}/branch-name.ts +1 -1
  347. package/src/modules/{workflow → kb-fs}/locking-filesystem.ts +2 -2
  348. package/src/modules/plugins/__tests__/join-requests.service.test.ts +2 -4
  349. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +1 -1
  350. package/src/modules/plugins/__tests__/plugins.routes.test.ts +1 -1
  351. package/src/modules/plugins/join-proposals.ts +1 -1
  352. package/src/modules/plugins/join-requests.service.ts +2 -1
  353. package/src/modules/plugins/plugin-provision.service.ts +3 -3
  354. package/src/modules/plugins/plugins.routes.ts +2 -2
  355. package/src/modules/plugins/plugins.service.ts +1 -1
  356. package/src/modules/secrets-vault/secrets-vault.routes.ts +1 -1
  357. package/src/modules/skills/__tests__/skills.service.test.ts +1 -1
  358. package/src/modules/skills/pending-skills.service.ts +2 -1
  359. package/src/modules/skills/skills.service.ts +1 -1
  360. package/src/modules/tool-helpers/__tests__/phase4-tools.test.ts +2 -1
  361. package/src/modules/tool-helpers/tool-context.ts +6 -5
  362. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +1 -1
  363. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +1 -1
  364. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +1 -1
  365. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +1 -1
  366. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +1 -1
  367. package/src/modules/tool-manuals/mcp-server-edit.service.ts +2 -1
  368. package/src/modules/tool-manuals/tool-manuals.routes.ts +2 -1
  369. package/src/modules/tool-manuals/tool-manuals.service.ts +1 -1
  370. package/src/modules/tool-manuals/tool-manuals.tools.ts +1 -1
  371. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  372. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -1
  373. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  374. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +1 -1
  375. package/src/modules/workflow/agent-tools/workflow.tools.ts +1 -1
  376. package/src/modules/workflow/file-lock.service.ts +1 -1
  377. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +1 -1
  378. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
  379. package/src/modules/workflow/git/__tests__/git.service.diffFileAtCommit.test.ts +1 -1
  380. package/src/modules/workflow/git/__tests__/git.service.diffFileBetweenBranches.test.ts +1 -1
  381. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +1 -1
  382. package/src/modules/workflow/git/git.service.ts +5 -5
  383. package/src/modules/workflow/git/pull-request.service.ts +1 -1
  384. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  385. package/src/modules/workflow/review-workflow/review-workflow.service.ts +2 -2
  386. package/src/modules/workflow/session-ontology.service.ts +1 -1
  387. package/src/modules/workflow/workflow.routes.ts +2 -2
  388. package/src/modules/workflow/workflow.service.ts +5 -5
  389. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +1 -1
  390. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +1 -1
  391. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -2
  392. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +1 -1
  393. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +2 -5
  394. package/src/modules/workspace/__tests__/workspace.service.test.ts +13 -1
  395. package/src/modules/workspace/__tests__/workspace.tools.test.ts +501 -3
  396. package/src/modules/workspace/file-readers/__tests__/doc-extract.test.ts +1658 -0
  397. package/src/modules/workspace/file-readers/__tests__/email-extract.test.ts +485 -0
  398. package/src/modules/workspace/file-readers/__tests__/file-reader.registry.test.ts +97 -0
  399. package/src/modules/workspace/file-readers/__tests__/image-read.test.ts +100 -0
  400. package/src/modules/workspace/file-readers/doc-extract.service.ts +104 -0
  401. package/src/modules/workspace/file-readers/doc-extract.types.ts +63 -0
  402. package/src/modules/workspace/file-readers/document-reader.ts +64 -0
  403. package/src/modules/workspace/file-readers/email-reader.ts +21 -0
  404. package/src/modules/workspace/file-readers/email-text.ts +193 -0
  405. package/src/modules/workspace/file-readers/extract-docx.ts +67 -0
  406. package/src/modules/workspace/file-readers/extract-eml.ts +92 -0
  407. package/src/modules/workspace/file-readers/extract-msg.ts +134 -0
  408. package/src/modules/workspace/file-readers/extract-odp.ts +63 -0
  409. package/src/modules/workspace/file-readers/extract-ods.ts +182 -0
  410. package/src/modules/workspace/file-readers/extract-odt.ts +48 -0
  411. package/src/modules/workspace/file-readers/extract-pdf.ts +178 -0
  412. package/src/modules/workspace/file-readers/extract-pptx.ts +302 -0
  413. package/src/modules/workspace/file-readers/extract-xlsx.ts +96 -0
  414. package/src/modules/workspace/file-readers/extraction-cache.ts +142 -0
  415. package/src/modules/workspace/file-readers/file-reader.registry.ts +45 -0
  416. package/src/modules/workspace/file-readers/file-reader.ts +104 -0
  417. package/src/modules/workspace/file-readers/image-read.ts +122 -0
  418. package/src/modules/workspace/file-readers/image-reader.ts +39 -0
  419. package/src/modules/workspace/file-readers/odf-text.ts +123 -0
  420. package/src/modules/workspace/file-readers/ooxml-text.ts +477 -0
  421. package/src/modules/workspace/file-readers/text-reader.ts +131 -0
  422. package/src/modules/workspace/startup/kb-startup-runner.ts +1 -1
  423. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +1 -1
  424. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +1 -1
  425. package/src/modules/workspace/startup/steps/roles-yaml.step.ts +2 -2
  426. package/src/modules/workspace/startup/steps/seed-tree.ts +1 -1
  427. package/src/modules/workspace/workspace.routes.ts +6 -5
  428. package/src/modules/workspace/workspace.service.ts +14 -18
  429. package/src/modules/workspace/workspace.tools.ts +177 -15
  430. package/src/shared/__tests__/join-request.test.ts +1 -1
  431. package/src/shared/__tests__/workspace-id.test.ts +17 -0
  432. package/src/{modules/workflow/workflow.errors.ts → shared/domain-errors.ts} +6 -1
  433. package/src/shared/workspace-id.ts +36 -0
  434. /package/src/modules/{access → access-model}/__tests__/kb-read-filter.test.ts +0 -0
  435. /package/src/modules/{access → access-model}/__tests__/roles-yaml-guard.test.ts +0 -0
  436. /package/src/modules/{access → access-model}/kb-read-filter.ts +0 -0
  437. /package/src/modules/{workflow/git → kb-fs}/__tests__/clone-config.test.ts +0 -0
  438. /package/src/modules/{workflow → kb-fs}/__tests__/file-change-notifier.test.ts +0 -0
  439. /package/src/modules/{workflow/git → kb-fs}/__tests__/mutex.test.ts +0 -0
  440. /package/src/modules/{workflow/git → kb-fs}/clone-config.ts +0 -0
  441. /package/src/modules/{workflow → kb-fs}/file-change-notifier.ts +0 -0
  442. /package/src/modules/{workflow/git → kb-fs}/mutex.ts +0 -0
  443. /package/src/modules/{workflow → kb-fs}/read-only-filesystem.ts +0 -0
@@ -4,6 +4,7 @@ import { mkdir, mkdtemp, rm } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { promisify } from 'node:util';
7
+ import AdmZip from 'adm-zip';
7
8
  import express from 'express';
8
9
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
9
10
  import { LocalFilesystem } from '@mastra/core/workspace';
@@ -15,8 +16,9 @@ import { registerWorkspaceTools } from '../workspace.tools.js';
15
16
  import { RoutineWritePolicyService } from '../routine-write-policy.js';
16
17
  import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
17
18
  import { SpillStore } from '../spill-store.js';
19
+ import { DocExtractService } from '../file-readers/doc-extract.service.js';
18
20
  import type { IAccessControl } from '../../access/access-control.interface.js';
19
- import { assertValidBranchName } from '../../workflow/git/branch-name.js';
21
+ import { assertValidBranchName } from '../../kb-fs/branch-name.js';
20
22
 
21
23
  const KB_DIR = 'knowledge-base';
22
24
 
@@ -45,6 +47,8 @@ function denyReads(denied: Set<string>): IAccessControl {
45
47
 
46
48
  let httpServer: HttpServer | undefined;
47
49
  let tempDir = '';
50
+ /** Fresh per-start doc-extraction cache root (OUTSIDE the workspace, like production). */
51
+ let docCacheDir = '';
48
52
  let fs: LocalFilesystem;
49
53
  /**
50
54
  * Every branch / workspaceId `execute_command`'s handler resolves a workspace
@@ -62,12 +66,15 @@ let writePolicy: RoutineWritePolicyService;
62
66
  * branch-less `execute_command` falls back to the session's own workspace.
63
67
  */
64
68
  let focusedBranch: string | undefined;
69
+ /** The registry the tools were mounted into, so a test can inspect their defs. */
70
+ let toolRegistry: ToolRegistry;
65
71
 
66
72
  async function start(
67
73
  scope: 'read' | 'write' = 'write',
68
74
  access: IAccessControl = allowAll,
69
75
  ): Promise<string> {
70
76
  tempDir = await mkdtemp(join(tmpdir(), 'ws-tools-'));
77
+ docCacheDir = await mkdtemp(join(tmpdir(), 'ws-doc-cache-'));
71
78
  fs = new LocalFilesystem({ basePath: tempDir, contained: true });
72
79
  await fs.writeFile('a.md', 'hello\nworld\n');
73
80
  workspacePathCalls = [];
@@ -75,6 +82,7 @@ async function start(
75
82
  focusedBranch = undefined;
76
83
 
77
84
  const registry = new ToolRegistry();
85
+ toolRegistry = registry;
78
86
  const resolve = async (auth: ToolAuth, signal: AbortSignal, sessionId?: string): Promise<ToolContext> => ({
79
87
  user: { id: 'u', email: 'e@x', name: 'N' },
80
88
  scope: auth.scope,
@@ -106,7 +114,7 @@ async function start(
106
114
  const app = express();
107
115
  app.use(express.json());
108
116
  const router = express.Router();
109
- registerWorkspaceTools(registry, router, fakeAuth, toolHandler, new SpillStore(join(tmpdir(), 'bevel-test-spills')), access, KB_DIR, {
117
+ registerWorkspaceTools(registry, router, fakeAuth, toolHandler, new SpillStore(join(tmpdir(), 'bevel-test-spills')), new DocExtractService(docCacheDir), access, KB_DIR, {
110
118
  service: {} as never,
111
119
  enabled: false, // these tests predate and don't exercise the ontology boundary
112
120
  kbDirName: KB_DIR,
@@ -133,6 +141,10 @@ afterEach(async () => {
133
141
  await rm(tempDir, { recursive: true, force: true });
134
142
  tempDir = '';
135
143
  }
144
+ if (docCacheDir) {
145
+ await rm(docCacheDir, { recursive: true, force: true });
146
+ docCacheDir = '';
147
+ }
136
148
  });
137
149
 
138
150
  describe('workspace file primitives', () => {
@@ -416,6 +428,491 @@ describe('read-permission gating', () => {
416
428
  });
417
429
  });
418
430
 
431
+ /**
432
+ * Document reading: read_file/grep consume the doc-extract service for office
433
+ * documents and PDFs; the agent text-editing tools refuse them. Fixtures are
434
+ * REAL files built in-test (a docx is just a zip with word/document.xml).
435
+ */
436
+ describe('office documents and PDFs', () => {
437
+ const docx = (...paragraphs: string[]): Buffer => {
438
+ const zip = new AdmZip();
439
+ zip.addFile('[Content_Types].xml', Buffer.from('<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"/>'));
440
+ zip.addFile(
441
+ 'word/document.xml',
442
+ Buffer.from(
443
+ '<?xml version="1.0"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>' +
444
+ paragraphs.map((p) => `<w:p><w:r><w:t>${p}</w:t></w:r></w:p>`).join('') +
445
+ '</w:body></w:document>',
446
+ ),
447
+ );
448
+ return zip.toBuffer();
449
+ };
450
+
451
+ const pptx = (slides: string[][]): Buffer => {
452
+ const zip = new AdmZip();
453
+ zip.addFile('[Content_Types].xml', Buffer.from('<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"/>'));
454
+ slides.forEach((paragraphs, i) => {
455
+ zip.addFile(
456
+ `ppt/slides/slide${i + 1}.xml`,
457
+ Buffer.from(
458
+ '<?xml version="1.0"?><p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><p:txBody>' +
459
+ paragraphs.map((p) => `<a:p><a:r><a:t>${p}</a:t></a:r></a:p>`).join('') +
460
+ '</p:txBody></p:sld>',
461
+ ),
462
+ );
463
+ });
464
+ return zip.toBuffer();
465
+ };
466
+
467
+ /** Minimal valid one-page PDF with `text` as its text layer. */
468
+ const pdf = (text: string): Buffer => {
469
+ const stream = `BT /F1 12 Tf 72 720 Td (${text}) Tj ET`;
470
+ const objects = [
471
+ '<< /Type /Catalog /Pages 2 0 R >>',
472
+ '<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
473
+ '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>',
474
+ `<< /Length ${stream.length} >>\nstream\n${stream}\nendstream`,
475
+ '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
476
+ ];
477
+ let out = '%PDF-1.4\n';
478
+ const offsets: number[] = [];
479
+ for (let i = 0; i < objects.length; i++) {
480
+ offsets.push(out.length);
481
+ out += `${i + 1} 0 obj\n${objects[i]}\nendobj\n`;
482
+ }
483
+ const xrefStart = out.length;
484
+ out += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`;
485
+ for (const off of offsets) out += `${String(off).padStart(10, '0')} 00000 n \n`;
486
+ out += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefStart}\n%%EOF\n`;
487
+ return Buffer.from(out, 'latin1');
488
+ };
489
+
490
+ /** Minimal ODF package: a zip with a `content.xml` carrying `body` under `<office:body>`. */
491
+ const odf = (body: string): Buffer => {
492
+ const zip = new AdmZip();
493
+ zip.addFile(
494
+ 'content.xml',
495
+ Buffer.from(
496
+ '<?xml version="1.0"?><office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" ' +
497
+ 'xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" ' +
498
+ 'xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:presentation="urn:oasis:names:tc:opendocument:xmlns:presentation:1.0">' +
499
+ `<office:body>${body}</office:body></office:document-content>`,
500
+ ),
501
+ );
502
+ return zip.toBuffer();
503
+ };
504
+
505
+ const odt = (...paragraphs: string[]): Buffer =>
506
+ odf(`<office:text>${paragraphs.map((p) => `<text:p>${p}</text:p>`).join('')}</office:text>`);
507
+
508
+ const odp = (slides: string[][]): Buffer =>
509
+ odf(
510
+ '<office:presentation>' +
511
+ slides
512
+ .map(
513
+ (paragraphs) =>
514
+ `<draw:page><draw:frame><draw:text-box>${paragraphs.map((p) => `<text:p>${p}</text:p>`).join('')}</draw:text-box></draw:frame></draw:page>`,
515
+ )
516
+ .join('') +
517
+ '</office:presentation>',
518
+ );
519
+
520
+ const ods = (name: string, rows: string[][]): Buffer =>
521
+ odf(
522
+ `<office:spreadsheet><table:table table:name="${name}">` +
523
+ rows
524
+ .map((cells) => `<table:table-row>${cells.map((c) => `<table:table-cell><text:p>${c}</text:p></table:table-cell>`).join('')}</table:table-row>`)
525
+ .join('') +
526
+ '</table:table></office:spreadsheet>',
527
+ );
528
+
529
+ const readContent = async (base: string, path: string, extra: Record<string, unknown> = {}): Promise<string> => {
530
+ const res = (await (await post(`${base}/api/agent/tools/read_file`, { path, ...extra })).json()) as { content: string };
531
+ return res.content;
532
+ };
533
+
534
+ it('read_file returns marker + extracted text for a docx', async () => {
535
+ const base = await start();
536
+ await fs.writeFile('report.docx', docx('Hello from Word', 'Second paragraph'));
537
+ const content = await readContent(base, 'report.docx');
538
+ const lines = content.split('\n');
539
+ expect(lines[0]).toMatch(/^\[extracted text of report\.docx — 2 paragraphs;/);
540
+ expect(lines[0]).toContain('layout, images and formatting omitted');
541
+ expect(lines.slice(1)).toEqual(['Hello from Word', 'Second paragraph']);
542
+ });
543
+
544
+ it('read_file slices offset/limit AFTER assembling marker + text (unchanged semantics)', async () => {
545
+ const base = await start();
546
+ await fs.writeFile('report.docx', docx('Sliceable content here'));
547
+ const full = await readContent(base, 'report.docx');
548
+ const sliced = await readContent(base, 'report.docx', { offset: 5, limit: 12 });
549
+ expect(sliced).toBe(full.slice(5, 17));
550
+ });
551
+
552
+ it('read_file returns [page N] text for a PDF', async () => {
553
+ const base = await start();
554
+ await fs.writeFile('paper.pdf', pdf('Findings inside a PDF'));
555
+ const content = await readContent(base, 'paper.pdf');
556
+ expect(content).toMatch(/^\[extracted text of paper\.pdf — 1 page;/);
557
+ expect(content).toContain('[page 1]\nFindings inside a PDF');
558
+ });
559
+
560
+ it('grep finds a term inside a pptx, with the [slide N] marker line locating it', async () => {
561
+ const base = await start();
562
+ await fs.writeFile('deck.pptx', pptx([['Intro'], ['Roadmap 2026', 'Ship documents']]));
563
+ const res = (await (await post(`${base}/api/agent/tools/grep`, { pattern: 'Roadmap' })).json()) as {
564
+ matches: { path: string; line: number; text: string }[];
565
+ note?: string;
566
+ };
567
+ expect(res.matches).toContainEqual(expect.objectContaining({ path: 'deck.pptx', text: 'Roadmap 2026' }));
568
+ // The extraction reads: marker line 1, [slide 1] line 2, Intro line 3,
569
+ // [slide 2] line 4, Roadmap line 5 — grep reports the extraction's numbers.
570
+ expect(res.matches.find((m) => m.text === 'Roadmap 2026')?.line).toBe(5);
571
+ // The structure markers themselves are searchable.
572
+ const markers = (await (await post(`${base}/api/agent/tools/grep`, { pattern: '\\[slide 2\\]' })).json()) as { matches: { path: string }[] };
573
+ expect(markers.matches).toContainEqual(expect.objectContaining({ path: 'deck.pptx' }));
574
+ expect(res.note).toBeUndefined();
575
+ });
576
+
577
+ it('grep extracts at most 20 uncached documents per call and notes the skipped rest; a re-run covers them', async () => {
578
+ const base = await start();
579
+ for (let i = 0; i < 22; i++) {
580
+ // Distinct content per file — identical bytes would share one cache entry
581
+ // and the walk would extract only once.
582
+ await fs.writeFile(`docs/f${String(i).padStart(2, '0')}.docx`, docx(`needle-${i} unique body ${i}`));
583
+ }
584
+ const first = (await (await post(`${base}/api/agent/tools/grep`, { pattern: 'needle-' })).json()) as {
585
+ matches: unknown[];
586
+ note?: string;
587
+ };
588
+ expect(first.matches).toHaveLength(20);
589
+ expect(first.note).toContain('2 document(s) (office/PDF/email files) were not searched');
590
+ // Second run: 20 are cached (free), budget covers the remaining 2.
591
+ const second = (await (await post(`${base}/api/agent/tools/grep`, { pattern: 'needle-' })).json()) as {
592
+ matches: unknown[];
593
+ note?: string;
594
+ };
595
+ expect(second.matches).toHaveLength(22);
596
+ expect(second.note).toBeUndefined();
597
+ });
598
+
599
+ it('read_file returns marker + extracted text for the three OpenDocument formats', async () => {
600
+ const base = await start();
601
+ await fs.writeFile('memo.odt', odt('Hello from Writer', 'Second paragraph'));
602
+ const odtContent = await readContent(base, 'memo.odt');
603
+ expect(odtContent.split('\n')).toEqual([
604
+ '[extracted text of memo.odt — 2 paragraphs; layout, images and formatting omitted]',
605
+ 'Hello from Writer',
606
+ 'Second paragraph',
607
+ ]);
608
+
609
+ await fs.writeFile('deck.odp', odp([['Impress intro'], ['Second page']]));
610
+ const odpContent = await readContent(base, 'deck.odp');
611
+ expect(odpContent).toMatch(/^\[extracted text of deck\.odp — 2 slides;/);
612
+ expect(odpContent).toContain('[slide 1]\nImpress intro\n[slide 2]\nSecond page');
613
+
614
+ await fs.writeFile('numbers.ods', ods('Inventory', [['Name', 'Qty'], ['Widget', '3']]));
615
+ const odsContent = await readContent(base, 'numbers.ods');
616
+ expect(odsContent).toMatch(/^\[extracted text of numbers\.ods — 1 sheet, rows as tab-separated values;/);
617
+ expect(odsContent).toContain('[sheet: Inventory]\nName\tQty\nWidget\t3');
618
+ });
619
+
620
+ it('grep finds a term inside an odp, with the [slide N] marker line locating it', async () => {
621
+ const base = await start();
622
+ await fs.writeFile('deck.odp', odp([['Intro'], ['Roadmap 2027', 'Ship OpenDocument']]));
623
+ const res = (await (await post(`${base}/api/agent/tools/grep`, { pattern: 'Roadmap' })).json()) as {
624
+ matches: { path: string; line: number; text: string }[];
625
+ };
626
+ // Extraction: marker line 1, [slide 1] 2, Intro 3, [slide 2] 4, Roadmap 5.
627
+ expect(res.matches).toContainEqual(expect.objectContaining({ path: 'deck.odp', text: 'Roadmap 2027', line: 5 }));
628
+ const markers = (await (await post(`${base}/api/agent/tools/grep`, { pattern: '\\[slide 2\\]' })).json()) as { matches: { path: string }[] };
629
+ expect(markers.matches).toContainEqual(expect.objectContaining({ path: 'deck.odp' }));
630
+ });
631
+
632
+ it('read_file answers a corrupt odt zip with an honest could-not-parse message (no 500)', async () => {
633
+ const base = await start();
634
+ await fs.writeFile('broken.odt', Buffer.from('not really a zip'));
635
+ const res = await post(`${base}/api/agent/tools/read_file`, { path: 'broken.odt' });
636
+ expect(res.status).toBe(200);
637
+ const { content } = (await res.json()) as { content: string };
638
+ expect(content).toContain('could not be parsed as a .odt');
639
+ expect(content).toContain('uploading a new version');
640
+ });
641
+
642
+ // Hand-written MIME — the email fixtures need no builder library.
643
+ const eml = (subject: string, body: string): Buffer =>
644
+ Buffer.from(
645
+ [
646
+ 'From: Ada Lovelace <ada@example.com>',
647
+ 'To: bob@example.com',
648
+ `Subject: ${subject}`,
649
+ 'Date: Mon, 5 Jan 2026 10:00:00 +0000',
650
+ 'Content-Type: text/plain; charset=utf-8',
651
+ '',
652
+ body,
653
+ ].join('\r\n'),
654
+ );
655
+
656
+ it('read_file returns marker + [from]/[subject] header block + body for a .eml email', async () => {
657
+ const base = await start();
658
+ await fs.writeFile('Inbox/offer.eml', eml('Quarterly numbers', 'Please see the summary.'));
659
+ const content = await readContent(base, 'Inbox/offer.eml');
660
+ expect(content.split('\n')).toEqual([
661
+ '[extracted text of Inbox/offer.eml — email message; formatting and full headers omitted]',
662
+ '[from] Ada Lovelace <ada@example.com>',
663
+ '[to] bob@example.com',
664
+ '[subject] Quarterly numbers',
665
+ '[date] 2026-01-05T10:00:00.000Z',
666
+ '',
667
+ 'Please see the summary.',
668
+ ]);
669
+ });
670
+
671
+ it('grep finds a term inside a .eml, with the [subject] header line itself searchable', async () => {
672
+ const base = await start();
673
+ await fs.writeFile('Inbox/offer.eml', eml('Quarterly numbers', 'The needle-2026 is in the body.'));
674
+ const res = (await (await post(`${base}/api/agent/tools/grep`, { pattern: 'needle-2026' })).json()) as {
675
+ matches: { path: string; line: number; text: string }[];
676
+ };
677
+ // Extraction: marker 1, [from] 2, [to] 3, [subject] 4, [date] 5, blank 6, body 7.
678
+ expect(res.matches).toContainEqual(
679
+ expect.objectContaining({ path: 'Inbox/offer.eml', text: 'The needle-2026 is in the body.', line: 7 }),
680
+ );
681
+ const header = (await (await post(`${base}/api/agent/tools/grep`, { pattern: '\\[subject\\] Quarterly' })).json()) as {
682
+ matches: { path: string; line: number }[];
683
+ };
684
+ expect(header.matches).toContainEqual(expect.objectContaining({ path: 'Inbox/offer.eml', line: 4 }));
685
+ });
686
+
687
+ it('write_file / edit_file refuse email files with the snapshot explanation', async () => {
688
+ const base = await start();
689
+ await fs.writeFile('Inbox/offer.eml', eml('original', 'original body'));
690
+ for (const [tool, body] of [
691
+ ['write_file', { path: 'Inbox/offer.eml', content: 'rewritten' }],
692
+ ['write_file', { path: 'Inbox/new-thread.msg', content: 'plain text' }],
693
+ ['edit_file', { path: 'Inbox/offer.eml', old_string: 'original', new_string: 'changed' }],
694
+ ] as const) {
695
+ const res = await post(`${base}/api/agent/tools/${tool}`, body);
696
+ expect(res.status, tool).toBe(400);
697
+ const { error } = (await res.json()) as { error: string };
698
+ expect(error, tool).toContain('email file');
699
+ expect(error, tool).toContain('snapshot');
700
+ expect(error, tool).toContain('uploading a new version');
701
+ }
702
+ // The email is untouched: reading it still extracts the original text.
703
+ expect(await readContent(base, 'Inbox/offer.eml')).toContain('original body');
704
+ });
705
+
706
+ it('read_file answers a corrupt .msg with an honest could-not-parse message (no 500)', async () => {
707
+ const base = await start();
708
+ await fs.writeFile('Inbox/broken.msg', Buffer.from('not a CFB container'));
709
+ const res = await post(`${base}/api/agent/tools/read_file`, { path: 'Inbox/broken.msg' });
710
+ expect(res.status).toBe(200);
711
+ const { content } = (await res.json()) as { content: string };
712
+ expect(content).toContain('could not be parsed as a .msg');
713
+ expect(content).toContain('uploading a new version');
714
+ });
715
+
716
+ it('read_file answers a corrupt docx with an honest could-not-parse message (no 500)', async () => {
717
+ const base = await start();
718
+ await fs.writeFile('broken.docx', Buffer.from('not really a zip'));
719
+ const res = await post(`${base}/api/agent/tools/read_file`, { path: 'broken.docx' });
720
+ expect(res.status).toBe(200);
721
+ const { content } = (await res.json()) as { content: string };
722
+ expect(content).toContain('could not be parsed as a .docx');
723
+ expect(content).toContain('uploading a new version');
724
+ });
725
+
726
+ it('read_file answers a legacy .doc with the convert-to-modern hint', async () => {
727
+ const base = await start();
728
+ await fs.writeFile('old.doc', Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0x00, 0x01, 0x02]));
729
+ const content = await readContent(base, 'old.doc');
730
+ expect(content).toContain('legacy office format');
731
+ expect(content).toContain('.docx');
732
+ });
733
+
734
+ it('edit_file / write_file refuse a legacy .doc with the convert-or-replace message — a binary the reader cannot extract must never be text-overwritten', async () => {
735
+ const base = await start();
736
+ await fs.writeFile('old.doc', Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0x00, 0x01, 0x02]));
737
+ for (const [tool, body] of [
738
+ ['edit_file', { path: 'old.doc', old_string: 'a', new_string: 'b' }],
739
+ ['write_file', { path: 'old.doc', content: 'plain text' }],
740
+ ] as const) {
741
+ const res = await post(`${base}/api/agent/tools/${tool}`, body);
742
+ expect(res.status, tool).toBe(400);
743
+ const { error } = (await res.json()) as { error: string };
744
+ expect(error, tool).toContain('legacy binary office format');
745
+ expect(error, tool).toContain('.docx');
746
+ expect(error, tool).toContain('uploading a new version');
747
+ }
748
+ });
749
+
750
+ it('write_file / edit_file refuse to overwrite BINARY content under any extension', async () => {
751
+ // read_file answers a NUL-bearing file with a notice instead of its bytes.
752
+ // A write gate that only asked about the EXTENSION let an agent overwrite
753
+ // exactly those bytes — destroying a file it was never allowed to see.
754
+ const base = await start();
755
+ await fs.writeFile('blob.dat', Buffer.from([0x00, 0x01, 0x02, 0x03, 0xff]));
756
+ for (const [tool, body] of [
757
+ ['write_file', { path: 'blob.dat', content: 'plain text' }],
758
+ ['edit_file', { path: 'blob.dat', old_string: 'a', new_string: 'b' }],
759
+ ] as const) {
760
+ const res = await post(`${base}/api/agent/tools/${tool}`, body);
761
+ expect(res.status, tool).toBe(400);
762
+ const { error } = (await res.json()) as { error: string };
763
+ expect(error, tool).toContain('binary content');
764
+ expect(error, tool).toContain('uploading a new version');
765
+ }
766
+ // …and a TEXT file under the same fallback reader still writes normally.
767
+ const ok = await post(`${base}/api/agent/tools/write_file`, { path: 'notes.dat', content: 'hello' });
768
+ expect(ok.status).toBe(200);
769
+ });
770
+
771
+ it('read_file answers other binary files with a one-line notice (zip names the unzip tool)', async () => {
772
+ const base = await start();
773
+ // .mp3, not an image: images return native MCP image content (see below).
774
+ await fs.writeFile('song.mp3', Buffer.from([0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]));
775
+ const mp3 = await readContent(base, 'song.mp3');
776
+ expect(mp3).toBe('[song.mp3 is a binary file (audio/mpeg, 9 bytes) — not readable as text.]');
777
+ await fs.writeFile('bundle.zip', Buffer.from([0x50, 0x4b, 0x03, 0x04, 0x00, 0x00]));
778
+ const zip = await readContent(base, 'bundle.zip');
779
+ expect(zip).toContain('application/zip');
780
+ expect(zip).toContain('unzip tool');
781
+ });
782
+
783
+ it('write_file / edit_file / write_files refuse document extensions with the round-trip explanation', async () => {
784
+ const base = await start();
785
+ await fs.writeFile('deck.pptx', pptx([['Original']]));
786
+ for (const [tool, body] of [
787
+ ['write_file', { path: 'new.docx', content: 'plain text' }],
788
+ ['write_file', { path: 'new.odt', content: 'plain text' }],
789
+ ['write_file', { path: 'slides.odp', content: 'plain text' }],
790
+ ['edit_file', { path: 'deck.pptx', old_string: 'Original', new_string: 'Changed' }],
791
+ ['edit_file', { path: 'numbers.ods', old_string: 'a', new_string: 'b' }],
792
+ ['write_files', { files: [{ path: 'ok.md', content: 'fine' }, { path: 'sheet.xlsx', content: 'nope' }] }],
793
+ ['write_files', { files: [{ path: 'ok.md', content: 'fine' }, { path: 'sheet.ods', content: 'nope' }] }],
794
+ ] as const) {
795
+ const res = await post(`${base}/api/agent/tools/${tool}`, body);
796
+ expect(res.status, tool).toBe(400);
797
+ const { error } = (await res.json()) as { error: string };
798
+ expect(error, tool).toContain('EXTRACTED text');
799
+ expect(error, tool).toContain('uploading a new version');
800
+ }
801
+ // The batch was refused atomically — the innocent .md was not written either.
802
+ expect((await post(`${base}/api/agent/tools/read_file`, { path: 'ok.md' })).status).not.toBe(200);
803
+ // And the pptx is untouched: reading it still extracts the original text.
804
+ expect(await readContent(base, 'deck.pptx')).toContain('Original');
805
+ });
806
+
807
+ it('the write tools DESCRIBE every refused family — modern, legacy binary, and the upload path — so agents learn before the call', async () => {
808
+ await start();
809
+ const tools = await toolRegistry.listInternal();
810
+ for (const name of ['write_file', 'write_files', 'edit_file']) {
811
+ const def = tools.find((t) => t.name === name);
812
+ expect(def, name).toBeDefined();
813
+ // Modern extractable formats…
814
+ expect(def!.description, name).toContain('.docx/.pptx/.xlsx/.odt/.odp/.ods/.pdf');
815
+ // …email files (extractions too, so the same refusal applies)…
816
+ expect(def!.description, name).toContain('.eml/.msg');
817
+ // …the legacy binary family the refusal also covers…
818
+ expect(def!.description, name).toContain('.doc/.ppt/.xls');
819
+ // …and the replace-by-upload way out.
820
+ expect(def!.description, name).toContain('uploading a new version');
821
+ }
822
+ });
823
+ });
824
+
825
+ /**
826
+ * Image reads: `read_file` on an image returns the `McpImageResult` sentinel
827
+ * (base64 + mimeType + self-describing note) that the MCP result shaping turns
828
+ * into a native image content block — never raw bytes and never the binary
829
+ * notice. Real fixtures: a genuine 1×1 PNG and 1×1 GIF.
830
+ */
831
+ describe('images', () => {
832
+ /** A real, complete 1×1 transparent PNG. */
833
+ const PNG_1X1 = Buffer.from(
834
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
835
+ 'base64',
836
+ );
837
+ /** A real, complete 1×1 GIF89a. */
838
+ const GIF_1X1 = Buffer.from('R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', 'base64');
839
+
840
+ interface ImageSentinel {
841
+ kind: string;
842
+ data: string;
843
+ mimeType: string;
844
+ note: string;
845
+ }
846
+
847
+ it('read_file on a png returns the image sentinel with base64, mimeType and a note naming path + dimensions + size', async () => {
848
+ const base = await start();
849
+ await fs.writeFile('logo.png', PNG_1X1);
850
+ const res = await post(`${base}/api/agent/tools/read_file`, { path: 'logo.png' });
851
+ expect(res.status).toBe(200);
852
+ const body = (await res.json()) as ImageSentinel;
853
+ expect(body.kind).toBe('bevel/mcp-image@v1');
854
+ expect(body.mimeType).toBe('image/png');
855
+ expect(body.data).toBe(PNG_1X1.toString('base64'));
856
+ expect(body.note).toContain('logo.png');
857
+ expect(body.note).toContain('image/png');
858
+ expect(body.note).toContain(`${PNG_1X1.length} bytes`);
859
+ expect(body.note).toContain('1×1 px');
860
+ });
861
+
862
+ it('read_file passes a gif through whole under the same cap (first frame is the client’s concern)', async () => {
863
+ const base = await start();
864
+ await fs.writeFile('anim.gif', GIF_1X1);
865
+ const body = (await (await post(`${base}/api/agent/tools/read_file`, { path: 'anim.gif' })).json()) as ImageSentinel;
866
+ expect(body.kind).toBe('bevel/mcp-image@v1');
867
+ expect(body.mimeType).toBe('image/gif');
868
+ expect(body.data).toBe(GIF_1X1.toString('base64'));
869
+ expect(body.note).toContain('1×1 px');
870
+ });
871
+
872
+ it('read_file maps .jpg/.jpeg to image/jpeg (dimensions omitted when the header has none to give)', async () => {
873
+ const base = await start();
874
+ const bytes = Buffer.from([0xff, 0xd8, 0xff, 0xd9]); // SOI + EOI, no frame header
875
+ await fs.writeFile('photo.jpg', bytes);
876
+ const body = (await (await post(`${base}/api/agent/tools/read_file`, { path: 'photo.jpg' })).json()) as ImageSentinel;
877
+ expect(body.mimeType).toBe('image/jpeg');
878
+ expect(body.data).toBe(bytes.toString('base64'));
879
+ expect(body.note).toBe(`[image: photo.jpg — image/jpeg, ${bytes.length} bytes]`);
880
+ });
881
+
882
+ it('read_file refuses an image over 3.5 MiB raw with the downscale message, not a sentinel', async () => {
883
+ const base = await start();
884
+ // 3,670,016 is the cap; one byte over must refuse. PNG magic + zero fill.
885
+ const big = Buffer.alloc(3_670_017);
886
+ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]).copy(big);
887
+ await fs.writeFile('huge.png', big);
888
+ const res = await post(`${base}/api/agent/tools/read_file`, { path: 'huge.png' });
889
+ expect(res.status).toBe(200);
890
+ const body = (await res.json()) as { path: string; content: string };
891
+ expect(body.path).toBe('huge.png');
892
+ expect(body.content).toContain('too large to return over MCP');
893
+ expect(body.content).toContain('3670016 bytes');
894
+ expect(body.content).toContain('Downscale');
895
+ expect(body.content).not.toContain(big.toString('base64').slice(0, 40));
896
+ });
897
+
898
+ it('read_file keeps .svg on the TEXT path — it is markup, not an image block', async () => {
899
+ const base = await start();
900
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"><rect width="8" height="8"/></svg>';
901
+ await fs.writeFile('icon.svg', svg);
902
+ expect(await (await post(`${base}/api/agent/tools/read_file`, { path: 'icon.svg' })).json()).toEqual({
903
+ path: 'icon.svg',
904
+ content: svg,
905
+ });
906
+ });
907
+
908
+ it('read_file on an image still honors the read gate (403 before any bytes are returned)', async () => {
909
+ const base = await start('write', denyReads(new Set(['Knowledge/Secret.png'])));
910
+ await fs.writeFile(`${KB_DIR}/Knowledge/Secret.png`, PNG_1X1);
911
+ const res = await post(`${base}/api/agent/tools/read_file`, { path: `${KB_DIR}/Knowledge/Secret.png` });
912
+ expect(res.status).toBe(403);
913
+ });
914
+ });
915
+
419
916
  /**
420
917
  * start_session must mint a REAL chat thread and return its id (not a bare
421
918
  * random UUID), so the same id works for KB reads AND for `ask` (whose
@@ -458,7 +955,7 @@ describe('start_session', () => {
458
955
  const router = express.Router();
459
956
  registerWorkspaceTools(
460
957
  registry, router, auth, toolHandler,
461
- new SpillStore(join(tmpdir(), 'bevel-test-spills')), allowAll, KB_DIR,
958
+ new SpillStore(join(tmpdir(), 'bevel-test-spills')), new DocExtractService(join(tmpdir(), 'bevel-test-doc-extract')), allowAll, KB_DIR,
462
959
  { service: {} as never, enabled: false, kbDirName: KB_DIR, recoveryBotEmail: 'recovery-bot@bevel.local', hooks: new WorkflowHooks() },
463
960
  new RoutineWritePolicyService(),
464
961
  fakeSessionSink,
@@ -522,6 +1019,7 @@ describe('branch is a required parameter in the tool contract', () => {
522
1019
  noopAuth,
523
1020
  toolHandler,
524
1021
  new SpillStore(join(tmpdir(), 'bevel-test-spills')),
1022
+ new DocExtractService(join(tmpdir(), 'bevel-test-doc-extract')),
525
1023
  allowAll,
526
1024
  KB_DIR,
527
1025
  { service: {} as never, enabled: false, kbDirName: KB_DIR, recoveryBotEmail: 'recovery-bot@bevel.local', hooks: new WorkflowHooks() },