@bevel-software/platform-core-backend 0.33.0 → 0.35.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 (602) hide show
  1. package/agent-guide/access-control.md +146 -289
  2. package/agent-guide/conventions.md +23 -25
  3. package/agent-guide/directory-structure.md +72 -120
  4. package/agent-guide/drafts-and-change-requests.md +92 -0
  5. package/agent-guide/for-git-users.md +47 -0
  6. package/agent-guide/glossary.md +36 -0
  7. package/agent-guide/how-agents-find-things.md +49 -0
  8. package/agent-guide/html-views.md +239 -8
  9. package/agent-guide/new-knowledge-base.md +9 -8
  10. package/agent-guide/skills.md +67 -42
  11. package/agent-guide/start-here.md +65 -0
  12. package/agent-guide/tool-manuals.md +133 -155
  13. package/agent-guide/tools.md +67 -0
  14. package/agent-guide/where-a-new-file-goes.md +45 -28
  15. package/dist/core/catalog-revision.d.ts +12 -1
  16. package/dist/core/catalog-revision.d.ts.map +1 -1
  17. package/dist/core/catalog-revision.js +23 -3
  18. package/dist/core/catalog-revision.js.map +1 -1
  19. package/dist/core/core-ports.d.ts +41 -0
  20. package/dist/core/core-ports.d.ts.map +1 -1
  21. package/dist/core/core-ports.js.map +1 -1
  22. package/dist/core/create-core-server.d.ts.map +1 -1
  23. package/dist/core/create-core-server.js +15 -8
  24. package/dist/core/create-core-server.js.map +1 -1
  25. package/dist/core/create-core-services.d.ts +22 -5
  26. package/dist/core/create-core-services.d.ts.map +1 -1
  27. package/dist/core/create-core-services.js +44 -16
  28. package/dist/core/create-core-services.js.map +1 -1
  29. package/dist/core/lifecycle.d.ts +16 -0
  30. package/dist/core/lifecycle.d.ts.map +1 -1
  31. package/dist/core/lifecycle.js +12 -2
  32. package/dist/core/lifecycle.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/modules/access/access-control.interface.d.ts +39 -9
  37. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  38. package/dist/modules/access/access-control.service.d.ts +15 -3
  39. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  40. package/dist/modules/access/access-control.service.js +239 -75
  41. package/dist/modules/access/access-control.service.js.map +1 -1
  42. package/dist/modules/access/access-mutation.service.d.ts +22 -24
  43. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  44. package/dist/modules/access/access-mutation.service.js +48 -51
  45. package/dist/modules/access/access-mutation.service.js.map +1 -1
  46. package/dist/modules/access/access-view.d.ts.map +1 -1
  47. package/dist/modules/access/access-view.js +12 -11
  48. package/dist/modules/access/access-view.js.map +1 -1
  49. package/dist/modules/access/access.routes.d.ts +3 -0
  50. package/dist/modules/access/access.routes.d.ts.map +1 -1
  51. package/dist/modules/access/access.routes.js +58 -33
  52. package/dist/modules/access/access.routes.js.map +1 -1
  53. package/dist/modules/access/groups-admin.routes.d.ts.map +1 -1
  54. package/dist/modules/access/groups-admin.routes.js +4 -2
  55. package/dist/modules/access/groups-admin.routes.js.map +1 -1
  56. package/dist/modules/access/groups-admin.service.d.ts +44 -13
  57. package/dist/modules/access/groups-admin.service.d.ts.map +1 -1
  58. package/dist/modules/access/groups-admin.service.js +80 -38
  59. package/dist/modules/access/groups-admin.service.js.map +1 -1
  60. package/dist/modules/access/groups-edit.d.ts +1 -1
  61. package/dist/modules/access/groups-edit.d.ts.map +1 -1
  62. package/dist/modules/access/groups-edit.js +1 -1
  63. package/dist/modules/access/groups-edit.js.map +1 -1
  64. package/dist/modules/access/index.d.ts +1 -1
  65. package/dist/modules/access/index.d.ts.map +1 -1
  66. package/dist/modules/access/reference-scan.d.ts +116 -24
  67. package/dist/modules/access/reference-scan.d.ts.map +1 -1
  68. package/dist/modules/access/reference-scan.js +305 -44
  69. package/dist/modules/access/reference-scan.js.map +1 -1
  70. package/dist/modules/access/roles-admin.service.d.ts +17 -8
  71. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  72. package/dist/modules/access/roles-admin.service.js +32 -23
  73. package/dist/modules/access/roles-admin.service.js.map +1 -1
  74. package/dist/modules/access-model/access-grammar.d.ts +35 -10
  75. package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
  76. package/dist/modules/access-model/access-grammar.js +60 -20
  77. package/dist/modules/access-model/access-grammar.js.map +1 -1
  78. package/dist/modules/access-model/access-splice.d.ts +22 -18
  79. package/dist/modules/access-model/access-splice.d.ts.map +1 -1
  80. package/dist/modules/access-model/access-splice.js +20 -14
  81. package/dist/modules/access-model/access-splice.js.map +1 -1
  82. package/dist/modules/access-model/group-files.d.ts +1 -0
  83. package/dist/modules/access-model/group-files.d.ts.map +1 -1
  84. package/dist/modules/access-model/group-files.js +5 -1
  85. package/dist/modules/access-model/group-files.js.map +1 -1
  86. package/dist/modules/agent-guide/agent-guide.d.ts +47 -7
  87. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
  88. package/dist/modules/agent-guide/agent-guide.js +103 -13
  89. package/dist/modules/agent-guide/agent-guide.js.map +1 -1
  90. package/dist/modules/agent-guide/agent-guide.tools.d.ts +6 -2
  91. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -1
  92. package/dist/modules/agent-guide/agent-guide.tools.js +46 -22
  93. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -1
  94. package/dist/modules/agent-guide/guide-budget.d.ts +48 -0
  95. package/dist/modules/agent-guide/guide-budget.d.ts.map +1 -0
  96. package/dist/modules/agent-guide/guide-budget.js +67 -0
  97. package/dist/modules/agent-guide/guide-budget.js.map +1 -0
  98. package/dist/modules/agent-guide/index.d.ts +2 -1
  99. package/dist/modules/agent-guide/index.d.ts.map +1 -1
  100. package/dist/modules/agent-guide/index.js +2 -1
  101. package/dist/modules/agent-guide/index.js.map +1 -1
  102. package/dist/modules/agent-instructions/compose.d.ts +3 -1
  103. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  104. package/dist/modules/agent-instructions/compose.js +7 -1
  105. package/dist/modules/agent-instructions/compose.js.map +1 -1
  106. package/dist/modules/agent-instructions/shared-file-rules.d.ts +5 -1
  107. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  108. package/dist/modules/agent-instructions/shared-file-rules.js +26 -9
  109. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  110. package/dist/modules/agent-scope/agent-scope.contract.d.ts +67 -0
  111. package/dist/modules/agent-scope/agent-scope.contract.d.ts.map +1 -0
  112. package/dist/modules/agent-scope/agent-scope.contract.js +27 -0
  113. package/dist/modules/agent-scope/agent-scope.contract.js.map +1 -0
  114. package/dist/modules/agent-scope/connection-scope.d.ts +111 -0
  115. package/dist/modules/agent-scope/connection-scope.d.ts.map +1 -0
  116. package/dist/modules/agent-scope/connection-scope.js +165 -0
  117. package/dist/modules/agent-scope/connection-scope.js.map +1 -0
  118. package/dist/modules/agent-scope/index.d.ts +3 -0
  119. package/dist/modules/agent-scope/index.d.ts.map +1 -0
  120. package/dist/modules/agent-scope/index.js +3 -0
  121. package/dist/modules/agent-scope/index.js.map +1 -0
  122. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  123. package/dist/modules/audit/agent-audit.service.js +2 -0
  124. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  125. package/dist/modules/audit/audit.contract.d.ts +6 -0
  126. package/dist/modules/audit/audit.contract.d.ts.map +1 -1
  127. package/dist/modules/audit/audit.contract.js.map +1 -1
  128. package/dist/modules/auth/account-admission.d.ts +12 -0
  129. package/dist/modules/auth/account-admission.d.ts.map +1 -1
  130. package/dist/modules/auth/account-admission.js +15 -0
  131. package/dist/modules/auth/account-admission.js.map +1 -1
  132. package/dist/modules/auth/auth.middleware.d.ts +27 -1
  133. package/dist/modules/auth/auth.middleware.d.ts.map +1 -1
  134. package/dist/modules/auth/auth.middleware.js +42 -4
  135. package/dist/modules/auth/auth.middleware.js.map +1 -1
  136. package/dist/modules/auth/auth.routes.d.ts +13 -9
  137. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  138. package/dist/modules/auth/auth.routes.js +35 -29
  139. package/dist/modules/auth/auth.routes.js.map +1 -1
  140. package/dist/modules/auth/auth.service.d.ts +55 -6
  141. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  142. package/dist/modules/auth/auth.service.js +128 -34
  143. package/dist/modules/auth/auth.service.js.map +1 -1
  144. package/dist/modules/auth/index.d.ts +1 -1
  145. package/dist/modules/auth/index.d.ts.map +1 -1
  146. package/dist/modules/auth/index.js +1 -1
  147. package/dist/modules/auth/index.js.map +1 -1
  148. package/dist/modules/auth/oidc-auth-provider.d.ts.map +1 -1
  149. package/dist/modules/auth/oidc-auth-provider.js +2 -9
  150. package/dist/modules/auth/oidc-auth-provider.js.map +1 -1
  151. package/dist/modules/code-mode/code-mode.tool.js +2 -2
  152. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  153. package/dist/modules/connection-probe/connection-probe.service.d.ts +0 -12
  154. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  155. package/dist/modules/connection-probe/connection-probe.service.js +1 -38
  156. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  157. package/dist/modules/database/core-schema.d.ts +68 -0
  158. package/dist/modules/database/core-schema.d.ts.map +1 -1
  159. package/dist/modules/database/core-schema.js +33 -0
  160. package/dist/modules/database/core-schema.js.map +1 -1
  161. package/dist/modules/declared-variables/declared-variables.routes.d.ts +8 -1
  162. package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -1
  163. package/dist/modules/declared-variables/declared-variables.routes.js +13 -2
  164. package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -1
  165. package/dist/modules/embed/embed.tools.d.ts.map +1 -1
  166. package/dist/modules/embed/embed.tools.js +5 -0
  167. package/dist/modules/embed/embed.tools.js.map +1 -1
  168. package/dist/modules/kb-fs/index.d.ts +2 -0
  169. package/dist/modules/kb-fs/index.d.ts.map +1 -1
  170. package/dist/modules/kb-fs/index.js +1 -0
  171. package/dist/modules/kb-fs/index.js.map +1 -1
  172. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  173. package/dist/modules/kb-fs/locking-filesystem.js +7 -1
  174. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  175. package/dist/modules/kb-fs/repo-path.d.ts +26 -6
  176. package/dist/modules/kb-fs/repo-path.d.ts.map +1 -1
  177. package/dist/modules/kb-fs/repo-path.js +22 -7
  178. package/dist/modules/kb-fs/repo-path.js.map +1 -1
  179. package/dist/modules/kb-fs/tmp-ref.d.ts +36 -0
  180. package/dist/modules/kb-fs/tmp-ref.d.ts.map +1 -0
  181. package/dist/modules/kb-fs/tmp-ref.js +76 -0
  182. package/dist/modules/kb-fs/tmp-ref.js.map +1 -0
  183. package/dist/modules/mcp/mcp-auth.middleware.d.ts +7 -0
  184. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  185. package/dist/modules/mcp/mcp-auth.middleware.js +14 -1
  186. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  187. package/dist/modules/mcp/mcp.routes.d.ts +7 -1
  188. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  189. package/dist/modules/mcp/mcp.routes.js +39 -4
  190. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  191. package/dist/modules/mcp/mcp.service.d.ts +11 -0
  192. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  193. package/dist/modules/mcp/mcp.service.js +74 -48
  194. package/dist/modules/mcp/mcp.service.js.map +1 -1
  195. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +24 -1
  196. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  197. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +45 -10
  198. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  199. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts +9 -0
  200. package/dist/modules/mcp/oauth/oauth-consent.routes.d.ts.map +1 -1
  201. package/dist/modules/mcp/oauth/oauth-consent.routes.js +34 -1
  202. package/dist/modules/mcp/oauth/oauth-consent.routes.js.map +1 -1
  203. package/dist/modules/plugins/index.d.ts +1 -1
  204. package/dist/modules/plugins/index.d.ts.map +1 -1
  205. package/dist/modules/plugins/index.js.map +1 -1
  206. package/dist/modules/plugins/plugin-links.d.ts +18 -2
  207. package/dist/modules/plugins/plugin-links.d.ts.map +1 -1
  208. package/dist/modules/plugins/plugin-links.js.map +1 -1
  209. package/dist/modules/plugins/plugins.routes.d.ts +5 -1
  210. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -1
  211. package/dist/modules/plugins/plugins.routes.js +32 -5
  212. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  213. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +11 -1
  214. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
  215. package/dist/modules/secrets-vault/db-secrets-vault.service.js +159 -24
  216. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  217. package/dist/modules/secrets-vault/index.d.ts +1 -1
  218. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  219. package/dist/modules/secrets-vault/index.js +1 -1
  220. package/dist/modules/secrets-vault/index.js.map +1 -1
  221. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +53 -11
  222. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
  223. package/dist/modules/secrets-vault/secrets-vault.contract.js +27 -0
  224. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
  225. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +9 -0
  226. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  227. package/dist/modules/secrets-vault/secrets-vault.routes.js +100 -42
  228. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  229. package/dist/modules/settings/oidc-check.d.ts.map +1 -1
  230. package/dist/modules/settings/oidc-check.js +5 -17
  231. package/dist/modules/settings/oidc-check.js.map +1 -1
  232. package/dist/modules/skills/index.d.ts +3 -3
  233. package/dist/modules/skills/index.d.ts.map +1 -1
  234. package/dist/modules/skills/index.js +0 -2
  235. package/dist/modules/skills/index.js.map +1 -1
  236. package/dist/modules/skills/skills.contract.d.ts +16 -0
  237. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  238. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  239. package/dist/modules/skills/skills.service.js +6 -1
  240. package/dist/modules/skills/skills.service.js.map +1 -1
  241. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  242. package/dist/modules/skills/skills.tools.js +25 -13
  243. package/dist/modules/skills/skills.tools.js.map +1 -1
  244. package/dist/modules/tool-auth/external-api-key.interface.d.ts +11 -0
  245. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  246. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  247. package/dist/modules/tool-auth/external-api-key.service.d.ts +1 -0
  248. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  249. package/dist/modules/tool-auth/external-api-key.service.js +4 -1
  250. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  251. package/dist/modules/tool-auth/index.d.ts +1 -1
  252. package/dist/modules/tool-auth/index.d.ts.map +1 -1
  253. package/dist/modules/tool-auth/internal-token.service.d.ts +9 -0
  254. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  255. package/dist/modules/tool-auth/internal-token.service.js +1 -0
  256. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  257. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +27 -3
  258. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  259. package/dist/modules/tool-auth/tool-auth.middleware.js +32 -6
  260. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  261. package/dist/modules/tool-helpers/tool-context.d.ts +7 -0
  262. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  263. package/dist/modules/tool-helpers/tool-context.js +3 -0
  264. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  265. package/dist/modules/tool-helpers/tool-def.js +1 -1
  266. package/dist/modules/tool-helpers/tool-def.js.map +1 -1
  267. package/dist/modules/tool-helpers/tool-handler.d.ts +15 -0
  268. package/dist/modules/tool-helpers/tool-handler.d.ts.map +1 -1
  269. package/dist/modules/tool-helpers/tool-handler.js +19 -0
  270. package/dist/modules/tool-helpers/tool-handler.js.map +1 -1
  271. package/dist/modules/tool-helpers/tool.contract.d.ts +10 -0
  272. package/dist/modules/tool-helpers/tool.contract.d.ts.map +1 -1
  273. package/dist/modules/tool-helpers/tool.contract.js.map +1 -1
  274. package/dist/modules/tool-helpers/validate-token.d.ts +3 -1
  275. package/dist/modules/tool-helpers/validate-token.d.ts.map +1 -1
  276. package/dist/modules/tool-helpers/validate-token.js +1 -1
  277. package/dist/modules/tool-helpers/validate-token.js.map +1 -1
  278. package/dist/modules/tool-manuals/index.d.ts +3 -3
  279. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  280. package/dist/modules/tool-manuals/index.js +0 -2
  281. package/dist/modules/tool-manuals/index.js.map +1 -1
  282. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +7 -1
  283. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  284. package/dist/modules/tool-manuals/mcp-json-discovery.js +69 -38
  285. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  286. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +46 -6
  287. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  288. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +7 -1
  289. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  290. package/dist/modules/tool-manuals/tool-manuals.routes.js +41 -4
  291. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  292. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +24 -13
  293. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  294. package/dist/modules/tool-manuals/tool-manuals.service.js +138 -59
  295. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  296. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +8 -2
  297. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  298. package/dist/modules/tool-manuals/tool-manuals.tools.js +56 -20
  299. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  300. package/dist/modules/tool-registry/manual.routes.d.ts +8 -1
  301. package/dist/modules/tool-registry/manual.routes.d.ts.map +1 -1
  302. package/dist/modules/tool-registry/manual.routes.js +27 -7
  303. package/dist/modules/tool-registry/manual.routes.js.map +1 -1
  304. package/dist/modules/tool-registry/tool.contract.d.ts +7 -0
  305. package/dist/modules/tool-registry/tool.contract.d.ts.map +1 -1
  306. package/dist/modules/tool-registry/tool.contract.js.map +1 -1
  307. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  308. package/dist/modules/workflow/agent-tools/workflow.tools.js +36 -27
  309. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  310. package/dist/modules/workflow/git/git.service.d.ts +2 -2
  311. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  312. package/dist/modules/workflow/git/git.service.js +20 -35
  313. package/dist/modules/workflow/git/git.service.js.map +1 -1
  314. package/dist/modules/workflow/workflow.service.d.ts +2 -1
  315. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  316. package/dist/modules/workflow/workflow.service.js +3 -0
  317. package/dist/modules/workflow/workflow.service.js.map +1 -1
  318. package/dist/modules/workspace/agent-access.gate.d.ts +1 -1
  319. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -1
  320. package/dist/modules/workspace/agent-access.gate.js +1 -1
  321. package/dist/modules/workspace/agent-access.gate.js.map +1 -1
  322. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts +28 -4
  323. package/dist/modules/workspace/file-readers/doc-extract.service.d.ts.map +1 -1
  324. package/dist/modules/workspace/file-readers/doc-extract.service.js +61 -40
  325. package/dist/modules/workspace/file-readers/doc-extract.service.js.map +1 -1
  326. package/dist/modules/workspace/index.d.ts +8 -0
  327. package/dist/modules/workspace/index.d.ts.map +1 -1
  328. package/dist/modules/workspace/index.js +8 -0
  329. package/dist/modules/workspace/index.js.map +1 -1
  330. package/dist/modules/workspace/nul-scan.d.ts +19 -0
  331. package/dist/modules/workspace/nul-scan.d.ts.map +1 -0
  332. package/dist/modules/workspace/nul-scan.js +55 -0
  333. package/dist/modules/workspace/nul-scan.js.map +1 -0
  334. package/dist/modules/workspace/pattern-matcher.contract.d.ts +110 -27
  335. package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -1
  336. package/dist/modules/workspace/pattern-matcher.contract.js +30 -13
  337. package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -1
  338. package/dist/modules/workspace/ripgrep-search.d.ts +54 -0
  339. package/dist/modules/workspace/ripgrep-search.d.ts.map +1 -0
  340. package/dist/modules/workspace/ripgrep-search.js +657 -0
  341. package/dist/modules/workspace/ripgrep-search.js.map +1 -0
  342. package/dist/modules/workspace/ripgrep-version.d.ts +15 -0
  343. package/dist/modules/workspace/ripgrep-version.d.ts.map +1 -0
  344. package/dist/modules/workspace/ripgrep-version.js +31 -0
  345. package/dist/modules/workspace/ripgrep-version.js.map +1 -0
  346. package/dist/modules/workspace/startup/beside-checkout.d.ts +8 -5
  347. package/dist/modules/workspace/startup/beside-checkout.d.ts.map +1 -1
  348. package/dist/modules/workspace/startup/beside-checkout.js +14 -5
  349. package/dist/modules/workspace/startup/beside-checkout.js.map +1 -1
  350. package/dist/modules/workspace/worker-pool.d.ts +3 -4
  351. package/dist/modules/workspace/worker-pool.d.ts.map +1 -1
  352. package/dist/modules/workspace/worker-pool.js.map +1 -1
  353. package/dist/modules/workspace/workspace-tmp.d.ts +146 -0
  354. package/dist/modules/workspace/workspace-tmp.d.ts.map +1 -0
  355. package/dist/modules/workspace/workspace-tmp.js +435 -0
  356. package/dist/modules/workspace/workspace-tmp.js.map +1 -0
  357. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  358. package/dist/modules/workspace/workspace.routes.js +5 -26
  359. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  360. package/dist/modules/workspace/workspace.service.d.ts +47 -6
  361. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  362. package/dist/modules/workspace/workspace.service.js +86 -6
  363. package/dist/modules/workspace/workspace.service.js.map +1 -1
  364. package/dist/modules/workspace/workspace.tools.d.ts +3 -2
  365. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  366. package/dist/modules/workspace/workspace.tools.js +531 -137
  367. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  368. package/dist/shared/oauth-token-error.d.ts +23 -0
  369. package/dist/shared/oauth-token-error.d.ts.map +1 -0
  370. package/dist/shared/oauth-token-error.js +47 -0
  371. package/dist/shared/oauth-token-error.js.map +1 -0
  372. package/dist/shared/path-order.d.ts +28 -8
  373. package/dist/shared/path-order.d.ts.map +1 -1
  374. package/dist/shared/path-order.js +37 -8
  375. package/dist/shared/path-order.js.map +1 -1
  376. package/dist/shared/read-capped.d.ts +19 -0
  377. package/dist/shared/read-capped.d.ts.map +1 -0
  378. package/dist/shared/read-capped.js +45 -0
  379. package/dist/shared/read-capped.js.map +1 -0
  380. package/migrations/0022_session_version.sql +6 -0
  381. package/migrations/0023_agent_path.sql +3 -0
  382. package/migrations/meta/0022_snapshot.json +2360 -0
  383. package/migrations/meta/0023_snapshot.json +2378 -0
  384. package/migrations/meta/_journal.json +14 -0
  385. package/package.json +4 -3
  386. package/src/__tests__/hexis-catalog.ts +144 -0
  387. package/src/__tests__/pg-scratch.test.ts +69 -0
  388. package/src/__tests__/pg-scratch.ts +31 -0
  389. package/src/__tests__/real-disk.ts +21 -0
  390. package/src/__tests__/slow-ripgrep.ts +65 -0
  391. package/src/__tests__/tokens.ts +19 -0
  392. package/src/__tests__/tool-manuals.ts +32 -0
  393. package/src/__tests__/tool-wiring.ts +25 -0
  394. package/src/core/__tests__/boot-without-ripgrep.test.ts +34 -0
  395. package/src/core/__tests__/catalog-revision.test.ts +89 -0
  396. package/src/core/__tests__/damaged-document.tools.test.ts +2 -2
  397. package/src/core/__tests__/gated-boot.test.ts +8 -1
  398. package/src/core/__tests__/lifecycle.test.ts +46 -13
  399. package/src/core/__tests__/oauth-sign-in-real-file.test.ts +179 -0
  400. package/src/core/catalog-revision.ts +24 -2
  401. package/src/core/core-ports.ts +42 -0
  402. package/src/core/create-core-server.ts +14 -1
  403. package/src/core/create-core-services.ts +62 -15
  404. package/src/core/lifecycle.ts +26 -2
  405. package/src/index.ts +1 -1
  406. package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +12 -0
  407. package/src/modules/access/__tests__/access-control.colliding-name-cases.test.ts +113 -0
  408. package/src/modules/access/__tests__/access-control.frontmatter-head.test.ts +117 -0
  409. package/src/modules/access/__tests__/access-control.group-named-admin.test.ts +122 -0
  410. package/src/modules/access/__tests__/access-control.preview-relocation.test.ts +50 -0
  411. package/src/modules/access/__tests__/access-declarations.test.ts +19 -0
  412. package/src/modules/access/__tests__/access-groups.test.ts +55 -22
  413. package/src/modules/access/__tests__/access-mutation.service.test.ts +85 -59
  414. package/src/modules/access/__tests__/access-restrict-here.test.ts +7 -5
  415. package/src/modules/access/__tests__/access.routes.admins.test.ts +1 -0
  416. package/src/modules/access/__tests__/access.routes.batch.test.ts +2 -0
  417. package/src/modules/access/__tests__/access.routes.folder-governs.test.ts +2 -0
  418. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +50 -14
  419. package/src/modules/access/__tests__/access.routes.overrides.test.ts +2 -1
  420. package/src/modules/access/__tests__/access.routes.proposal-branch.test.ts +1 -0
  421. package/src/modules/access/__tests__/access.routes.prospective.test.ts +52 -10
  422. package/src/modules/access/__tests__/access.routes.revoke.test.ts +18 -13
  423. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
  424. package/src/modules/access/__tests__/grant-sources.test.ts +20 -15
  425. package/src/modules/access/__tests__/groups-admin.routes.test.ts +61 -0
  426. package/src/modules/access/__tests__/groups-admin.service.test.ts +211 -18
  427. package/src/modules/access/__tests__/holder-changes.test.ts +127 -0
  428. package/src/modules/access/__tests__/reference-scan.test.ts +366 -14
  429. package/src/modules/access/__tests__/roles-admin.security.test.ts +1 -1
  430. package/src/modules/access/__tests__/roles-admin.service.test.ts +43 -22
  431. package/src/modules/access/__tests__/roles-capabilities.test.ts +6 -2
  432. package/src/modules/access/__tests__/roles.routes.test.ts +1 -1
  433. package/src/modules/access/access-control.interface.ts +43 -10
  434. package/src/modules/access/access-control.service.ts +250 -72
  435. package/src/modules/access/access-mutation.service.ts +48 -53
  436. package/src/modules/access/access-view.ts +12 -10
  437. package/src/modules/access/access.routes.ts +66 -37
  438. package/src/modules/access/groups-admin.routes.ts +4 -2
  439. package/src/modules/access/groups-admin.service.ts +114 -44
  440. package/src/modules/access/groups-edit.ts +1 -1
  441. package/src/modules/access/index.ts +1 -1
  442. package/src/modules/access/reference-scan.ts +319 -46
  443. package/src/modules/access/roles-admin.service.ts +43 -26
  444. package/src/modules/access/synced-groups-writer.ts +1 -1
  445. package/src/modules/access-model/__tests__/access-grammar.test.ts +38 -1
  446. package/src/modules/access-model/__tests__/access-splice.test.ts +50 -8
  447. package/src/modules/access-model/access-grammar.ts +65 -20
  448. package/src/modules/access-model/access-splice.ts +31 -23
  449. package/src/modules/access-model/group-files.ts +5 -0
  450. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +110 -25
  451. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +38 -11
  452. package/src/modules/agent-guide/__tests__/guide-budget.test.ts +95 -0
  453. package/src/modules/agent-guide/__tests__/guide-eval.test.ts +182 -0
  454. package/src/modules/agent-guide/__tests__/guide-matches-code.test.ts +263 -0
  455. package/src/modules/agent-guide/__tests__/tool-surface.json +514 -0
  456. package/src/modules/agent-guide/__tests__/tool-surface.test.ts +75 -0
  457. package/src/modules/agent-guide/agent-guide.tools.ts +51 -22
  458. package/src/modules/agent-guide/agent-guide.ts +120 -13
  459. package/src/modules/agent-guide/guide-budget.ts +73 -0
  460. package/src/modules/agent-guide/index.ts +12 -0
  461. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +5 -4
  462. package/src/modules/agent-instructions/compose.ts +7 -1
  463. package/src/modules/agent-instructions/shared-file-rules.ts +27 -9
  464. package/src/modules/agent-scope/__tests__/agent-surfaces.connect-as.test.ts +421 -0
  465. package/src/modules/agent-scope/__tests__/connection-scope.test.ts +46 -0
  466. package/src/modules/agent-scope/__tests__/in-memory-kb.ts +148 -0
  467. package/src/modules/agent-scope/agent-scope.contract.ts +83 -0
  468. package/src/modules/agent-scope/connection-scope.ts +199 -0
  469. package/src/modules/agent-scope/index.ts +15 -0
  470. package/src/modules/audit/agent-audit.service.ts +2 -0
  471. package/src/modules/audit/audit.contract.ts +6 -0
  472. package/src/modules/auth/__tests__/auth.middleware.test.ts +30 -2
  473. package/src/modules/auth/__tests__/auth.routes.test.ts +71 -5
  474. package/src/modules/auth/__tests__/auth.service.test.ts +90 -9
  475. package/src/modules/auth/__tests__/password-change-sessions.pg.test.ts +269 -0
  476. package/src/modules/auth/account-admission.ts +19 -0
  477. package/src/modules/auth/auth.middleware.ts +45 -4
  478. package/src/modules/auth/auth.routes.ts +46 -29
  479. package/src/modules/auth/auth.service.ts +135 -34
  480. package/src/modules/auth/index.ts +2 -0
  481. package/src/modules/auth/oidc-auth-provider.ts +2 -9
  482. package/src/modules/code-mode/code-mode.tool.ts +2 -2
  483. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +5 -24
  484. package/src/modules/connection-probe/connection-probe.service.ts +1 -34
  485. package/src/modules/database/__tests__/session-version-migration.pg.test.ts +133 -0
  486. package/src/modules/database/core-schema.ts +33 -0
  487. package/src/modules/declared-variables/__tests__/declared-variables.route.test.ts +44 -1
  488. package/src/modules/declared-variables/declared-variables.routes.ts +12 -1
  489. package/src/modules/embed/__tests__/embed.tools.test.ts +12 -0
  490. package/src/modules/embed/embed.tools.ts +5 -0
  491. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +25 -0
  492. package/src/modules/kb-fs/__tests__/repo-path.test.ts +53 -8
  493. package/src/modules/kb-fs/index.ts +2 -0
  494. package/src/modules/kb-fs/locking-filesystem.ts +7 -1
  495. package/src/modules/kb-fs/repo-path.ts +33 -7
  496. package/src/modules/kb-fs/tmp-ref.ts +81 -0
  497. package/src/modules/mcp/__tests__/connect-as.pg.test.ts +137 -0
  498. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +29 -5
  499. package/src/modules/mcp/__tests__/mcp-routes-harness.ts +8 -0
  500. package/src/modules/mcp/__tests__/mcp.connect-as.test.ts +195 -0
  501. package/src/modules/mcp/__tests__/mcp.routes.connect-as.test.ts +192 -0
  502. package/src/modules/mcp/__tests__/oauth-consent.connect-as.test.ts +146 -0
  503. package/src/modules/mcp/mcp-auth.middleware.ts +19 -2
  504. package/src/modules/mcp/mcp.routes.ts +39 -3
  505. package/src/modules/mcp/mcp.service.ts +112 -52
  506. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +46 -8
  507. package/src/modules/mcp/oauth/oauth-consent.routes.ts +41 -1
  508. package/src/modules/plugins/__tests__/plugins.tools.test.ts +38 -2
  509. package/src/modules/plugins/index.ts +1 -1
  510. package/src/modules/plugins/plugin-links.ts +19 -2
  511. package/src/modules/plugins/plugins.routes.ts +35 -4
  512. package/src/modules/secrets-vault/__tests__/connect-pending.route.test.ts +113 -0
  513. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +430 -3
  514. package/src/modules/secrets-vault/__tests__/oauth-live-declaration.route.test.ts +218 -0
  515. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +39 -6
  516. package/src/modules/secrets-vault/db-secrets-vault.service.ts +170 -27
  517. package/src/modules/secrets-vault/index.ts +1 -1
  518. package/src/modules/secrets-vault/secrets-vault.contract.ts +61 -11
  519. package/src/modules/secrets-vault/secrets-vault.routes.ts +114 -42
  520. package/src/modules/settings/oidc-check.ts +5 -18
  521. package/src/modules/skills/__tests__/skills.service.test.ts +34 -0
  522. package/src/modules/skills/index.ts +7 -2
  523. package/src/modules/skills/skills.contract.ts +17 -0
  524. package/src/modules/skills/skills.service.ts +5 -1
  525. package/src/modules/skills/skills.tools.ts +45 -14
  526. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +4 -0
  527. package/src/modules/tool-auth/external-api-key.interface.ts +11 -1
  528. package/src/modules/tool-auth/external-api-key.service.ts +5 -2
  529. package/src/modules/tool-auth/index.ts +1 -1
  530. package/src/modules/tool-auth/internal-token.service.ts +10 -0
  531. package/src/modules/tool-auth/tool-auth.middleware.ts +45 -3
  532. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +19 -11
  533. package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +3 -1
  534. package/src/modules/tool-helpers/__tests__/route-tool-schemas.catalog.test.ts +38 -0
  535. package/src/modules/tool-helpers/tool-context.ts +10 -0
  536. package/src/modules/tool-helpers/tool-def.ts +1 -1
  537. package/src/modules/tool-helpers/tool-handler.ts +34 -0
  538. package/src/modules/tool-helpers/tool.contract.ts +10 -0
  539. package/src/modules/tool-helpers/validate-token.ts +4 -2
  540. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +80 -4
  541. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +197 -11
  542. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +42 -0
  543. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +99 -8
  544. package/src/modules/tool-manuals/index.ts +6 -2
  545. package/src/modules/tool-manuals/mcp-json-discovery.ts +73 -34
  546. package/src/modules/tool-manuals/tool-manuals.contract.ts +40 -6
  547. package/src/modules/tool-manuals/tool-manuals.routes.ts +40 -3
  548. package/src/modules/tool-manuals/tool-manuals.service.ts +169 -61
  549. package/src/modules/tool-manuals/tool-manuals.tools.ts +81 -25
  550. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +6 -113
  551. package/src/modules/tool-registry/manual.routes.ts +26 -5
  552. package/src/modules/tool-registry/tool.contract.ts +7 -0
  553. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  554. package/src/modules/workflow/__tests__/workflow.service.sync.test.ts +1 -1
  555. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +54 -3
  556. package/src/modules/workflow/agent-tools/workflow.tools.ts +36 -27
  557. package/src/modules/workflow/git/__tests__/git.service.list-branches.test.ts +130 -3
  558. package/src/modules/workflow/git/git.service.ts +29 -48
  559. package/src/modules/workflow/workflow.service.ts +5 -0
  560. package/src/modules/workspace/__tests__/git-internals.security.test.ts +2 -2
  561. package/src/modules/workspace/__tests__/holds-nul.test.ts +66 -0
  562. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +3 -3
  563. package/src/modules/workspace/__tests__/ripgrep-search.test.ts +570 -0
  564. package/src/modules/workspace/__tests__/ripgrep-version.test.ts +29 -0
  565. package/src/modules/workspace/__tests__/workspace-tmp.tools.test.ts +928 -0
  566. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +17 -37
  567. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +20 -8
  568. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +10 -12
  569. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +22 -10
  570. package/src/modules/workspace/__tests__/workspace.tools.test.ts +549 -162
  571. package/src/modules/workspace/agent-access.gate.ts +1 -1
  572. package/src/modules/workspace/file-readers/__tests__/doc-extract.service.test.ts +10 -0
  573. package/src/modules/workspace/file-readers/doc-extract.service.ts +71 -38
  574. package/src/modules/workspace/index.ts +20 -0
  575. package/src/modules/workspace/nul-scan.ts +50 -0
  576. package/src/modules/workspace/pattern-matcher.contract.ts +123 -32
  577. package/src/modules/workspace/ripgrep-search.ts +715 -0
  578. package/src/modules/workspace/ripgrep-version.ts +35 -0
  579. package/src/modules/workspace/startup/__tests__/beside-checkout.test.ts +11 -0
  580. package/src/modules/workspace/startup/beside-checkout.ts +13 -5
  581. package/src/modules/workspace/worker-pool.ts +3 -4
  582. package/src/modules/workspace/workspace-tmp.ts +457 -0
  583. package/src/modules/workspace/workspace.routes.ts +6 -27
  584. package/src/modules/workspace/workspace.service.ts +92 -15
  585. package/src/modules/workspace/workspace.tools.ts +574 -176
  586. package/src/shared/__tests__/read-capped.test.ts +68 -0
  587. package/src/shared/oauth-token-error.ts +46 -0
  588. package/src/shared/path-order.ts +37 -8
  589. package/src/shared/read-capped.ts +40 -0
  590. package/starter-packs/engineering/Plugins/engineering-starter/access.md +1 -1
  591. package/starter-packs/general/Plugins/general-starter/access.md +1 -1
  592. package/starter-packs/operations/Plugins/operations-starter/access.md +1 -1
  593. package/starter-packs/sales/Plugins/sales-starter/access.md +1 -1
  594. package/starter-packs/support/Plugins/support-starter/access.md +1 -1
  595. package/agent-guide/finding-things.md +0 -7
  596. package/agent-guide/introduction.md +0 -27
  597. package/dist/modules/workspace/bounded-matcher.d.ts +0 -73
  598. package/dist/modules/workspace/bounded-matcher.d.ts.map +0 -1
  599. package/dist/modules/workspace/bounded-matcher.js +0 -200
  600. package/dist/modules/workspace/bounded-matcher.js.map +0 -1
  601. package/src/modules/workspace/__tests__/bounded-matcher.test.ts +0 -213
  602. package/src/modules/workspace/bounded-matcher.ts +0 -271
