@bevel-software/platform-core-backend 0.33.0 → 0.34.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 (497) hide show
  1. package/agent-guide/access-control.md +19 -8
  2. package/agent-guide/html-views.md +231 -5
  3. package/agent-guide/tool-manuals.md +1 -1
  4. package/dist/core/catalog-revision.d.ts +12 -1
  5. package/dist/core/catalog-revision.d.ts.map +1 -1
  6. package/dist/core/catalog-revision.js +23 -3
  7. package/dist/core/catalog-revision.js.map +1 -1
  8. package/dist/core/core-ports.d.ts +41 -0
  9. package/dist/core/core-ports.d.ts.map +1 -1
  10. package/dist/core/core-ports.js.map +1 -1
  11. package/dist/core/create-core-server.d.ts.map +1 -1
  12. package/dist/core/create-core-server.js +14 -7
  13. package/dist/core/create-core-server.js.map +1 -1
  14. package/dist/core/create-core-services.d.ts +16 -5
  15. package/dist/core/create-core-services.d.ts.map +1 -1
  16. package/dist/core/create-core-services.js +37 -12
  17. package/dist/core/create-core-services.js.map +1 -1
  18. package/dist/core/lifecycle.d.ts +16 -0
  19. package/dist/core/lifecycle.d.ts.map +1 -1
  20. package/dist/core/lifecycle.js +12 -2
  21. package/dist/core/lifecycle.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/modules/access/access-control.interface.d.ts +17 -9
  26. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  27. package/dist/modules/access/access-control.service.d.ts +12 -2
  28. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  29. package/dist/modules/access/access-control.service.js +164 -75
  30. package/dist/modules/access/access-control.service.js.map +1 -1
  31. package/dist/modules/access/access-mutation.service.d.ts +22 -24
  32. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  33. package/dist/modules/access/access-mutation.service.js +48 -51
  34. package/dist/modules/access/access-mutation.service.js.map +1 -1
  35. package/dist/modules/access/access-view.d.ts.map +1 -1
  36. package/dist/modules/access/access-view.js +12 -11
  37. package/dist/modules/access/access-view.js.map +1 -1
  38. package/dist/modules/access/access.routes.d.ts +3 -0
  39. package/dist/modules/access/access.routes.d.ts.map +1 -1
  40. package/dist/modules/access/access.routes.js +35 -29
  41. package/dist/modules/access/access.routes.js.map +1 -1
  42. package/dist/modules/access/groups-admin.routes.d.ts.map +1 -1
  43. package/dist/modules/access/groups-admin.routes.js +4 -2
  44. package/dist/modules/access/groups-admin.routes.js.map +1 -1
  45. package/dist/modules/access/groups-admin.service.d.ts +44 -13
  46. package/dist/modules/access/groups-admin.service.d.ts.map +1 -1
  47. package/dist/modules/access/groups-admin.service.js +80 -38
  48. package/dist/modules/access/groups-admin.service.js.map +1 -1
  49. package/dist/modules/access/groups-edit.d.ts +1 -1
  50. package/dist/modules/access/groups-edit.d.ts.map +1 -1
  51. package/dist/modules/access/groups-edit.js +1 -1
  52. package/dist/modules/access/groups-edit.js.map +1 -1
  53. package/dist/modules/access/reference-scan.d.ts +116 -24
  54. package/dist/modules/access/reference-scan.d.ts.map +1 -1
  55. package/dist/modules/access/reference-scan.js +305 -44
  56. package/dist/modules/access/reference-scan.js.map +1 -1
  57. package/dist/modules/access/roles-admin.service.d.ts +17 -8
  58. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  59. package/dist/modules/access/roles-admin.service.js +32 -23
  60. package/dist/modules/access/roles-admin.service.js.map +1 -1
  61. package/dist/modules/access-model/access-grammar.d.ts +35 -10
  62. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  63. package/dist/modules/access-model/access-grammar.js +60 -20
  64. package/dist/modules/access-model/access-grammar.js.map +1 -1
  65. package/dist/modules/access-model/access-splice.d.ts +22 -18
  66. package/dist/modules/access-model/access-splice.d.ts.map +1 -1
  67. package/dist/modules/access-model/access-splice.js +20 -14
  68. package/dist/modules/access-model/access-splice.js.map +1 -1
  69. package/dist/modules/access-model/group-files.d.ts +1 -0
  70. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  71. package/dist/modules/access-model/group-files.js +5 -1
  72. package/dist/modules/access-model/group-files.js.map +1 -1
  73. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -1
  74. package/dist/modules/agent-guide/agent-guide.tools.js +13 -3
  75. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -1
  76. package/dist/modules/agent-instructions/compose.d.ts +3 -1
  77. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  78. package/dist/modules/agent-instructions/compose.js +3 -1
  79. package/dist/modules/agent-instructions/compose.js.map +1 -1
  80. package/dist/modules/agent-instructions/shared-file-rules.d.ts +5 -1
  81. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  82. package/dist/modules/agent-instructions/shared-file-rules.js +16 -1
  83. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  84. package/dist/modules/agent-scope/agent-scope.contract.d.ts +67 -0
  85. package/dist/modules/agent-scope/agent-scope.contract.d.ts.map +1 -0
  86. package/dist/modules/agent-scope/agent-scope.contract.js +27 -0
  87. package/dist/modules/agent-scope/agent-scope.contract.js.map +1 -0
  88. package/dist/modules/agent-scope/connection-scope.d.ts +111 -0
  89. package/dist/modules/agent-scope/connection-scope.d.ts.map +1 -0
  90. package/dist/modules/agent-scope/connection-scope.js +165 -0
  91. package/dist/modules/agent-scope/connection-scope.js.map +1 -0
  92. package/dist/modules/agent-scope/index.d.ts +3 -0
  93. package/dist/modules/agent-scope/index.d.ts.map +1 -0
  94. package/dist/modules/agent-scope/index.js +3 -0
  95. package/dist/modules/agent-scope/index.js.map +1 -0
  96. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  97. package/dist/modules/audit/agent-audit.service.js +2 -0
  98. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  99. package/dist/modules/audit/audit.contract.d.ts +6 -0
  100. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  101. package/dist/modules/audit/audit.contract.js.map +1 -1
  102. package/dist/modules/auth/account-admission.d.ts +12 -0
  103. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  104. package/dist/modules/auth/account-admission.js +15 -0
  105. package/dist/modules/auth/account-admission.js.map +1 -1
  106. package/dist/modules/auth/auth.middleware.d.ts +27 -1
  107. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  108. package/dist/modules/auth/auth.middleware.js +42 -4
  109. package/dist/modules/auth/auth.middleware.js.map +1 -1
  110. package/dist/modules/auth/auth.routes.d.ts +13 -9
  111. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  112. package/dist/modules/auth/auth.routes.js +35 -29
  113. package/dist/modules/auth/auth.routes.js.map +1 -1
  114. package/dist/modules/auth/auth.service.d.ts +55 -6
  115. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  116. package/dist/modules/auth/auth.service.js +128 -34
  117. package/dist/modules/auth/auth.service.js.map +1 -1
  118. package/dist/modules/auth/index.d.ts +1 -1
  119. package/dist/modules/auth/index.d.ts.map +1 -1
  120. package/dist/modules/auth/index.js +1 -1
  121. package/dist/modules/auth/index.js.map +1 -1
  122. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  123. package/dist/modules/auth/oidc-auth-provider.js +2 -9
  124. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  125. package/dist/modules/database/core-schema.d.ts +68 -0
  126. package/dist/modules/database/core-schema.d.ts.map +1 -1
  127. package/dist/modules/database/core-schema.js +33 -0
  128. package/dist/modules/database/core-schema.js.map +1 -1
  129. package/dist/modules/declared-variables/declared-variables.routes.d.ts +8 -1
  130. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  131. package/dist/modules/declared-variables/declared-variables.routes.js +13 -2
  132. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  133. package/dist/modules/embed/embed.tools.d.ts.map +1 -1
  134. package/dist/modules/embed/embed.tools.js +5 -0
  135. package/dist/modules/embed/embed.tools.js.map +1 -1
  136. package/dist/modules/kb-fs/index.d.ts +2 -0
  137. package/dist/modules/kb-fs/index.d.ts.map +1 -1
  138. package/dist/modules/kb-fs/index.js +1 -0
  139. package/dist/modules/kb-fs/index.js.map +1 -1
  140. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  141. package/dist/modules/kb-fs/locking-filesystem.js +7 -1
  142. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  143. package/dist/modules/kb-fs/repo-path.d.ts +26 -6
  144. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  145. package/dist/modules/kb-fs/repo-path.js +22 -7
  146. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  147. package/dist/modules/kb-fs/tmp-ref.d.ts +36 -0
  148. package/dist/modules/kb-fs/tmp-ref.d.ts.map +1 -0
  149. package/dist/modules/kb-fs/tmp-ref.js +76 -0
  150. package/dist/modules/kb-fs/tmp-ref.js.map +1 -0
  151. package/dist/modules/mcp/mcp-auth.middleware.d.ts +7 -0
  152. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  153. package/dist/modules/mcp/mcp-auth.middleware.js +14 -1
  154. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  155. package/dist/modules/mcp/mcp.routes.d.ts +7 -1
  156. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  157. package/dist/modules/mcp/mcp.routes.js +39 -4
  158. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  159. package/dist/modules/mcp/mcp.service.d.ts +11 -0
  160. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  161. package/dist/modules/mcp/mcp.service.js +68 -13
  162. package/dist/modules/mcp/mcp.service.js.map +1 -1
  163. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -1
  164. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  165. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +45 -10
  166. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  167. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +9 -0
  168. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  169. package/dist/modules/mcp/oauth/oauth-consent.routes.js +34 -1
  170. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  171. package/dist/modules/plugins/index.d.ts +1 -1
  172. package/dist/modules/plugins/index.d.ts.map +1 -1
  173. package/dist/modules/plugins/index.js.map +1 -1
  174. package/dist/modules/plugins/plugin-links.d.ts +18 -2
  175. package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
  176. package/dist/modules/plugins/plugin-links.js.map +1 -1
  177. package/dist/modules/plugins/plugins.routes.d.ts +5 -1
  178. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  179. package/dist/modules/plugins/plugins.routes.js +32 -5
  180. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  181. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +9 -0
  182. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  183. package/dist/modules/secrets-vault/secrets-vault.routes.js +15 -3
  184. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  185. package/dist/modules/skills/index.d.ts +3 -3
  186. package/dist/modules/skills/index.d.ts.map +1 -1
  187. package/dist/modules/skills/index.js +0 -2
  188. package/dist/modules/skills/index.js.map +1 -1
  189. package/dist/modules/skills/skills.contract.d.ts +16 -0
  190. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  191. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  192. package/dist/modules/skills/skills.service.js +6 -1
  193. package/dist/modules/skills/skills.service.js.map +1 -1
  194. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  195. package/dist/modules/skills/skills.tools.js +24 -12
  196. package/dist/modules/skills/skills.tools.js.map +1 -1
  197. package/dist/modules/tool-auth/external-api-key.interface.d.ts +11 -0
  198. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  199. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  200. package/dist/modules/tool-auth/external-api-key.service.d.ts +1 -0
  201. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  202. package/dist/modules/tool-auth/external-api-key.service.js +4 -1
  203. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  204. package/dist/modules/tool-auth/index.d.ts +1 -1
  205. package/dist/modules/tool-auth/index.d.ts.map +1 -1
  206. package/dist/modules/tool-auth/internal-token.service.d.ts +9 -0
  207. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  208. package/dist/modules/tool-auth/internal-token.service.js +1 -0
  209. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  210. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +27 -3
  211. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  212. package/dist/modules/tool-auth/tool-auth.middleware.js +32 -6
  213. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  214. package/dist/modules/tool-helpers/tool-context.d.ts +7 -0
  215. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  216. package/dist/modules/tool-helpers/tool-context.js +3 -0
  217. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  218. package/dist/modules/tool-helpers/tool-handler.d.ts +15 -0
  219. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  220. package/dist/modules/tool-helpers/tool-handler.js +19 -0
  221. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  222. package/dist/modules/tool-helpers/tool.contract.d.ts +10 -0
  223. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  224. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  225. package/dist/modules/tool-helpers/validate-token.d.ts +3 -1
  226. package/dist/modules/tool-helpers/validate-token.d.ts.map +1 -1
  227. package/dist/modules/tool-helpers/validate-token.js +1 -1
  228. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  229. package/dist/modules/tool-manuals/index.d.ts +3 -3
  230. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  231. package/dist/modules/tool-manuals/index.js +0 -2
  232. package/dist/modules/tool-manuals/index.js.map +1 -1
  233. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +19 -6
  234. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  235. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +7 -1
  236. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  237. package/dist/modules/tool-manuals/tool-manuals.routes.js +41 -4
  238. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  239. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +13 -6
  240. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  241. package/dist/modules/tool-manuals/tool-manuals.service.js +23 -15
  242. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  243. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  244. package/dist/modules/tool-manuals/tool-manuals.tools.js +25 -10
  245. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  246. package/dist/modules/tool-registry/manual.routes.d.ts +8 -1
  247. package/dist/modules/tool-registry/manual.routes.d.ts.map +1 -1
  248. package/dist/modules/tool-registry/manual.routes.js +27 -7
  249. package/dist/modules/tool-registry/manual.routes.js.map +1 -1
  250. package/dist/modules/tool-registry/tool.contract.d.ts +7 -0
  251. package/dist/modules/tool-registry/tool.contract.d.ts.map +1 -1
  252. package/dist/modules/tool-registry/tool.contract.js.map +1 -1
  253. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  254. package/dist/modules/workflow/agent-tools/workflow.tools.js +14 -9
  255. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  256. package/dist/modules/workflow/git/git.service.d.ts +2 -2
  257. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  258. package/dist/modules/workflow/git/git.service.js +20 -35
  259. package/dist/modules/workflow/git/git.service.js.map +1 -1
  260. package/dist/modules/workflow/workflow.service.d.ts +2 -1
  261. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  262. package/dist/modules/workflow/workflow.service.js +3 -0
  263. package/dist/modules/workflow/workflow.service.js.map +1 -1
  264. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +28 -4
  265. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  266. package/dist/modules/workspace/file-readers/doc-extract.service.js +61 -40
  267. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  268. package/dist/modules/workspace/index.d.ts +8 -0
  269. package/dist/modules/workspace/index.d.ts.map +1 -1
  270. package/dist/modules/workspace/index.js +8 -0
  271. package/dist/modules/workspace/index.js.map +1 -1
  272. package/dist/modules/workspace/nul-scan.d.ts +19 -0
  273. package/dist/modules/workspace/nul-scan.d.ts.map +1 -0
  274. package/dist/modules/workspace/nul-scan.js +55 -0
  275. package/dist/modules/workspace/nul-scan.js.map +1 -0
  276. package/dist/modules/workspace/pattern-matcher.contract.d.ts +110 -27
  277. package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -1
  278. package/dist/modules/workspace/pattern-matcher.contract.js +30 -13
  279. package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -1
  280. package/dist/modules/workspace/ripgrep-search.d.ts +54 -0
  281. package/dist/modules/workspace/ripgrep-search.d.ts.map +1 -0
  282. package/dist/modules/workspace/ripgrep-search.js +657 -0
  283. package/dist/modules/workspace/ripgrep-search.js.map +1 -0
  284. package/dist/modules/workspace/ripgrep-version.d.ts +15 -0
  285. package/dist/modules/workspace/ripgrep-version.d.ts.map +1 -0
  286. package/dist/modules/workspace/ripgrep-version.js +31 -0
  287. package/dist/modules/workspace/ripgrep-version.js.map +1 -0
  288. package/dist/modules/workspace/startup/beside-checkout.d.ts +8 -5
  289. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -1
  290. package/dist/modules/workspace/startup/beside-checkout.js +14 -5
  291. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -1
  292. package/dist/modules/workspace/worker-pool.d.ts +3 -4
  293. package/dist/modules/workspace/worker-pool.d.ts.map +1 -1
  294. package/dist/modules/workspace/worker-pool.js.map +1 -1
  295. package/dist/modules/workspace/workspace-tmp.d.ts +146 -0
  296. package/dist/modules/workspace/workspace-tmp.d.ts.map +1 -0
  297. package/dist/modules/workspace/workspace-tmp.js +435 -0
  298. package/dist/modules/workspace/workspace-tmp.js.map +1 -0
  299. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  300. package/dist/modules/workspace/workspace.routes.js +5 -26
  301. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  302. package/dist/modules/workspace/workspace.service.d.ts +47 -6
  303. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  304. package/dist/modules/workspace/workspace.service.js +86 -6
  305. package/dist/modules/workspace/workspace.service.js.map +1 -1
  306. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  307. package/dist/modules/workspace/workspace.tools.js +460 -116
  308. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  309. package/dist/shared/path-order.d.ts +28 -8
  310. package/dist/shared/path-order.d.ts.map +1 -1
  311. package/dist/shared/path-order.js +37 -8
  312. package/dist/shared/path-order.js.map +1 -1
  313. package/migrations/0022_session_version.sql +6 -0
  314. package/migrations/0023_agent_path.sql +3 -0
  315. package/migrations/meta/0022_snapshot.json +2360 -0
  316. package/migrations/meta/0023_snapshot.json +2378 -0
  317. package/migrations/meta/_journal.json +14 -0
  318. package/package.json +3 -3
  319. package/src/__tests__/pg-scratch.test.ts +69 -0
  320. package/src/__tests__/pg-scratch.ts +31 -0
  321. package/src/__tests__/slow-ripgrep.ts +65 -0
  322. package/src/core/__tests__/boot-without-ripgrep.test.ts +34 -0
  323. package/src/core/__tests__/catalog-revision.test.ts +89 -0
  324. package/src/core/__tests__/damaged-document.tools.test.ts +2 -2
  325. package/src/core/__tests__/gated-boot.test.ts +8 -1
  326. package/src/core/__tests__/lifecycle.test.ts +46 -13
  327. package/src/core/catalog-revision.ts +24 -2
  328. package/src/core/core-ports.ts +42 -0
  329. package/src/core/create-core-server.ts +13 -0
  330. package/src/core/create-core-services.ts +49 -12
  331. package/src/core/lifecycle.ts +26 -2
  332. package/src/index.ts +1 -1
  333. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +12 -0
  334. package/src/modules/access/__tests__/access-control.colliding-name-cases.test.ts +113 -0
  335. package/src/modules/access/__tests__/access-control.frontmatter-head.test.ts +117 -0
  336. package/src/modules/access/__tests__/access-control.group-named-admin.test.ts +122 -0
  337. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +50 -0
  338. package/src/modules/access/__tests__/access-declarations.test.ts +19 -0
  339. package/src/modules/access/__tests__/access-groups.test.ts +55 -22
  340. package/src/modules/access/__tests__/access-mutation.service.test.ts +85 -59
  341. package/src/modules/access/__tests__/access-restrict-here.test.ts +7 -5
  342. package/src/modules/access/__tests__/access.routes.admins.test.ts +1 -0
  343. package/src/modules/access/__tests__/access.routes.batch.test.ts +2 -0
  344. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +2 -0
  345. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +50 -14
  346. package/src/modules/access/__tests__/access.routes.overrides.test.ts +2 -1
  347. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +1 -0
  348. package/src/modules/access/__tests__/access.routes.prospective.test.ts +2 -0
  349. package/src/modules/access/__tests__/access.routes.revoke.test.ts +18 -13
  350. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
  351. package/src/modules/access/__tests__/grant-sources.test.ts +20 -15
  352. package/src/modules/access/__tests__/groups-admin.routes.test.ts +61 -0
  353. package/src/modules/access/__tests__/groups-admin.service.test.ts +211 -18
  354. package/src/modules/access/__tests__/reference-scan.test.ts +366 -14
  355. package/src/modules/access/__tests__/roles-admin.security.test.ts +1 -1
  356. package/src/modules/access/__tests__/roles-admin.service.test.ts +43 -22
  357. package/src/modules/access/__tests__/roles-capabilities.test.ts +6 -2
  358. package/src/modules/access/__tests__/roles.routes.test.ts +1 -1
  359. package/src/modules/access/access-control.interface.ts +17 -10
  360. package/src/modules/access/access-control.service.ts +158 -72
  361. package/src/modules/access/access-mutation.service.ts +48 -53
  362. package/src/modules/access/access-view.ts +12 -10
  363. package/src/modules/access/access.routes.ts +43 -33
  364. package/src/modules/access/groups-admin.routes.ts +4 -2
  365. package/src/modules/access/groups-admin.service.ts +114 -44
  366. package/src/modules/access/groups-edit.ts +1 -1
  367. package/src/modules/access/reference-scan.ts +319 -46
  368. package/src/modules/access/roles-admin.service.ts +43 -26
  369. package/src/modules/access/synced-groups-writer.ts +1 -1
  370. package/src/modules/access-model/__tests__/access-grammar.test.ts +38 -1
  371. package/src/modules/access-model/__tests__/access-splice.test.ts +50 -8
  372. package/src/modules/access-model/access-grammar.ts +65 -20
  373. package/src/modules/access-model/access-splice.ts +31 -23
  374. package/src/modules/access-model/group-files.ts +5 -0
  375. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +31 -0
  376. package/src/modules/agent-guide/agent-guide.tools.ts +13 -2
  377. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +1 -0
  378. package/src/modules/agent-instructions/compose.ts +3 -1
  379. package/src/modules/agent-instructions/shared-file-rules.ts +17 -1
  380. package/src/modules/agent-scope/__tests__/agent-surfaces.connect-as.test.ts +421 -0
  381. package/src/modules/agent-scope/__tests__/connection-scope.test.ts +46 -0
  382. package/src/modules/agent-scope/__tests__/in-memory-kb.ts +148 -0
  383. package/src/modules/agent-scope/agent-scope.contract.ts +83 -0
  384. package/src/modules/agent-scope/connection-scope.ts +199 -0
  385. package/src/modules/agent-scope/index.ts +15 -0
  386. package/src/modules/audit/agent-audit.service.ts +2 -0
  387. package/src/modules/audit/audit.contract.ts +6 -0
  388. package/src/modules/auth/__tests__/auth.middleware.test.ts +30 -2
  389. package/src/modules/auth/__tests__/auth.routes.test.ts +71 -5
  390. package/src/modules/auth/__tests__/auth.service.test.ts +90 -9
  391. package/src/modules/auth/__tests__/password-change-sessions.pg.test.ts +269 -0
  392. package/src/modules/auth/account-admission.ts +19 -0
  393. package/src/modules/auth/auth.middleware.ts +45 -4
  394. package/src/modules/auth/auth.routes.ts +46 -29
  395. package/src/modules/auth/auth.service.ts +135 -34
  396. package/src/modules/auth/index.ts +2 -0
  397. package/src/modules/auth/oidc-auth-provider.ts +2 -9
  398. package/src/modules/database/__tests__/session-version-migration.pg.test.ts +133 -0
  399. package/src/modules/database/core-schema.ts +33 -0
  400. package/src/modules/declared-variables/__tests__/declared-variables.route.test.ts +44 -1
  401. package/src/modules/declared-variables/declared-variables.routes.ts +12 -1
  402. package/src/modules/embed/__tests__/embed.tools.test.ts +12 -0
  403. package/src/modules/embed/embed.tools.ts +5 -0
  404. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +25 -0
  405. package/src/modules/kb-fs/__tests__/repo-path.test.ts +53 -8
  406. package/src/modules/kb-fs/index.ts +2 -0
  407. package/src/modules/kb-fs/locking-filesystem.ts +7 -1
  408. package/src/modules/kb-fs/repo-path.ts +33 -7
  409. package/src/modules/kb-fs/tmp-ref.ts +81 -0
  410. package/src/modules/mcp/__tests__/connect-as.pg.test.ts +137 -0
  411. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +29 -5
  412. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +8 -0
  413. package/src/modules/mcp/__tests__/mcp.connect-as.test.ts +195 -0
  414. package/src/modules/mcp/__tests__/mcp.routes.connect-as.test.ts +192 -0
  415. package/src/modules/mcp/__tests__/oauth-consent.connect-as.test.ts +146 -0
  416. package/src/modules/mcp/mcp-auth.middleware.ts +19 -2
  417. package/src/modules/mcp/mcp.routes.ts +39 -3
  418. package/src/modules/mcp/mcp.service.ts +107 -17
  419. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +46 -8
  420. package/src/modules/mcp/oauth/oauth-consent.routes.ts +41 -1
  421. package/src/modules/plugins/__tests__/plugins.tools.test.ts +38 -2
  422. package/src/modules/plugins/index.ts +1 -1
  423. package/src/modules/plugins/plugin-links.ts +19 -2
  424. package/src/modules/plugins/plugins.routes.ts +35 -4
  425. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +113 -0
  426. package/src/modules/secrets-vault/secrets-vault.routes.ts +23 -3
  427. package/src/modules/skills/__tests__/skills.service.test.ts +34 -0
  428. package/src/modules/skills/index.ts +7 -2
  429. package/src/modules/skills/skills.contract.ts +17 -0
  430. package/src/modules/skills/skills.service.ts +5 -1
  431. package/src/modules/skills/skills.tools.ts +44 -13
  432. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +4 -0
  433. package/src/modules/tool-auth/external-api-key.interface.ts +11 -1
  434. package/src/modules/tool-auth/external-api-key.service.ts +5 -2
  435. package/src/modules/tool-auth/index.ts +1 -1
  436. package/src/modules/tool-auth/internal-token.service.ts +10 -0
  437. package/src/modules/tool-auth/tool-auth.middleware.ts +45 -3
  438. package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +3 -1
  439. package/src/modules/tool-helpers/tool-context.ts +10 -0
  440. package/src/modules/tool-helpers/tool-handler.ts +34 -0
  441. package/src/modules/tool-helpers/tool.contract.ts +10 -0
  442. package/src/modules/tool-helpers/validate-token.ts +4 -2
  443. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +80 -4
  444. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +42 -0
  445. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
  446. package/src/modules/tool-manuals/index.ts +6 -2
  447. package/src/modules/tool-manuals/tool-manuals.contract.ts +18 -6
  448. package/src/modules/tool-manuals/tool-manuals.routes.ts +40 -3
  449. package/src/modules/tool-manuals/tool-manuals.service.ts +31 -15
  450. package/src/modules/tool-manuals/tool-manuals.tools.ts +28 -11
  451. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +2 -2
  452. package/src/modules/tool-registry/manual.routes.ts +26 -5
  453. package/src/modules/tool-registry/tool.contract.ts +7 -0
  454. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  455. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +1 -1
  456. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +54 -3
  457. package/src/modules/workflow/agent-tools/workflow.tools.ts +14 -9
  458. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +130 -3
  459. package/src/modules/workflow/git/git.service.ts +29 -48
  460. package/src/modules/workflow/workflow.service.ts +5 -0
  461. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -2
  462. package/src/modules/workspace/__tests__/holds-nul.test.ts +66 -0
  463. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +3 -3
  464. package/src/modules/workspace/__tests__/ripgrep-search.test.ts +570 -0
  465. package/src/modules/workspace/__tests__/ripgrep-version.test.ts +29 -0
  466. package/src/modules/workspace/__tests__/workspace-tmp.tools.test.ts +928 -0
  467. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +17 -37
  468. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +20 -8
  469. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +2 -2
  470. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +22 -10
  471. package/src/modules/workspace/__tests__/workspace.tools.test.ts +393 -113
  472. package/src/modules/workspace/file-readers/__tests__/doc-extract.service.test.ts +10 -0
  473. package/src/modules/workspace/file-readers/doc-extract.service.ts +71 -38
  474. package/src/modules/workspace/index.ts +20 -0
  475. package/src/modules/workspace/nul-scan.ts +50 -0
  476. package/src/modules/workspace/pattern-matcher.contract.ts +123 -32
  477. package/src/modules/workspace/ripgrep-search.ts +715 -0
  478. package/src/modules/workspace/ripgrep-version.ts +35 -0
  479. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +11 -0
  480. package/src/modules/workspace/startup/beside-checkout.ts +13 -5
  481. package/src/modules/workspace/worker-pool.ts +3 -4
  482. package/src/modules/workspace/workspace-tmp.ts +457 -0
  483. package/src/modules/workspace/workspace.routes.ts +6 -27
  484. package/src/modules/workspace/workspace.service.ts +92 -15
  485. package/src/modules/workspace/workspace.tools.ts +500 -155
  486. package/src/shared/path-order.ts +37 -8
  487. package/starter-packs/engineering/Plugins/engineering-starter/access.md +1 -1
  488. package/starter-packs/general/Plugins/general-starter/access.md +1 -1
  489. package/starter-packs/operations/Plugins/operations-starter/access.md +1 -1
  490. package/starter-packs/sales/Plugins/sales-starter/access.md +1 -1
  491. package/starter-packs/support/Plugins/support-starter/access.md +1 -1
  492. package/dist/modules/workspace/bounded-matcher.d.ts +0 -73
  493. package/dist/modules/workspace/bounded-matcher.d.ts.map +0 -1
  494. package/dist/modules/workspace/bounded-matcher.js +0 -200
  495. package/dist/modules/workspace/bounded-matcher.js.map +0 -1
  496. package/src/modules/workspace/__tests__/bounded-matcher.test.ts +0 -213
  497. package/src/modules/workspace/bounded-matcher.ts +0 -271
@@ -3,6 +3,7 @@ import cors from 'cors';
3
3
  import path from 'node:path';
4
4
  import type { Router, RequestHandler } from 'express';
5
5
  import { createAuthRoutes } from '../modules/auth/auth.routes.js';
6
+ import { authCookieSecure } from '../modules/auth/auth.middleware.js';
6
7
  import { createWorkspaceRoutes } from '../modules/workspace/workspace.routes.js';
7
8
  import {
8
9
  createAgentUploadRoutes,
@@ -364,6 +365,9 @@ export async function createCoreServer(
364
365
  core.authMiddleware,
365
366
  core.authProviders,
366
367
  core.config.loginPasswordEnabled,
368
+ // The cookie's Secure flag follows the scheme the deployment is reached
369
+ // on, as the SSO providers' does: one rule for every sign-in path.
370
+ { cookieSecure: authCookieSecure(core.config.publicBackendUrl) },
367
371
  ),
368
372
  );
369
373
 
@@ -383,6 +387,7 @@ export async function createCoreServer(
383
387
  core.internalTokenService,
384
388
  core.mcpOAuthProvider,
385
389
  core.mcpResourceMetadataUrl,
390
+ core.agentScopeService,
386
391
  ));
387
392
 
388
393
  // Remote sync — `POST /api/sync`, called by a git host's webhook or a