@@ -0,0 +1,35 @@
1
+ /**
2
+ * ripgrep is what the agents' `grep` tool searches with (`ripgrep-search.ts`),
3
+ * and there is no other search to fall back on. So its absence is found at
4
+ * boot, the way a git too old is (`git-version.ts`): by the deploy that
5
+ * produced the broken image, never by the first agent that searches.
6
+ *
7
+ * The Hexis Dockerfile installs it from Debian (`ripgrep`); a server run from
8
+ * source needs `rg` on its PATH.
9
+ */
10
+
11
+ import { execFile } from 'node:child_process';
12
+ import { promisify } from 'node:util';
13
+ import { logger } from '../../shared/logging.js';
14
+
15
+ const log = logger('grep');
16
+
17
+ const execFileAsync = promisify(execFile);
18
+
19
+ /**
20
+ * Run `rg --version`, log what answered, and throw a worded error naming the
21
+ * binary when it cannot be run. Call once during startup.
22
+ */
23
+ export async function assertRipgrep(): Promise<void> {
24
+ let stdout: string;
25
+ try {
26
+ ({ stdout } = await execFileAsync('rg', ['--version']));
27
+ } catch (err) {
28
+ throw new Error(
29
+ `ripgrep (\`rg\`) is required on the server's PATH but could not be run: ${String(err)}. ` +
30
+ 'The grep tool searches with it. The Hexis Dockerfile installs it (Debian package `ripgrep`); ' +
31
+ 'a server run from source needs ripgrep installed.',
32
+ );
33
+ }
34
+ log.info(`search engine: ${stdout.split('\n')[0]!.trim()}`);
35
+ }
@@ -61,6 +61,17 @@ describe('noteBesideCheckout', () => {
61
61
  expect(notes).toEqual([]);
62
62
  });
63
63
 
64
+ it("says nothing about the platform's own tmp folder, and names a file or a link squatting its name", async () => {
65
+ await workspace('main', { dirs: ['tmp/u/chat-uploads'] });
66
+ await scan();
67
+ expect(notes).toEqual([]);
68
+ const dir = await workspace('bob%2Fdraft', { files: ['tmp'] });
69
+ await scan();
70
+ expect(notes).toHaveLength(1);
71
+ expect(notes[0]).toContain(`${BESIDE_CHECKOUT_NOTE} "tmp"`);
72
+ await expect(fs.stat(path.join(dir, 'tmp'))).resolves.toBeTruthy();
73
+ });
74
+
64
75
  it('marks the folders with a trailing slash so one line says which is which', async () => {
65
76
  await workspace('main', { dirs: ['Reports'], files: ['Reports.md'] });
66
77
  await scan();
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { logger } from '../../../shared/logging.js';
4
4
  import { isAbsence } from '../../../shared/fs.contract.js';
5
5
  import { printable } from '../../../shared/printable.js';
6
+ import { WORKSPACE_TMP_DIR } from '../workspace-tmp.js';
6
7
 
7
8
  const log = logger('kb-startup');
8
9
 
@@ -26,11 +27,14 @@ export const BESIDE_CHECKOUT_NOTE = 'Beside the checkout, not in the repository:
26
27
  * clone, and a boot that quietly removed it would be a data loss nobody asked
27
28
  * for. The operator cleans up, and the note stops on the next start.
28
29
  *
29
- * EVERYTHING other than the checkout folder is named, the platform's own
30
- * transient `tmp/` scratch included on the rare start that finds one (a run
31
- * clears it when it finishes). That is deliberate: the note's claim is about
32
- * what is not in the repository, and a note that quietly kept exceptions would
33
- * be a note an operator could not trust.
30
+ * EVERYTHING other than the checkout folder and the platform's own `tmp/`
31
+ * folder is named. `tmp/` is the one exception, stated here: it is the
32
+ * platform's (what the chat and the readers stage lives there, reached
33
+ * through the `__tmp__/` ref — see `workspace-tmp.ts`), it has a sweep of its
34
+ * own, and a note that told an operator to remove it by hand would be asking
35
+ * them to delete people's staged files. Everything else is a stray: the note's
36
+ * claim is about what is not in the repository, and a note that quietly kept
37
+ * further exceptions would be a note an operator could not trust.
34
38
  *
35
39
  * NOT a `OnServerStart` step, and deliberately outside the KB startup phase.
36
40
  * That phase is gated — it does nothing until the branch model and the
@@ -65,6 +69,10 @@ export async function noteBesideCheckout(
65
69
  // the service judges it: a clone mounted elsewhere is the operator's
66
70
  // business, here as it is there.
67
71
  if (entry.name === kbDirName && (await isDirectory(path.join(workspaceDir, entry.name)))) continue;
72
+ // The platform's own tmp folder, swept on its own (see the note above).
73
+ // Judged by `lstat`, as the sweep judges it: a `tmp` that is a link, or
74
+ // a file, is not that folder, and is named like any other stray.
75
+ if (entry.name === WORKSPACE_TMP_DIR && entry.isDirectory()) continue;
68
76
  // A trailing slash marks the folders, so one line says which is which
69
77
  // without the operator having to go and look. Then `printable`, because
70
78
  // every name here is disk-controlled text on its way into an operator's
@@ -2,10 +2,9 @@ import type { Worker } from 'node:worker_threads';
2
2
 
3
3
  /**
4
4
  * The pooled worker threads that keep CPU-bound work off the request thread:
5
- * the mechanics every worker kind shares, whatever its worker runs. Two kinds
6
- * use it today, each with a pool of its own — the regex matcher `grep` runs a
7
- * caller's pattern in (`bounded-matcher.ts`) and the document extractor every
8
- * office, PDF and email extraction runs in (`file-readers/extract-in-worker.ts`).
5
+ * the mechanics every worker kind shares, whatever its worker runs. One kind
6
+ * uses it today, with a pool of its own — the document extractor every office,
7
+ * PDF and email extraction runs in (`file-readers/extract-in-worker.ts`).
9
8
  *
10
9
  * What a kind brings is how to start its worker; what the pool owns is the
11
10
  * rest: at most `poolMax` workers, a capped queue of jobs waiting for one,
@@ -0,0 +1,457 @@
1
+ import { promises as fs, type Stats } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { PathNotFoundError, WorkflowDomainError, WorkflowValidationError } from '../../shared/domain-errors.js';
4
+ import { assertWithinDirectory } from '../../shared/path-containment.js';
5
+ import { sanitizedPath } from '../../shared/printable.js';
6
+ import { hasControlCharacter, TMP_REF_PREFIX } from '../kb-fs/index.js';
7
+ import { isAbsence, type ITreeWalker } from '../../shared/fs.contract.js';
8
+ import { logger } from '../../shared/logging.js';
9
+ import { lstatOrNull } from '../../shared/rename-no-replace.js';
10
+
11
+ const log = logger('workspace-tmp');
12
+
13
+ /**
14
+ * A workspace's `tmp/` folder, reached through a reserved ref.
15
+ *
16
+ * Every workspace directory holds its checkout and, beside it, a `tmp/`
17
+ * folder: the chat stages attachments there, the SharePoint reader stages
18
+ * documents there, and nothing in it is ever committed. The file tools place
19
+ * every path under the checkout (see `kb-fs/repo-path.ts`), so the folder is
20
+ * named by a REF instead — `__tmp__/<path>` — which `read_file`, `file_stat`,
21
+ * `grep` and `copy_file` (as its source) accept beside a repository path, and
22
+ * every other tool refuses (`kb-fs/tmp-ref.ts`, the one admission gate every
23
+ * tool's path inputs pass). A plain `tmp/…` path is not that ref and never
24
+ * will be: `tmp` is a legal folder name inside a repository, and the
25
+ * normaliser's rule is what keeps every other path in it.
26
+ *
27
+ * The ref is scoped twice. To the WORKSPACE: it is resolved against the
28
+ * workspace of the branch the call names, exactly like a repository path. And
29
+ * to the CALLER: what a person stages lands under their own folder,
30
+ * `tmp/<owner id>/` ({@link stageTmpRef}, the one way in), and a ref resolves
31
+ * inside the caller's own folder and nowhere else ({@link resolveTmpRef}). A
32
+ * file somebody else staged is therefore not there — the plain not-found,
33
+ * the same answer as for a file that never existed — rather than something an
34
+ * access rule has to refuse: the folder is outside the repository, where no
35
+ * `access.md` governs anything, and the only person a staged attachment is
36
+ * for is the one who attached it. The ref names ONE file — no folder, no
37
+ * listing — and nothing outside the caller's folder is reachable through it,
38
+ * links included. Every resolution goes through {@link resolveTmpRef}; there
39
+ * is no second reader of the folder.
40
+ *
41
+ * Entries live for {@link TMP_MAX_AGE_MS} after their last modification, then
42
+ * the platform's periodic sweep removes them ({@link sweepTmpFolder},
43
+ * scheduled by {@link TmpSweeper}) — or sooner, when a distribution clears a
44
+ * workspace's whole `tmp/` after a run of its own (`clearTmp`): "up to seven
45
+ * days" is the promise, not "seven days".
46
+ */
47
+
48
+ /** The folder the ref reaches, beside the checkout in every workspace directory. */
49
+ export const WORKSPACE_TMP_DIR = 'tmp';
50
+
51
+ /** How long an entry of a `tmp/` folder lives after its last modification: seven days. */
52
+ export const TMP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
53
+
54
+ /** How often the platform sweeps every workspace's `tmp/` folder: hourly. */
55
+ export const TMP_SWEEP_INTERVAL_MS = 60 * 60 * 1000;
56
+
57
+ /** A ref that breaks one of the rules, with the rule named. */
58
+ function invalidRef(ref: string, rule: string): WorkflowValidationError {
59
+ const shown = sanitizedPath(ref);
60
+ return new WorkflowValidationError(`"${shown}" is not a valid tmp ref: ${rule}`, { kind: 'tmp-ref-invalid', path: shown });
61
+ }
62
+
63
+ /**
64
+ * The folder of one owner inside a workspace's `tmp/`: `tmp/<owner id>/`.
65
+ * The id is an account id, which the platform mints and which is one path
66
+ * segment by construction; one that is not — empty, a separator in it, a
67
+ * `.`/`..`, a control character — is a programming error, never a file to
68
+ * look for, and is refused as one.
69
+ */
70
+ function ownerFolder(workspaceDir: string, ownerId: string): string {
71
+ if (
72
+ ownerId === '' ||
73
+ ownerId === '.' ||
74
+ ownerId === '..' ||
75
+ ownerId.includes('/') ||
76
+ ownerId.includes('\\') ||
77
+ hasControlCharacter(ownerId) ||
78
+ ownerId.length > 255
79
+ ) {
80
+ throw new Error(`Invalid tmp owner id "${sanitizedPath(ownerId)}": an owner's tmp folder is one path segment.`);
81
+ }
82
+ return path.join(workspaceDir, WORKSPACE_TMP_DIR, ownerId);
83
+ }
84
+
85
+ /**
86
+ * What a failed look at the folder answers, naming the REF and never the path
87
+ * on disk — the raw error (which carries that path) is the server log's, not
88
+ * the caller's: nothing there is the ordinary not-found; a loop of links is
89
+ * refused by the link rule; anything else (a permission, an I/O error) is a
90
+ * 500 that says the ref could not be read.
91
+ */
92
+ function tmpRefFailure(ref: string, err: unknown): Error {
93
+ if (isAbsence(err)) return new PathNotFoundError(ref);
94
+ const shown = sanitizedPath(ref);
95
+ const code = (err as { code?: unknown } | null)?.code;
96
+ if (code === 'ELOOP') {
97
+ return invalidRef(ref, 'it goes through a link that cannot be followed (a link that leads back to itself).');
98
+ }
99
+ log.warn('could not read a tmp ref:', { err, ref: shown });
100
+ return new WorkflowDomainError(`"${shown}" could not be read from the workspace's tmp folder.`, 500, {
101
+ kind: 'tmp-ref-unreadable',
102
+ path: shown,
103
+ });
104
+ }
105
+
106
+ /**
107
+ * The ref's path inside the `tmp/` folder, as segments — or the refusal that
108
+ * names the rule it breaks. Lexical only: the folder and link rules need the
109
+ * disk and are {@link resolveTmpRef}'s.
110
+ */
111
+ function tmpRefSegments(ref: string): string[] {
112
+ const rest = ref.startsWith(TMP_REF_PREFIX) ? ref.slice(TMP_REF_PREFIX.length) : '';
113
+ // First, because the refusal below would otherwise quote the character
114
+ // that forges a second line — and a line break is a separator to git and
115
+ // to every line-oriented log this ref may reach.
116
+ if (hasControlCharacter(ref)) {
117
+ throw invalidRef(ref, 'it contains a control character (a line break, a NUL or another invisible byte), which is part of no file name.');
118
+ }
119
+ if (rest === '') {
120
+ throw invalidRef(ref, `it names the tmp folder itself. A tmp ref names one file: \`${TMP_REF_PREFIX}<path to the file>\`.`);
121
+ }
122
+ if (rest.startsWith('/') || /^[A-Za-z]:/.test(rest)) {
123
+ throw invalidRef(ref, 'the path after the prefix is absolute. It is a path inside the workspace\'s tmp folder, written without a leading slash.');
124
+ }
125
+ if (rest.includes('\\')) {
126
+ throw invalidRef(ref, 'it contains a backslash. Separate folders with `/`.');
127
+ }
128
+ if (rest.endsWith('/')) {
129
+ throw invalidRef(ref, 'it ends with `/`, which names a folder. A tmp ref names one file; the tmp folder is not listed.');
130
+ }
131
+ const segments = rest.split('/');
132
+ if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
133
+ throw invalidRef(ref, 'it contains a `.` or `..` segment (or an empty one). A tmp ref names one file inside the workspace\'s tmp folder and cannot climb out of it.');
134
+ }
135
+ return segments;
136
+ }
137
+
138
+ /**
139
+ * `folder` is absent, or a real directory — never a link, never a file. The
140
+ * `tmp/` folder and an owner's folder are judged by this before anything
141
+ * resolves or lands under them: a link standing in either place would carry
142
+ * the whole folder somewhere else, and a real-path containment check judged
143
+ * against the link's target would not notice.
144
+ */
145
+ async function assertOwnFolder(folder: string, ref: string, failed: (err: unknown) => never): Promise<void> {
146
+ const stat = await lstatOrNull(folder).catch(failed);
147
+ if (stat !== null && !stat.isDirectory()) {
148
+ throw invalidRef(ref, "its tmp folder is not a folder of this workspace's own (a link or a file stands where it should be).");
149
+ }
150
+ }
151
+
152
+ /**
153
+ * THE resolution of a ref, against one owner's folder in one workspace
154
+ * directory: the file's absolute path and its stat, or a refusal. Every read
155
+ * of the folder starts here.
156
+ *
157
+ * - The lexical rules first ({@link tmpRefSegments}), before the disk is
158
+ * asked anything.
159
+ * - Nothing there — the owner's folder included, which a person who has
160
+ * staged nothing does not have — is a {@link PathNotFoundError} naming the
161
+ * REF, never the path on disk: the answer every file tool turns into its
162
+ * ordinary 404. What another person staged is "nothing there" too. Every
163
+ * other failure names the ref alone as well ({@link tmpRefFailure}).
164
+ * - The file's REAL path must lie inside the owner's folder's real path, so a
165
+ * link — anywhere along the ref — that leads out of it (into another
166
+ * owner's folder included) is refused rather than followed. A link that
167
+ * stays inside is the file it points at.
168
+ * - A folder is refused by the rule, never listed.
169
+ */
170
+ export async function resolveTmpRef(
171
+ workspaceDir: string,
172
+ ownerId: string,
173
+ ref: string,
174
+ ): Promise<{ absolutePath: string; stat: Stats }> {
175
+ const segments = tmpRefSegments(ref);
176
+ const ownerDir = ownerFolder(workspaceDir, ownerId);
177
+ const absent = (err: unknown): never => {
178
+ throw tmpRefFailure(ref, err);
179
+ };
180
+ // `tmp/` and the owner's folder must BE folders of this workspace, not
181
+ // links to ones somewhere else: the containment below is judged against
182
+ // the owner's folder's real path, which a link would move along with it.
183
+ await assertOwnFolder(path.join(workspaceDir, WORKSPACE_TMP_DIR), ref, absent);
184
+ await assertOwnFolder(ownerDir, ref, absent);
185
+ if ((await lstatOrNull(ownerDir).catch(absent)) === null) throw new PathNotFoundError(ref);
186
+ const realOwnerDir = await fs.realpath(ownerDir).catch(absent);
187
+ const real = await fs.realpath(path.join(ownerDir, ...segments)).catch(absent);
188
+ // THE containment check (shared/path-containment.ts), on the real paths;
189
+ // its refusal is answered as the link rule, naming the ref. The folder
190
+ // itself passes it, and is refused below as a folder.
191
+ try {
192
+ assertWithinDirectory(real, realOwnerDir);
193
+ } catch {
194
+ throw invalidRef(ref, 'it goes through a link that leads out of your tmp folder, and links out of it are never followed.');
195
+ }
196
+ const stat = await fs.stat(real).catch(absent);
197
+ if (stat.isDirectory()) {
198
+ throw invalidRef(ref, 'it names a folder. A tmp ref names one file; the tmp folder is not listed.');
199
+ }
200
+ if (!stat.isFile()) {
201
+ throw invalidRef(ref, 'it names something that is not a file.');
202
+ }
203
+ return { absolutePath: real, stat };
204
+ }
205
+
206
+ /** The bytes of the one file a ref names in `ownerId`'s folder of `workspaceDir`'s tmp (see {@link resolveTmpRef}). */
207
+ export async function readTmpRef(workspaceDir: string, ownerId: string, ref: string): Promise<Buffer> {
208
+ const { absolutePath } = await resolveTmpRef(workspaceDir, ownerId, ref);
209
+ return fs.readFile(absolutePath).catch((err: unknown) => {
210
+ // Swept or replaced between the resolve and the read: the same answer a
211
+ // moment later would give, still naming the ref.
212
+ throw tmpRefFailure(ref, err);
213
+ });
214
+ }
215
+
216
+ /** What `file_stat` reports about a staged file: the fields a repository file's stat carries. */
217
+ export interface TmpFileStat {
218
+ /** The file's own name, the last segment of the ref. */
219
+ name: string;
220
+ size: number;
221
+ createdAt: Date;
222
+ modifiedAt: Date;
223
+ }
224
+
225
+ /** The stat of the one file a ref names (see {@link resolveTmpRef}). */
226
+ export async function statTmpRef(workspaceDir: string, ownerId: string, ref: string): Promise<TmpFileStat> {
227
+ const { stat } = await resolveTmpRef(workspaceDir, ownerId, ref);
228
+ return { name: ref.slice(ref.lastIndexOf('/') + 1), size: stat.size, createdAt: stat.birthtime, modifiedAt: stat.mtime };
229
+ }
230
+
231
+ /**
232
+ * Stage `bytes` at `relativePath` in `ownerId`'s folder of `workspaceDir`'s
233
+ * tmp, and answer the ref the owner reads it back with: the ONE way a file
234
+ * gets into the folder the ref reaches, so the convention — one folder per
235
+ * owner, the file's path inside it — is this function's and no stager's.
236
+ * `relativePath` obeys a ref's own rules (one file, no `..`, no absolute
237
+ * path, no backslash, no control character); the refusal names the rule.
238
+ * Replaces the FILE at the path: a staged file is the stager's own scratch,
239
+ * and the chat names uploads by their content. Never writes through a link:
240
+ * not one standing where `tmp/` or the owner's folder should be, not one on
241
+ * the way, and not one at the file's own name (judged by `lstat`, which is
242
+ * what every platform has; the check and the write are two steps, as they
243
+ * are everywhere else in this file).
244
+ */
245
+ export async function stageTmpRef(workspaceDir: string, ownerId: string, relativePath: string, bytes: Uint8Array): Promise<string> {
246
+ const ref = TMP_REF_PREFIX + relativePath;
247
+ const segments = tmpRefSegments(ref);
248
+ const ownerDir = ownerFolder(workspaceDir, ownerId);
249
+ const rethrow = (err: unknown): never => {
250
+ throw err;
251
+ };
252
+ await assertOwnFolder(path.join(workspaceDir, WORKSPACE_TMP_DIR), ref, rethrow);
253
+ await assertOwnFolder(ownerDir, ref, rethrow);
254
+ // The workspace directory itself may not exist yet: a staged file belongs
255
+ // beside the checkout whether or not the branch is cloned, and the
256
+ // directory's path is the root's own (`workspaceDirWithinRoot`), nothing
257
+ // the ref names — so it is simply made.
258
+ await fs.mkdir(workspaceDir, { recursive: true });
259
+ // The folders on the way — `tmp/` and the owner's folder included, when a
260
+ // first staging has yet to make them — made one at a time and each judged
261
+ // before the next is entered: never through a link, which a recursive
262
+ // mkdir would follow, creating folders wherever the link leads before
263
+ // anything could refuse it. The one rule for the whole path, the folders
264
+ // already there included.
265
+ await makeFoldersWithin(workspaceDir, [WORKSPACE_TMP_DIR, ownerId, ...segments.slice(0, -1)], ref);
266
+ const target = path.join(ownerDir, ...segments);
267
+ // The name itself: a file to replace, nothing yet, or something that is
268
+ // neither — a link (which a write would follow) or a folder — refused.
269
+ const standing = await lstatOrNull(target);
270
+ if (standing !== null && !standing.isFile()) {
271
+ throw invalidRef(ref, 'it names something that is not a file.');
272
+ }
273
+ await fs.writeFile(target, bytes);
274
+ return ref;
275
+ }
276
+
277
+ /**
278
+ * The folders of `segments` under `base`, each one entered only once it is
279
+ * known to be a real directory of the owner's folder: a missing one is made
280
+ * (and judged again, since another stage may have made it meanwhile), and a
281
+ * link or a file standing in its place is refused as the link rule — before
282
+ * any folder is made beyond it, so a link on the way creates nothing where it
283
+ * leads. `lstat`, as everywhere in this file, so a link is seen as a link.
284
+ */
285
+ async function makeFoldersWithin(base: string, segments: readonly string[], ref: string): Promise<void> {
286
+ let at = base;
287
+ for (const segment of segments) {
288
+ at = path.join(at, segment);
289
+ let standing = await lstatOrNull(at);
290
+ if (standing === null) {
291
+ await fs.mkdir(at).catch((err: unknown) => {
292
+ if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;
293
+ });
294
+ standing = await lstatOrNull(at);
295
+ }
296
+ if (standing === null || !standing.isDirectory()) {
297
+ throw invalidRef(ref, 'it goes through a link that leads out of your tmp folder, and links out of it are never followed.');
298
+ }
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Remove every file of one `tmp/` folder, at any depth, whose last
304
+ * modification is older than `maxAgeMs`, and every folder in it that the
305
+ * sweep leaves empty and that was itself older than that before the sweep
306
+ * touched it. Judged per FILE, because the stagers keep a long-lived folder
307
+ * each (`chat-uploads/`, …) whose own mtime moves with every new upload: a
308
+ * folder judged as a whole would keep an old file for as long as anyone kept
309
+ * uploading beside it. Everything is judged by its own `lstat`, and a link is
310
+ * removed as the link, never followed. The `tmp/` folder itself stays. A
311
+ * folder that is not there is nothing to sweep. Returns the paths removed,
312
+ * relative to `tmp/`.
313
+ *
314
+ * The tree is read by THE walk (`walker`, see `shared/fs.contract.ts`), not a
315
+ * loop of its own: its files, its other entries (a link, a socket — never
316
+ * entered) and its folders are collected, each folder's age read as it is
317
+ * listed, so before anything in it goes. A folder that cannot be listed is
318
+ * the walk's hole: logged, and walked around. The removals follow the walk —
319
+ * the entries first, then the folders deepest first, so a folder emptied by
320
+ * the sweep is judged after its contents.
321
+ *
322
+ * `signal` ends the sweep early: once it is aborted the walk stops listing
323
+ * and nothing more is removed — it is asked again right before each removal,
324
+ * after every awaited look — and what is already gone is returned.
325
+ */
326
+ export async function sweepTmpFolder(
327
+ walker: ITreeWalker,
328
+ workspaceDir: string,
329
+ maxAgeMs: number,
330
+ now: number,
331
+ signal?: AbortSignal,
332
+ ): Promise<string[]> {
333
+ const tmpDir = path.join(workspaceDir, WORKSPACE_TMP_DIR);
334
+ // `lstat` the folder itself: a `tmp` that is a link is not a folder of
335
+ // this workspace's to sweep (the walk would follow it). Not there is
336
+ // nothing to sweep.
337
+ if ((await lstatOrNull(tmpDir))?.isDirectory() !== true) return [];
338
+ const join = (dir: string, name: string): string => (dir === '' ? name : `${dir}/${name}`);
339
+ const entries: string[] = [];
340
+ const oldDirs: string[] = [];
341
+ const stopped = (): boolean => signal?.aborted === true;
342
+ // The entry's own `lstat` (shared/rename-no-replace.ts), or null when it is
343
+ // gone; any other failure spoils that one entry: logged, and left alone.
344
+ const entryStat = async (rel: string): Promise<Stats | null> => {
345
+ try {
346
+ return await lstatOrNull(path.join(tmpDir, rel));
347
+ } catch (err) {
348
+ log.warn('could not stat a tmp entry:', { err, entry: sanitizedPath(rel) });
349
+ return null;
350
+ }
351
+ };
352
+ await walker.walk(tmpDir, { until: stopped }, [
353
+ {
354
+ async onDir(rel) {
355
+ if (rel === '') return;
356
+ // Its age is read BEFORE its contents go: removing them moves it.
357
+ const stat = await entryStat(rel);
358
+ if (stat && now - stat.mtimeMs > maxAgeMs) oldDirs.push(rel);
359
+ },
360
+ onFile(dir, name) {
361
+ entries.push(join(dir, name));
362
+ },
363
+ onOther(dir, entry) {
364
+ entries.push(join(dir, entry.name));
365
+ },
366
+ onHole(rel, err) {
367
+ log.warn('could not list a tmp folder:', { err, folder: sanitizedPath(rel) });
368
+ },
369
+ },
370
+ ]);
371
+ const removed: string[] = [];
372
+ const attempt = async (rel: string, remove: (abs: string) => Promise<void>): Promise<void> => {
373
+ try {
374
+ await remove(path.join(tmpDir, rel));
375
+ removed.push(rel);
376
+ } catch (err) {
377
+ // Gone mid-sweep is what the sweep wanted, and a folder something is
378
+ // still in stays; anything else spoils one entry, not the sweep.
379
+ const code = (err as { code?: unknown } | null)?.code;
380
+ if (!isAbsence(err) && code !== 'ENOTEMPTY' && code !== 'EEXIST') {
381
+ log.warn('could not sweep a tmp entry:', { err, entry: sanitizedPath(rel) });
382
+ }
383
+ }
384
+ };
385
+ for (const rel of entries) {
386
+ if (stopped()) return removed;
387
+ const stat = await entryStat(rel);
388
+ if (!stat || now - stat.mtimeMs <= maxAgeMs) continue;
389
+ // Asked again AFTER the awaited look: a stop that came while it ran
390
+ // removes nothing more, this entry included.
391
+ if (stopped()) return removed;
392
+ // A file, or a link removed as the link.
393
+ await attempt(rel, (abs) => fs.unlink(abs));
394
+ }
395
+ // Walk order lists a folder before what is in it, so the reverse is deepest first.
396
+ for (const rel of oldDirs.reverse()) {
397
+ if (stopped()) return removed;
398
+ await attempt(rel, (abs) => fs.rmdir(abs));
399
+ }
400
+ return removed;
401
+ }
402
+
403
+ /**
404
+ * The periodic sweep of every workspace's `tmp/` folder, on a timer.
405
+ *
406
+ * Started with the graph (`startCore`) and stopped with it (`stopCore`):
407
+ * stopping clears the timer and CANCELS the sweep in flight — its signal is
408
+ * aborted, so it removes nothing more and ends at the next entry it would
409
+ * have looked at — and `drainSweep` waits for it to end. `stopCore` bounds
410
+ * that wait by its own deadline, like every other drain there: a sweep stuck
411
+ * inside one filesystem call (a hung mount) can outlive the wait, but it
412
+ * removes nothing once that call returns. The first sweep runs at start, not
413
+ * one interval later, so what a previous process left past its age goes
414
+ * without waiting. The timer is `unref`'d: nothing here is worth keeping a
415
+ * process alive for.
416
+ */
417
+ export class TmpSweeper {
418
+ private timer: ReturnType<typeof setInterval> | null = null;
419
+ private sweeping: Promise<void> | null = null;
420
+ private stop: AbortController | null = null;
421
+
422
+ constructor(private readonly sweep: (signal: AbortSignal) => Promise<unknown>) {}
423
+
424
+ /** Sweep now, and every `intervalMs` after. Idempotent. */
425
+ startSweeping(intervalMs: number = TMP_SWEEP_INTERVAL_MS): void {
426
+ if (this.timer) return;
427
+ this.stop = new AbortController();
428
+ this.tick();
429
+ this.timer = setInterval(() => this.tick(), intervalMs);
430
+ this.timer.unref?.();
431
+ }
432
+
433
+ /** Clear the timer and cancel the sweep in flight; {@link drainSweep} waits for it to end. */
434
+ stopSweeping(): void {
435
+ if (this.timer) clearInterval(this.timer);
436
+ this.timer = null;
437
+ this.stop?.abort();
438
+ this.stop = null;
439
+ }
440
+
441
+ /** Wait for the sweep in flight, if any, to end. */
442
+ async drainSweep(): Promise<void> {
443
+ await this.sweeping;
444
+ }
445
+
446
+ /** One sweep, unless one is still running; its failure is logged. */
447
+ private tick(): void {
448
+ if (this.sweeping || !this.stop) return;
449
+ const running = this.sweep(this.stop.signal)
450
+ .then(() => undefined)
451
+ .catch((err: unknown) => log.warn('could not sweep the workspaces\' tmp folders:', { err }))
452
+ .finally(() => {
453
+ if (this.sweeping === running) this.sweeping = null;
454
+ });
455
+ this.sweeping = running;
456
+ }
457
+ }
@@ -3,7 +3,7 @@ import { logger } from '../../shared/logging.js';
3
3
 
4
4
  const log = logger('workspace.routes');
5
5
  import path from 'node:path';
6
- import { IGNORE_FILENAME, isAbsence, type ITreeWalker } from '../../shared/fs.contract.js';
6
+ import { isAbsence, type ITreeWalker } from '../../shared/fs.contract.js';
7
7
  import { printable } from '../../shared/printable.js';
8
8
  import type { IAdminAccessService } from '../admin/index.js';
9
9
  import express from 'express';
@@ -652,7 +652,7 @@ export function createWorkspaceRoutes(
652
652
  */
653
653
  function buildTreeReadFilter(workspaceId: string, userEmail: string): ReadTreeFilter {
654
654
  return async (wsRelPaths) => {
655
- const verdict: Map<string, boolean | 'unlisted'> = await resolveReadableMap(
655
+ const verdict: Map<string, boolean> = await resolveReadableMap(
656
656
  (w, e, rels) => accessControl.canReadBatch(w, e, rels),
657
657
  workspaceId,
658
658
  userEmail,
@@ -673,31 +673,10 @@ export function createWorkspaceRoutes(
673
673
  const rel = toKbRelative(wp, kbDirName);
674
674
  if (rel !== null && structuralRoots.has(rel)) verdict.set(wp, true);
675
675
  }
676
-
677
- // `.bevelignore` is ADMIN-ONLY. It is the file that decides what the file
678
- // tree and the agent view show at all, so it is deployment configuration
679
- // rather than knowledge — and it sits alone in being visible: its
680
- // siblings (`.gitignore`, `roles.yaml`, `access.md`, the agent guide) are
681
- // already hidden from every reader by the shipped ignore rules. Somebody
682
- // who cannot act on it gains a puzzle; somebody who can needs to reach it.
683
- //
684
- // Read permission is deliberately NOT the lever. Everyone can read it —
685
- // it has to be readable to be applied — so hiding it is a listing
686
- // decision, made here, rather than an ACL fiction maintained in a file.
687
- //
688
- // Resolved through `AdminAccessService` rather than by asking whether
689
- // this caller can write `roles.yaml` in THIS workspace: admin is settled
690
- // on the default branch precisely so that editing `roles.yaml` on your
691
- // own branch cannot promote you, and the same reasoning applies to
692
- // anything gated on being an admin.
693
- //
694
- // `'unlisted'`, not `false`: hiding it withholds no content, so a
695
- // non-admin in an empty knowledge base (every one ships this file) is
696
- // told it is empty, not that something is being kept from them.
697
- const ignoreFiles = wsRelPaths.filter((wp) => path.basename(wp) === IGNORE_FILENAME);
698
- if (ignoreFiles.length > 0 && !(await adminAccess.isAdmin(userEmail))) {
699
- for (const wp of ignoreFiles) verdict.set(wp, 'unlisted');
700
- }
676
+ // `.bevelignore` is listed like any other file: whoever may read it sees
677
+ // it. It used to be admins only (a listing decision, since everyone can
678
+ // read it) — Razvan reversed that on 2026-10-23: a reader who can open
679
+ // the file may find it in the tree.
701
680
  return verdict;
702
681
  };
703
682
  }