@@ -423,6 +428,7 @@ export async function createCoreServer(
423
428
  toolManualService: core.toolManualService,
424
429
  accessControl: core.accessControl,
425
430
  connectionProbe: core.connectionProbeService,
431
+ agents: core.agentScopeService,
426
432
  stateSecret: core.config.jwtSecret,
427
433
  publicBackendUrl: core.config.publicBackendUrl,
428
434
  publicFrontendUrl: core.config.publicFrontendUrl,
@@ -552,6 +558,7 @@ export async function createCoreServer(
552
558
  core.manualAuthMiddleware,
553
559
  async (userId) => (await core.authService.getUserById(userId))?.email,
554
560
  readOnlyHints,
561
+ core.agentScopeService,
555
562
  ));
556
563
  toolsRouter.use(createToolHintsAgentRoutes(readOnlyHints, core.manualAuthMiddleware));
557
564
  // The aggregated manual list (KB + the caller's accessible `.tool`s) + inline
@@ -568,6 +575,7 @@ export async function createCoreServer(
568
575
  disk: core.disk,
569
576
  pluginIndex: core.pluginIndexService,
570
577
  },
578
+ core.agentScopeService,
571
579
  ));
572
580
  // What every connected agent is told at session start, as the hosted proxy
573
581
  // composes it: read by the local `hexis-mcp` bridge at startup and by the
@@ -582,6 +590,7 @@ export async function createCoreServer(
582
590
  skills: core.skillService,
583
591
  manualAuth: core.manualAuthMiddleware,
584
592
  resolveUserEmail: async (userId) => (await core.authService.getUserById(userId))?.email,
593
+ agentScope: core.agentScopeService,
585
594
  // The views move the revision too: a release that changes a view and no
586
595
  // manual would otherwise leave a running local server on the old one.
587
596
  mcpApps: core.mcpAppService,
@@ -596,6 +605,7 @@ export async function createCoreServer(
596
605
  core.secretsVaultService,
597
606
  core.manualAuthMiddleware,
598
607
  async (userId) => (await core.authService.getUserById(userId))?.email,
608
+ core.agentScopeService,
599
609
  ));
600
610
  app.use('/api', toolsRouter);
601
611
 
@@ -638,6 +648,7 @@ export async function createCoreServer(
638
648
  createPluginCreationRoutes(
639
649
  core.pluginProvisionService,
640
650
  async (req) => (req.userId ? ((await core.authService.getUserById(req.userId)) ?? null) : null),
651
+ core.agentScopeService,
641
652
  ),
642
653
  );
643
654
 
@@ -705,6 +716,7 @@ export async function createCoreServer(
705
716
  core.eventBus,
706
717
  core.db,
707
718
  core.kb,
719
+ core.disk,
708
720
  [core.config.adminEmail],
709
721
  ));
710
722
  // Asking for Can edit or Owner on an item from its Manage access dialog, and
@@ -904,6 +916,7 @@ export async function createCoreServer(
904
916
  clientNameFor: (st) => core.githubFacade.clientNameFor(st),
905
917
  completeConsent: (userId, st) => core.githubFacade.completeConsent(userId, st),
906
918
  },
919
+ agents: core.agentScopeService,
907
920
  }));
908
921
 
909
922
  // What an Owner pastes into Claude's admin settings to register this
@@ -63,32 +63,38 @@ function parseDomainList(raw: string): string[] {
63
63
  .filter((d) => d.length > 0);
64
64
  }
65
65
  import { SpillStore } from '../modules/workspace/spill-store.js';
66
+ import { TmpSweeper } from '../modules/workspace/workspace-tmp.js';
66
67
  import { AgentUploadStore, assertUploadsRootOutsideWorkspaces } from '../modules/workspace/agent-upload.store.js';
67
68
  import { AgentDownloadStore, type IAgentDownloadStore } from '../modules/workspace/agent-download.store.js';
68
69
  import { createDownloadFetcherIdentifier } from '../modules/workspace/agent-download.routes.js';
69
- import { WorkerPatternMatcher } from '../modules/workspace/bounded-matcher.js';
70
- import type { IPatternMatcher } from '../modules/workspace/pattern-matcher.contract.js';
71
70
  import type { Request } from 'express';
72
71
  import {
72
+ assertRipgrep,
73
73
  DocExtractService,
74
+ RipgrepPatternMatcher,
74
75
  WorkerDocumentExtractor,
75
76
  type IDocumentExtractor,
77
+ type IPatternMatcher,
76
78
  } from '../modules/workspace/index.js';
77
79
  import { UuidSessionSink, type ISessionSink } from '../modules/workspace/session-sink.js';
78
80
  import { AuthService } from '../modules/auth/auth.service.js';
79
81
  import { AccountHooks, type IAccountHooks } from '../modules/auth/account-hooks.js';
80
82
  import { AccountErasureService } from '../modules/auth/account-erasure.service.js';
81
83
  import { OidcAuthProvider, oidcSettingsFrom } from '../modules/auth/oidc-auth-provider.js';
82
- import { createAuthMiddleware, readAuthCookie } from '../modules/auth/auth.middleware.js';
84
+ import { authCookieSecure, createAuthMiddleware, readAuthCookie } from '../modules/auth/auth.middleware.js';
83
85
  import { AccessControlService, loadActiveGroups } from '../modules/access/access-control.service.js';
84
86
  import { AccessMutationService } from '../modules/access/access-mutation.service.js';
85
87
  import { CreatorAccessService } from '../modules/access/creator-access.js';
86
88
  import { ChangeReadGate } from '../modules/access/change-read-gate.js';
87
89
  import { GroupsAdminService } from '../modules/access/groups-admin.service.js';
88
90
  import { UserAccessRemovalService } from '../modules/access/user-access-removal.service.js';
89
- import { makeRolesYamlWriteValidator } from '../modules/access-model/roles-yaml-guard.js';
90
- import { PendingSkillsService, SkillService } from '../modules/skills/index.js';
91
- import { PendingToolsService, ToolManualService } from '../modules/tool-manuals/index.js';
91
+ import { makeRolesYamlWriteValidator } from '../modules/access-model/index.js';
92
+ // The implementations, from their files: the module entries offer them as
93
+ // types only, so nothing that imports an entry loads a service.
94
+ import { SkillService } from '../modules/skills/skills.service.js';
95
+ import { PendingSkillsService } from '../modules/skills/pending-skills.service.js';
96
+ import { ToolManualService } from '../modules/tool-manuals/tool-manuals.service.js';
97
+ import { PendingToolsService } from '../modules/tool-manuals/pending-tools.service.js';
92
98
  import { McpServerEditService } from '../modules/tool-manuals/mcp-server-edit.service.js';
93
99
  import { ToolDeleteService } from '../modules/tool-manuals/tool-delete.service.js';
94
100
  import {
@@ -105,6 +111,7 @@ import {
105
111
  MarketplaceCompilerService,
106
112
  KbPluginSource,
107
113
  } from '../modules/plugins/index.js';
114
+ import { noAgentConfigurations, type IAgentScopeResolver } from '../modules/agent-scope/index.js';
108
115
  import { MarketplaceRepoService } from '../modules/marketplace/index.js';
109
116
  import {
110
117
  GitHubFacadeCredentialsService,
@@ -255,7 +262,9 @@ export interface CoreServices {
255
262
  agentUploadStore: AgentUploadStore;
256
263
  /** The bytes `request_file_download` captured, held until their one-time link is fetched or expires. */
257
264
  agentDownloadStore: IAgentDownloadStore;
258
- /** Runs a caller's `grep` pattern off the request thread under a budget; its workers end when the graph stops. */
265
+ /** Sweeps every workspace's `tmp/` folder of entries older than seven days, hourly; started and stopped with the graph. */
266
+ tmpSweeper: TmpSweeper;
267
+ /** Runs a caller's `grep` pattern in ripgrep under a budget; its running searches end when the graph stops. */
259
268
  patternMatcher: IPatternMatcher;
260
269
  /**
261
270
  * Every user a download fetch identifies itself as, by its bearer and its
@@ -309,6 +318,12 @@ export interface CoreServices {
309
318
  pluginJoinRequestJobs: PluginJoinRequestJobs;
310
319
  /** Which plugins hold which skills (inline or linked) — see `PluginLinkIndex`. */
311
320
  pluginLinkIndex: PluginLinkIndex;
321
+ /**
322
+ * Connecting as an agent configuration: the choices offered at consent and key
323
+ * mint, and the scope every agent surface narrows a connection to — see
324
+ * `modules/agent-scope`.
325
+ */
326
+ agentScopeService: IAgentScopeResolver;
312
327
  /** Link / unlink / repair shared skills into plugins. */
313
328
  pluginLinksService: PluginLinksService;
314
329
  pluginRenameService: PluginRenameService;
@@ -432,6 +447,9 @@ export async function createCoreServices(
432
447
  // (see `git-version.ts`): every clone, fetch and refresh below depends on it,
433
448
  // so an unsupported binary is better surfaced here than at the first merge.
434
449
  await assertGitVersion();
450
+ // The same for ripgrep, which the grep tool searches with and has no
451
+ // fallback for (see `ripgrep-version.ts`).
452
+ await assertRipgrep();
435
453
  // What tells this knowledge base's locks, migration ledger and secrets
436
454
  // loader apart from another's in the same process and database. Empty for
437
455
  // the default schema, so a single-tenant deployment keeps the lock ids and
@@ -767,6 +785,10 @@ export async function createCoreServices(
767
785
  // Shared, workspace-independent store for oversized `call_tool_chain` results,
768
786
  // read back via `read_file`. Sibling of `workspacesRoot`, never committed.
769
787
  const spillStore = new SpillStore(config.spillRoot);
788
+ // What the chat, the SharePoint reader and tool chains stage in a
789
+ // workspace's `tmp/` folder lives seven days past its last change; the file
790
+ // tools read it through the `__tmp__/` ref meanwhile (see workspace-tmp.ts).
791
+ const tmpSweeper = new TmpSweeper((signal) => workspaceService.sweepTmp(undefined, undefined, signal));
770
792
  // The upload route an agent lands files by, so their content never passes
771
793
  // through the model. Bytes live BESIDE the workspaces root (never inside
772
794
  // one) until the apply commits them or the token expires — checked here
@@ -795,7 +817,7 @@ export async function createCoreServices(
795
817
  'captured download bytes wait there for their one-time links, so a root inside a workspace would let the ' +
796
818
  'file tools read them. Point AGENT_UPLOADS_ROOT at a directory whose parent is outside WORKSPACES_ROOT.',
797
819
  });
798
- const patternMatcher = new WorkerPatternMatcher();
820
+ const patternMatcher = new RipgrepPatternMatcher();
799
821
  const agentDownloadStore = new AgentDownloadStore({
800
822
  root: agentDownloadsRoot,
801
823
  publicBaseUrl: config.publicBackendUrl,
@@ -851,6 +873,17 @@ export async function createCoreServices(
851
873
  // The link index resolves manifests against the released catalog, and the
852
874
  // plugin index counts through it (inline + linked), so it comes first.
853
875
  const pluginLinkIndex = new PluginLinkIndex(workspaceService, skillService, accessControl, kb, pluginSource);
876
+ // What "Connect as" offers and what a connection made as an agent loads:
877
+ // the distribution's resolver (`CorePorts.agentScope`), made HERE with the
878
+ // core services it reads through — its configurations behind the person's
879
+ // read gate, `plugins:` expanded through the link index above — or core's
880
+ // default, which offers no agent and loads nothing for a path it cannot
881
+ // read. Made once, before anything consults it: the tool-context resolver
882
+ // below captures it, as the routes the server mounts do, so there is one
883
+ // resolver in effect and no later assignment to miss. Core owns the
884
+ // mechanism and no configuration (see `modules/agent-scope`).
885
+ const agentScopeService: IAgentScopeResolver =
886
+ ports.agentScope?.({ workspaceService, accessControl, kb, disk, pluginLinkIndex }) ?? noAgentConfigurations();
854
887
  const pluginIndexService = new PluginIndexService(
855
888
  workspaceService,
856
889
  accessControl,
@@ -1487,6 +1520,7 @@ export async function createCoreServices(
1487
1520
  kbDirName: kbDirName,
1488
1521
  creatorAccess,
1489
1522
  loadActiveGroups,
1523
+ agentScope: agentScopeService,
1490
1524
  });
1491
1525
  const writeAccess = ports.writeAccess ?? alwaysWritable;
1492
1526
  // The branch every tool call runs on is resolved by the handler, before the
@@ -1496,8 +1530,8 @@ export async function createCoreServices(
1496
1530
  defaultBranch: () => kb.defaultBranch,
1497
1531
  isMissing: (branch) => workspaceService.isBranchMissing(branch),
1498
1532
  });
1499
- const toolAuthMiddleware = createToolAuthMiddleware(externalApiKeyService, internalTokenService, authService);
1500
- const verifyToolToken = createTokenVerifier(externalApiKeyService, internalTokenService, authService);
1533
+ const toolAuthMiddleware = createToolAuthMiddleware(externalApiKeyService, internalTokenService, authService, mcpOAuthProvider);
1534
+ const verifyToolToken = createTokenVerifier(externalApiKeyService, internalTokenService, authService, mcpOAuthProvider);
1501
1535
  // Every credential kind this server issues — connection keys, internal
1502
1536
  // tokens and app sessions (bearer or cookie) — so a fetch carrying another
1503
1537
  // user's identity of ANY kind is refused the link.
@@ -1508,7 +1542,7 @@ export async function createCoreServices(
1508
1542
  // Read-only manual endpoints accept the above PLUS a browser JWT, so a
1509
1543
  // logged-in user can browse the catalog with their session. Execution routes
1510
1544
  // keep `toolAuthMiddleware` (no JWT), so a session can read but not invoke.
1511
- const manualAuthMiddleware = createManualAuthMiddleware(externalApiKeyService, internalTokenService, authService);
1545
+ const manualAuthMiddleware = createManualAuthMiddleware(externalApiKeyService, internalTokenService, authService, mcpOAuthProvider);
1512
1546
 
1513
1547
  // Recovery-bot identity: a synthetic `recovery-bot@bevel.local` user
1514
1548
  // backing (a) the recovery agent's commit authorship and (b) the
@@ -1575,7 +1609,7 @@ export async function createCoreServices(
1575
1609
  settings: () => oidcSettingsFrom(settings),
1576
1610
  publicBackendUrl: config.publicBackendUrl,
1577
1611
  publicFrontendUrl: config.publicFrontendUrl,
1578
- cookieSecure: config.publicBackendUrl.startsWith('https'),
1612
+ cookieSecure: authCookieSecure(config.publicBackendUrl),
1579
1613
  // A real sign-in proves the values that sign-in used. Recorded against
1580
1614
  // those — under their own key — so it says nothing about any saved
1581
1615
  // since, and cannot overwrite what was recorded about them.
@@ -1619,6 +1653,7 @@ export async function createCoreServices(
1619
1653
  workspaceService,
1620
1654
  workflowService,
1621
1655
  accessControl,
1656
+ disk,
1622
1657
  kbDirName,
1623
1658
  () => kb.defaultBranch,
1624
1659
  eventBus,
@@ -1673,6 +1708,7 @@ export async function createCoreServices(
1673
1708
  spillStore,
1674
1709
  agentUploadStore,
1675
1710
  agentDownloadStore,
1711
+ tmpSweeper,
1676
1712
  patternMatcher,
1677
1713
  agentDownloadFetcher,
1678
1714
  documentExtractor,
@@ -1694,6 +1730,7 @@ export async function createCoreServices(
1694
1730
  joinRequestsService,
1695
1731
  pluginJoinRequestJobs,
1696
1732
  pluginLinkIndex,
1733
+ agentScopeService,
1697
1734
  pluginLinksService,
1698
1735
  pluginRenameService,
1699
1736
  marketplaceCompiler,
@@ -6,6 +6,7 @@ import { noteBesideCheckout } from '../modules/workspace/startup/beside-checkout
6
6
  import { unregisterBevelSecretsVariableLoader } from '../modules/secrets-vault/secrets-variable-loader.js';
7
7
  import type { WorkflowService } from '../modules/workflow/workflow.service.js';
8
8
  import type { PluginJoinRequestJobs } from '../modules/plugins/join-request-jobs.service.js';
9
+ import type { TmpSweeper } from '../modules/workspace/workspace-tmp.js';
9
10
  import { printable } from '../shared/printable.js';
10
11
  import { logger } from '../shared/logging.js';
11
12
 
@@ -293,6 +294,8 @@ export interface BootableCore {
293
294
  kbStartupRunner: Pick<KbStartupRunner, 'runAll' | 'retryUntilMaintained'>;
294
295
  workflowService: Pick<WorkflowService, 'closeChangeRequestsWithDeletedBranches' | 'tidyAfterSweep'>;
295
296
  pluginJoinRequestJobs: Pick<PluginJoinRequestJobs, 'startSweeping'>;
297
+ /** The periodic sweep of every workspace's `tmp/` folder, started here and stopped by {@link stopCore}. */
298
+ tmpSweeper: Pick<TmpSweeper, 'startSweeping'>;
296
299
  /**
297
300
  * The startup phase's retry, when the boot survived an unreachable remote
298
301
  * and is asking again on a timer; null otherwise. Set by {@link startCore},
@@ -336,6 +339,13 @@ export async function startCore<C extends BootableCore>(
336
339
  });
337
340
  }
338
341
 
342
+ // The seven-day sweep of every workspace's `tmp/` folder (see
343
+ // `workspace-tmp.ts`), now and hourly. Needs nothing the startup phase
344
+ // provides — it reads the workspaces root and nothing else — so it is not
345
+ // held behind it, and a remote that is down does not leave staged files to
346
+ // pile up. Stopped by `stopCore`.
347
+ core.tmpSweeper.startSweeping();
348
+
339
349
  // The KB startup phase — AFTER the distribution's onBoot, because a FATAL
340
350
  // template finding raised there must stop the boot before anything seeds
341
351
  // from that template; the runner then brings every branch up to this build
@@ -448,6 +458,16 @@ export interface CoreStopDeps {
448
458
  agentUploadStore?: { stopSweeping(): void; drainSweep(): Promise<void> };
449
459
  /** The agent download store, whose sweep deletes captured bytes nobody fetched — stopped for the same reason. */
450
460
  agentDownloadStore?: { stopSweeping(): void; drainSweep(): Promise<void> };
461
+ /**
462
+ * The sweep of the workspaces' `tmp/` folders: stopped with this graph, so
463
+ * an evicted tenant's graph leaves no timer behind, and its sweep in flight
464
+ * cancelled (it removes nothing more) and then awaited within the stop's
465
+ * deadline, so the next graph over the same root does not meet it still
466
+ * walking there — unless one filesystem call outlasts that deadline, after
467
+ * which the cancelled sweep still removes nothing. Optional: a caller that
468
+ * has not built one passes nothing.
469
+ */
470
+ tmpSweeper?: { stopSweeping(): void; drainSweep(): Promise<void> };
451
471
  /**
452
472
  * The grep pattern matcher, whose worker threads are this graph's own: ended
453
473
  * here so a graph a tenant host evicts leaves no worker behind. Optional: a
@@ -555,6 +575,7 @@ async function releaseCore(deps: CoreStopDeps, remaining: () => number, log: (m:
555
575
  // running to stop deleting.
556
576
  deps.agentUploadStore?.stopSweeping();
557
577
  deps.agentDownloadStore?.stopSweeping();
578
+ deps.tmpSweeper?.stopSweeping();
558
579
  // A startup phase still asking for an unreachable remote stops asking:
559
580
  // synchronous, and nothing after it must be able to clone into a
560
581
  // workspaces folder that is about to belong to nobody.
@@ -587,15 +608,18 @@ async function releaseCore(deps: CoreStopDeps, remaining: () => number, log: (m:
587
608
  if (deps.agentDownloadStore) {
588
609
  await bounded(deps.agentDownloadStore.drainSweep(), remaining(), 'finishing the download sweep', log);
589
610
  }
611
+ if (deps.tmpSweeper) {
612
+ await bounded(deps.tmpSweeper.drainSweep(), remaining(), "finishing the workspaces' tmp sweep", log);
613
+ }
590
614
  // The tidy-up in flight fetches and deletes branches through the pool and
591
615
  // the default branch's checkout: awaited, so the next graph for this
592
616
  // tenant cannot find a branch deletion still running under it.
593
617
  if (deps.workflowService) {
594
618
  await bounded(deps.workflowService.drainTidy(), remaining(), 'finishing the branch tidy-up', log);
595
619
  }
596
- // Its workers end with the graph; a search still running is answered busy.
620
+ // Its ripgrep runs end with the graph; a search still running is answered busy or stopped.
597
621
  if (deps.patternMatcher) {
598
- await bounded(deps.patternMatcher.close(), remaining(), 'ending the search workers', log);
622
+ await bounded(deps.patternMatcher.close(), remaining(), 'ending the running searches', log);
599
623
  }
600
624
  // Likewise: an extraction still running is answered busy, and nothing about its file is remembered.
601
625
  if (deps.documentExtractor) {
package/src/index.ts CHANGED
@@ -26,7 +26,7 @@ export {
26
26
  type ServerExtensions,
27
27
  type ToolSurfaceCtx,
28
28
  } from './core/create-core-server.js';
29
- export { noopRecoveryAgent, type CorePorts } from './core/core-ports.js';
29
+ export { noopRecoveryAgent, type AgentScopeCollaborators, type CorePorts } from './core/core-ports.js';
30
30
  // Logging: the contract, the slot a shell installs its sink into, and the
31
31
  // tagged-logger lookup every module logs through. See shared/logging.ts.
32
32
  export { setLogger, logger, createConsoleLogger } from './shared/logging.js';
@@ -242,6 +242,17 @@ describe('Admin write floor at the repository root', () => {
242
242
  expect(await fs.readFile(path.join(repo, 'access.md'), 'utf-8')).toContain('write:\n - Admin');
243
243
  });
244
244
 
245
+ it('lets the root drop a GROUP named Admin — group/Admin is never the role', async () => {
246
+ await write(repo, 'access.md', '---\n---\nwrite:\n - Admin\n - group/Admin\n');
247
+ const group = { kind: 'role' as const, role: 'group/Admin' };
248
+ await expect(mutation.revoke(WS, 'folder', '', group, ADMIN, 'write')).resolves.toMatchObject({
249
+ changed: true,
250
+ });
251
+ const after = await fs.readFile(path.join(repo, 'access.md'), 'utf-8');
252
+ expect(after).not.toContain('group/Admin');
253
+ expect(after).toContain('write:\n - Admin'); // the role's line is untouched
254
+ });
255
+
245
256
  it('refuses deny-here on a PERSON who is an admin at the root, with the reason, and writes nothing', async () => {
246
257
  const before = await fs.readFile(path.join(repo, 'access.md'), 'utf-8');
247
258
  for (const email of [ADMIN, OWNER]) {
@@ -310,6 +321,7 @@ describe('Admin write floor at the repository root', () => {
310
321
  { emit: () => {} } as unknown as WorkflowEventBus,
311
322
  usersDbDouble(),
312
323
  testKbContext({ kbDirName: KB }),
324
+ new NodeFs(),
313
325
  ),
314
326
  );
315
327
  server = await new Promise<Server>((resolve) => {
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The case table for a name shared by a role and a group (the moved property
3
+ * of decision #315, "a bare name means the role"). Before this change a bare
4
+ * name meant the GROUP when both existed; it now means the ROLE. Exactly the
5
+ * colliding bare spellings change meaning, in grants AND in denials (decided
6
+ * by Razvan on 2026-10-23: a bare denial means the role exactly as a bare
7
+ * grant does; a group denial is written `deny group/<Name>`):
8
+ *
9
+ * rule | role-only member | group-only member
10
+ * -----------------------|-----------------------|----------------------
11
+ * read: Reviewer | was denied → granted | was granted → denied
12
+ * deny Reviewer | was allowed → denied | was denied → allowed
13
+ * read: group/Reviewer | denied (unchanged) | granted (new token)
14
+ * deny group/Reviewer | allowed (unchanged) | denied (new token)
15
+ * read: role/Reviewer | granted (unchanged) | denied (unchanged)
16
+ * deny role/Reviewer | denied (unchanged) | allowed (unchanged)
17
+ *
18
+ * A bare denial never touches the same-named group, so `read: group/Reviewer`
19
+ * beside `deny Reviewer` lets the group in. A name held by only one kind
20
+ * resolves as before. Every file with a colliding bare entry is listed by
21
+ * the groups roster (`bareNameResolvesToRole`) so an Admin can rewrite it as
22
+ * `group/<Name>` or `role/<Name>`.
23
+ */
24
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
25
+ import fs from 'node:fs/promises';
26
+ import path from 'node:path';
27
+ import os from 'node:os';
28
+
29
+ import { NodeFs } from '../../kb-fs/node-fs.js';
30
+ import type { WorkspaceService } from '../../workspace/index.js';
31
+ import { AccessControlService } from '../access-control.service.js';
32
+
33
+ const KB_DIR = 'knowledge-base';
34
+ const WS = 'ws-colliding-name';
35
+
36
+ const ADMIN = 'admin@x.io';
37
+ const ROLE_MEMBER = 'role-member@x.io';
38
+ const GROUP_MEMBER = 'group-member@x.io';
39
+ const DOC = 'Docs/Plan.md';
40
+
41
+ const ROOT_FILES: Record<string, string> = {
42
+ 'roles.yaml': `roles:\n Admin:\n - ${ADMIN}\n Reviewer:\n - ${ROLE_MEMBER}\n`,
43
+ 'groups.yaml': `groups:\n Reviewer:\n - ${GROUP_MEMBER}\n`,
44
+ 'access.md': '---\nwrite:\n - Admin\n---\nwrite:\n - Admin\n',
45
+ [DOC]: '# Plan\n',
46
+ };
47
+
48
+ describe('a name shared by a role and a group: grant and deny case table', () => {
49
+ let root: string;
50
+ let repo: string;
51
+ let svc: AccessControlService;
52
+
53
+ beforeEach(async () => {
54
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-colliding-name-'));
55
+ const workspaceDir = path.join(root, WS);
56
+ repo = path.join(workspaceDir, KB_DIR);
57
+ for (const [rel, text] of Object.entries(ROOT_FILES)) {
58
+ await fs.mkdir(path.dirname(path.join(repo, rel)), { recursive: true });
59
+ await fs.writeFile(path.join(repo, rel), text);
60
+ }
61
+ const workspaceService = {
62
+ getWorkspacePath: async () => workspaceDir,
63
+ ensureRemotesFetched: async () => undefined,
64
+ } as unknown as WorkspaceService;
65
+ svc = new AccessControlService(workspaceService, KB_DIR, new NodeFs());
66
+ });
67
+
68
+ afterEach(async () => {
69
+ await fs.rm(root, { recursive: true, force: true });
70
+ });
71
+
72
+ /** Who may read DOC when the Docs folder's read list is `entries`. */
73
+ async function readers(entries: string[]): Promise<{ role: boolean; group: boolean }> {
74
+ const body = `read:\n${entries.map((e) => ` - ${e}\n`).join('')}`;
75
+ await fs.writeFile(path.join(repo, 'Docs/access.md'), `---\nwrite:\n - Admin\n---\n${body}`);
76
+ svc.invalidate(WS);
77
+ return {
78
+ role: await svc.canRead(WS, ROLE_MEMBER, DOC),
79
+ group: await svc.canRead(WS, GROUP_MEMBER, DOC),
80
+ };
81
+ }
82
+
83
+ it('a bare grant reaches the role only', async () => {
84
+ expect(await readers(['Reviewer'])).toEqual({ role: true, group: false });
85
+ });
86
+
87
+ it('a bare denial reaches the role only: the members of the same-named group are untouched', async () => {
88
+ expect(await readers(['everyone', 'deny Reviewer'])).toEqual({ role: false, group: true });
89
+ });
90
+
91
+ it('a group grant beside a bare denial lets the group in', async () => {
92
+ expect(await readers(['group/Reviewer', 'deny Reviewer'])).toEqual({ role: false, group: true });
93
+ });
94
+
95
+ it('group/<Name> grants and denies the group only', async () => {
96
+ expect(await readers(['group/Reviewer'])).toEqual({ role: false, group: true });
97
+ expect(await readers(['everyone', 'deny group/Reviewer'])).toEqual({ role: true, group: false });
98
+ });
99
+
100
+ it('role/<Name> grants and denies the role only', async () => {
101
+ expect(await readers(['role/Reviewer'])).toEqual({ role: true, group: false });
102
+ expect(await readers(['everyone', 'deny role/Reviewer'])).toEqual({ role: false, group: true });
103
+ });
104
+
105
+ it('a bare name held only by a group still means the group, in grants and denials', async () => {
106
+ await fs.writeFile(
107
+ path.join(repo, 'roles.yaml'),
108
+ `roles:\n Admin:\n - ${ADMIN}\n`,
109
+ );
110
+ expect(await readers(['Reviewer'])).toEqual({ role: false, group: true });
111
+ expect(await readers(['everyone', 'deny Reviewer'])).toEqual({ role: true, group: false });
112
+ });
113
+ });
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { NodeFs } from '../../kb-fs/node-fs.js';
6
+ import type { WorkspaceService } from '../../workspace/workspace.service.js';
7
+ import { AccessControlService } from '../access-control.service.js';
8
+
9
+ const KB = 'knowledge-base';
10
+ const RULE = 'read:\n - deny everyone\n - Product Manager\n';
11
+ const ali = 'ali@bevel.software'; // no role
12
+ const felix = 'felix@example.com'; // Product Manager
13
+
14
+ /**
15
+ * A file's own access rules live in its frontmatter, so the read checks read
16
+ * a file only as far as its frontmatter reaches — a line, for a file with
17
+ * none. That is what keeps a batch over a tree of large transcripts (grep's,
18
+ * the explorer's) from reading the whole tree, and these pin that the head
19
+ * answers exactly as the whole file would, fence rule and all.
20
+ */
21
+ describe('AccessControlService — a file is read only as far as its frontmatter', () => {
22
+ let root: string;
23
+ let repo: string;
24
+ let svc: AccessControlService;
25
+ const workspaceId = 'ws-frontmatter-head';
26
+
27
+ beforeEach(async () => {
28
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-fm-head-'));
29
+ const workspaceDir = path.join(root, workspaceId);
30
+ repo = path.join(workspaceDir, KB);
31
+ await fs.mkdir(path.join(repo, 'Open'), { recursive: true });
32
+ await fs.writeFile(path.join(repo, 'roles.yaml'), 'roles:\n Admin:\n - razvan@bevel.software\n Product Manager:\n - felix@example.com\n');
33
+ await fs.writeFile(path.join(repo, 'Open', 'access.md'), '---\nread:\n - everyone\n---\n');
34
+ svc = new AccessControlService(
35
+ { getWorkspacePath: async () => workspaceDir } as unknown as WorkspaceService,
36
+ KB,
37
+ new NodeFs(),
38
+ );
39
+ });
40
+ afterEach(async () => {
41
+ await fs.rm(root, { recursive: true, force: true });
42
+ });
43
+
44
+ const put = (name: string, content: string | Buffer) => fs.writeFile(path.join(repo, 'Open', name), content);
45
+ const verdicts = async (name: string) => {
46
+ const rel = `Open/${name}`;
47
+ const batch = await svc.canReadBatch(workspaceId, ali, [rel]);
48
+ const felixBatch = await svc.canReadBatch(workspaceId, felix, [rel]);
49
+ return {
50
+ ali: batch.get(rel),
51
+ felix: felixBatch.get(rel),
52
+ single: await svc.canRead(workspaceId, ali, rel),
53
+ };
54
+ };
55
+
56
+ it('honours the rules of a file far too large to read whole, at once', async () => {
57
+ await put('huge.md', `---\n${RULE}---\n`);
58
+ // 600 MB, sparse: more text than one string can hold, so a read of the
59
+ // whole file could not even be decoded, and its rule would be missed.
60
+ await fs.truncate(path.join(repo, 'Open', 'huge.md'), 600 * 1024 * 1024);
61
+ expect(await verdicts('huge.md')).toEqual({ ali: false, felix: true, single: false });
62
+ });
63
+
64
+ it('reads a frontmatter longer than any one read through to its closing fence', async () => {
65
+ await put('long-fm.md', `---\nsummary: "${'x'.repeat(200_000)}"\n${RULE}---\nbody\n`);
66
+ expect(await verdicts('long-fm.md')).toEqual({ ali: false, felix: true, single: false });
67
+ });
68
+
69
+ it('scans a frontmatter line far longer than one read once, not again with every read', async () => {
70
+ // A long line inside the frontmatter, then the rule; and a closing fence
71
+ // padded with 3 MB of spaces, which stays a fence however long it runs.
72
+ const longValue = (chars: number) =>
73
+ `---\nsummary: "${'x'.repeat(chars)}"\n${RULE}${' '.repeat(3_000_000)}---${' '.repeat(3_000_000)}\nbody\n`;
74
+ await put('long-value.md', longValue(32_000_000));
75
+ expect(await verdicts('long-value.md')).toEqual({ ali: false, felix: true, single: false });
76
+ // How the cost grows, not how long it takes on this machine: a line 16
77
+ // times longer costs about 16 times as much scanned once, and far more
78
+ // (about 60 times, measured) scanned again with every read. The slowest
79
+ // short check against the fastest long one, so noise only helps a scan
80
+ // that is linear pass.
81
+ const timed = async (name: string, chars: number): Promise<number> => {
82
+ await put(name, `---\nsummary: "${'x'.repeat(chars)}"\n${RULE}---\n`);
83
+ const started = performance.now();
84
+ await svc.canReadBatch(workspaceId, ali, [`Open/${name}`]);
85
+ return performance.now() - started;
86
+ };
87
+ const short: number[] = [];
88
+ const long: number[] = [];
89
+ for (let i = 0; i < 3; i++) {
90
+ short.push(await timed(`short-${i}.md`, 2_000_000));
91
+ long.push(await timed(`long-${i}.md`, 32_000_000));
92
+ }
93
+ expect(Math.min(...long) / Math.max(...short)).toBeLessThan(32);
94
+ }, 60_000);
95
+
96
+ it('applies the fence rule as the whole-file scan does: CRLF, a BOM, padding around the fence', async () => {
97
+ await put('crlf.md', `---\r\n${RULE.replace(/\n/g, '\r\n')}---\r\nbody\r\n`);
98
+ await put('bom.md', `\uFEFF---\n${RULE}---\n`);
99
+ await put('padded.md', `${' '.repeat(5_000)}--- \n${RULE} ---\n`);
100
+ for (const name of ['crlf.md', 'bom.md', 'padded.md']) {
101
+ expect(await verdicts(name), name).toEqual({ ali: false, felix: true, single: false });
102
+ }
103
+ });
104
+
105
+ it('finds no rules where the whole-file scan finds none', async () => {
106
+ // A fenced block that is not at the top is body, not frontmatter.
107
+ await put('later.md', `# title\n---\n${RULE}---\n`);
108
+ // An opening fence never closed is no frontmatter either.
109
+ await put('unclosed.md', `---\n${RULE}`);
110
+ // A first line that is long, and no fence, ends the read at once.
111
+ await put('long-line.md', `${'x'.repeat(3_000_000)}\n---\n${RULE}---\n`);
112
+ await put('empty.md', '');
113
+ for (const name of ['later.md', 'unclosed.md', 'long-line.md', 'empty.md']) {
114
+ expect(await verdicts(name), name).toEqual({ ali: true, felix: true, single: true });
115
+ }
116
+ });
117
+ });