@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
@@ -1,5 +1,5 @@
1
1
  import nodeFs from 'node:fs/promises';
2
- import { join } from 'node:path';
2
+ import { isAbsolute, join, relative, sep } from 'node:path';
3
3
  import AdmZip from 'adm-zip';
4
4
  import type { Router, RequestHandler } from 'express';
5
5
  import type { LocalFilesystem } from '@mastra/core/workspace';
@@ -22,7 +22,9 @@ import {
22
22
  assertInsideRepo,
23
23
  assertRepoRootNameFree,
24
24
  assertRepoRootNameFreeArgs,
25
+ hasControlCharacter,
25
26
  isInsideRepo,
27
+ isTmpRef,
26
28
  normalizePathArgs,
27
29
  normalizeWorkspacePath,
28
30
  } from '../kb-fs/index.js';
@@ -35,10 +37,11 @@ import type { AccessDecisionSource, AccessTargetKind, IAccessControl } from '../
35
37
  import { accessRoster, resolveAccessView, accessMdPathForFolder, fileCarriesAccessRules, governingFolderOf } from '../access/index.js';
36
38
  import type { SpillStore } from './spill-store.js';
37
39
  import type { DocExtractService } from './file-readers/doc-extract.service.js';
38
- import { displayPath, type FileKind, type FileReaderRegistry, type ReadOrigin, type ReadResult } from './file-readers/file-reader.js';
40
+ import { displayPath, type FileKind, type FileReader, type FileReaderRegistry, type ReadOrigin, type ReadResult } from './file-readers/file-reader.js';
39
41
  import { fileTypeOf, needsContent } from './file-readers/content-mode.js';
40
42
  import { createFileReaderRegistry } from './file-readers/file-reader.registry.js';
41
43
  import { DocumentReader } from './file-readers/document-reader.js';
44
+ import { TextReader } from './file-readers/text-reader.js';
42
45
  import { mcpImageResult } from '@bevel-software/platform-mcp-core';
43
46
  import {
44
47
  folderPlaceholderPath,
@@ -63,7 +66,14 @@ import {
63
66
  type AgentGuideReader,
64
67
  } from '../agent-guide/index.js';
65
68
  import { removeEmptyDirs } from './empty-dirs.js';
66
- import { endsTheSearch, type IPatternMatcher, type IPatternSearch } from './pattern-matcher.contract.js';
69
+ import {
70
+ compareSearchPaths,
71
+ PATTERN_DIALECT,
72
+ type IPatternMatcher,
73
+ type IPatternSearch,
74
+ type TreeScope,
75
+ } from './pattern-matcher.contract.js';
76
+ import { holdsNul } from './nul-scan.js';
67
77
  import { FIRST_RUN_SECTION_ID, firstRunNote, knowledgeFolderIsNew, type FirstRunStarterSource } from './first-run.js';
68
78
  import { planMoveLinks } from './move-links.js';
69
79
  import { rethrowAsWriteDenial } from './write-denial.js';
@@ -99,6 +109,13 @@ interface AccessVerbs {
99
109
  owner: boolean;
100
110
  }
101
111
 
112
+
113
+ /**
114
+ * The caller's verdicts on a file you staged in the workspace's tmp folder: it
115
+ * may be read (and copied out), nothing else — the folder is read-only
116
+ * through the ref, and `request_file_download` does not take one.
117
+ */
118
+ const TMP_ACCESS: AccessVerbs = { read: true, write: false, download: false, owner: false };
102
119
  /** How many files `file_stat` counts under a folder before it stops and says so. */
103
120
  const DESCENDANTS_CAP = 10_000;
104
121
 
@@ -154,18 +171,25 @@ async function filterReadableEntries(
154
171
  fileWsPaths.push(dir ? `${dir}/${e.name}` : e.name);
155
172
  }
156
173
  if (fileWsPaths.length === 0) return entries;
174
+ const readable = await readableFiles(gate, fileWsPaths);
175
+ return entries.filter((e) => e.type === 'directory' || readable.has(dir ? `${dir}/${e.name}` : e.name));
176
+ }
177
+
178
+ /**
179
+ * The FILES among `wsPaths` the caller may read, judged in one batched ACL
180
+ * load by the FULL `canReadBatch` (per-node frontmatter honoured), via the
181
+ * shared `resolveReadableMap`. A path the verdict does not name is unreadable.
182
+ */
183
+ async function readableFiles(gate: ReadGate, wsPaths: string[]): Promise<Set<string>> {
184
+ if (wsPaths.length === 0) return new Set();
157
185
  const verdict = await resolveReadableMap(
158
186
  (wid, email, rels) => gate.accessControl.canReadBatch(wid, email, rels),
159
187
  gate.workspaceId,
160
188
  gate.userEmail,
161
189
  gate.kbDirName,
162
- fileWsPaths,
190
+ wsPaths,
163
191
  );
164
- return entries.filter((e) => {
165
- if (e.type === 'directory') return true;
166
- const wsPath = dir ? `${dir}/${e.name}` : e.name;
167
- return verdict.get(wsPath) === true;
168
- });
192
+ return new Set(wsPaths.filter((p) => verdict.get(p) === true));
169
193
  }
170
194
 
171
195
  /**
@@ -225,6 +249,14 @@ const int = (description: string): JsonSchema => ({ type: 'integer', description
225
249
 
226
250
  const str = (description: string): JsonSchema => ({ type: 'string', description });
227
251
 
252
+ /**
253
+ * A write to the default branch is live at once, so the agent asks first. Said on the four
254
+ * tools behind 87 of the 91 writes there in the agent-guide benchmark (2026-10-26); `start-here`
255
+ * says it for the other write tools.
256
+ */
257
+ const ASK_BEFORE_DEFAULT_BRANCH =
258
+ ' On the default branch a write is live for everyone at once: unless the person already said where, ask them first whether to write there or propose it on a draft.';
259
+
228
260
  /**
229
261
  * The upload route, named on every tool that takes content as a JSON string.
230
262
  *
@@ -284,6 +316,32 @@ const RESERVED_ROOT_NAME_TARGETS: Readonly<Record<string, readonly string[]>> =
284
316
  apply_file_upload: ['destination'],
285
317
  };
286
318
 
319
+ /**
320
+ * The inputs that take a tmp ref (`__tmp__/<path>`, see `workspace-tmp.ts`)
321
+ * beside a repository path — the four reads of the one file it names. Every
322
+ * other tool, and every other input of these four (`copy_file`'s `dest`
323
+ * among them), refuses a ref before anything else runs — in
324
+ * `normalizePathArgs`, through the one admission gate (`kb-fs/tmp-ref.ts`):
325
+ * the folder is read-only through the ref, and `copy_file` to a repository
326
+ * path is how a file is kept.
327
+ */
328
+ const TMP_REF_INPUTS: Readonly<Record<string, readonly string[]>> = {
329
+ read_file: ['path'],
330
+ file_stat: ['path'],
331
+ grep: ['path'],
332
+ copy_file: ['src'],
333
+ };
334
+
335
+ /**
336
+ * A path input that also takes a tmp ref (see `TMP_REF_INPUTS`): said on the
337
+ * input, because the descriptions of the tools that take one have no room
338
+ * left, and what the ref is is in the shared rules.
339
+ */
340
+ const orTmpRef = (schema: JsonSchema): JsonSchema => ({
341
+ ...schema,
342
+ description: `${schema.description ?? ''} Or a \`__tmp__/…\` ref to one file you staged in the workspace's tmp folder.`,
343
+ });
344
+
287
345
  function asText(content: string | Buffer): string {
288
346
  return typeof content === 'string' ? content : content.toString('utf8');
289
347
  }
@@ -639,56 +697,78 @@ function modeOf(a: Record<string, unknown>): WriteMode {
639
697
  }
640
698
 
641
699
  /**
642
- * What searching ONE file amounted to. The walk ignores this (a file with
643
- * nothing searchable is just a file with no matches), but a grep whose path
644
- * NAMES that one file has nothing else to report: without this it could only
645
- * answer an empty match list, which reads as "your pattern is not in there"
646
- * when the truth is "there was never any text to look at".
700
+ * What a file holds for `grep` to search. The walk ignores the difference
701
+ * between the last two (a file with nothing searchable is just a file with no
702
+ * matches), but a grep whose path NAMES that one file has nothing else to
703
+ * report: without it, it could only answer an empty match list, which reads as
704
+ * "your pattern is not in there" when the truth is "there was never any text
705
+ * to look at".
647
706
  */
648
- type FileGrepOutcome =
649
- /** Its text was searched; any matches are in `out`. */
650
- | 'searched'
707
+ type SearchableText =
708
+ /** Text to search. */
709
+ | { kind: 'text'; text: string }
651
710
  /** No searchable text at all: an image, binary content, a corrupt document (remembered or fresh). */
652
- | 'no-text'
711
+ | { kind: 'no-text' }
653
712
  /** A cold document the per-call extraction budget could not afford (counted in `docs`). */
654
- | 'budget-skipped';
713
+ | { kind: 'budget-skipped' };
714
+
715
+ /** One line of a grep answer: that path, the 1-based line number, the line cut to {@link GREP_LINE_CHARS}. */
716
+ interface GrepMatch {
717
+ path: string;
718
+ line: number;
719
+ text: string;
720
+ }
721
+
722
+ /** The longest stretch of a matching line a grep answer carries. */
723
+ const GREP_LINE_CHARS = 300;
724
+
725
+ /** {@link searchableText}'s `bytesOf` for a repository path: the workspace filesystem's own read. */
726
+ function fileBytes(fs: LocalFilesystem): (path: string) => Promise<Buffer> {
727
+ return async (path) => asBytes(await fs.readFile(path));
728
+ }
655
729
 
656
730
  /**
657
- * Search ONE file and append its matches to `out`. Shared by the directory
658
- * walk and by a grep whose `path` names a file, so both produce the same match
659
- * shape (that path, 1-based line numbers, 300-char text) under the same cap.
660
- *
661
- * What is searched is the file's reader's business: text content for the text
662
- * reader (null on NUL bytes / invalid UTF-8 — binary is not searchable), the
663
- * EXTRACTION for a document reader (marker line included, so the
664
- * `[slide N]`/`[sheet: …]`/`[page N]` lines are themselves searchable and line
665
- * numbers match what read_file returns), nothing for images.
731
+ * Files the text pass judges, then searches, at a time: first this many, then
732
+ * twice as many each time, up to {@link GREP_MAX_BATCH}. A verdict reads the
733
+ * file's own access rules, so a batch costs one read of each of its files.
734
+ * Its names also go to ripgrep on the command line, so a batch also ends
735
+ * once they reach {@link GREP_BATCH_CHARS}, well inside the system's limit.
736
+ */
737
+ const GREP_FIRST_BATCH = 256;
738
+ const GREP_MAX_BATCH = 2_048;
739
+ const GREP_BATCH_CHARS = 256 * 1024;
740
+
741
+ /**
742
+ * The text ONE file holds for `grep`, read through its reader: the text
743
+ * content for a text reader (none on NUL bytes / invalid UTF-8 — binary is not
744
+ * searchable), the EXTRACTION for a document reader (marker line included, so
745
+ * the `[slide N]`/`[sheet: …]`/`[page N]` lines are themselves searchable and
746
+ * line numbers match what read_file returns), nothing for images.
666
747
  *
667
748
  * Throws whatever reading the file throws — the caller decides whether that is
668
- * a file to skip (the walk) or an error to surface (a single-file grep).
749
+ * a file to skip (a tree search) or an error to surface (a single-file grep).
750
+ * `bytesOf` is where the file's bytes come from: the workspace filesystem for
751
+ * a repository path, the tmp resolver for a tmp ref.
669
752
  */
670
- async function grepOneFile(
671
- fs: LocalFilesystem,
753
+ async function searchableText(
754
+ bytesOf: (path: string) => Promise<Buffer>,
672
755
  path: string,
673
- matcher: IPatternSearch,
674
- out: { path: string; line: number; text: string }[],
675
- max: number,
676
756
  docs: DocGrepState,
677
- ): Promise<FileGrepOutcome> {
757
+ ): Promise<SearchableText> {
678
758
  const reader = docs.readers.readerFor(path);
679
- const bytes = asBytes(await fs.readFile(path));
759
+ const bytes = await bytesOf(path);
680
760
  let content: string | null;
681
761
  if (reader instanceof DocumentReader) {
682
762
  // What the cache remembers is free — an extraction is searched, a failure
683
763
  // (a damaged file, or one that overran its budget within the hour) is
684
764
  // skipped without extracting it again. A cold document draws on the
685
- // per-walk budget (see UNCACHED_DOCS_PER_GREP) — beyond it the search
765
+ // per-call budget (see UNCACHED_DOCS_PER_GREP) — beyond it the search
686
766
  // skips the document and counts it.
687
767
  let res = await reader.cachedRead(bytes, path);
688
768
  if (res === undefined) {
689
769
  if (docs.uncachedBudget <= 0) {
690
770
  docs.skippedUncached++;
691
- return 'budget-skipped';
771
+ return { kind: 'budget-skipped' };
692
772
  }
693
773
  docs.uncachedBudget--;
694
774
  res = await reader.read(bytes, path, docs.origin);
@@ -698,14 +778,44 @@ async function grepOneFile(
698
778
  } else {
699
779
  content = reader.greppableText ? await reader.greppableText(bytes, path) : null;
700
780
  }
701
- if (content === null) return 'no-text';
702
- const lines = content.split('\n');
703
- // matchingLines posts these to the worker in bounded batches, so no one
704
- // copy of a large file runs on the request thread.
705
- for (const i of await matcher.matchingLines(lines, max - out.length)) {
706
- out.push({ path, line: i + 1, text: lines[i]!.slice(0, 300) });
781
+ return content === null ? { kind: 'no-text' } : { kind: 'text', text: content };
782
+ }
783
+
784
+ /**
785
+ * grep over the one text file `path`, by ripgrep from the folder holding it,
786
+ * its lines pushed onto `out`, when it is a regular file; `null`, with
787
+ * nothing searched, for anything else (a link, one that loops, a file gone
788
+ * since), which is read as read_file reads it instead. A file that answers no
789
+ * line has no text when ripgrep finds it binary: it holds a NUL, which
790
+ * {@link holdsNul} looks for in the very file that was judged.
791
+ */
792
+ async function grepTextFile(
793
+ fs: LocalFilesystem,
794
+ path: string,
795
+ search: IPatternSearch,
796
+ max: number,
797
+ out: GrepMatch[],
798
+ ): Promise<{ kind: 'searched' } | { kind: 'no-text' } | null> {
799
+ const slash = path.lastIndexOf('/');
800
+ const scope = await treeScope(fs, slash < 0 ? '' : path.slice(0, slash));
801
+ const name = path.slice(slash + 1);
802
+ const absolute = join(scope.cwd, scope.root, name);
803
+ const judged = await nodeFs.lstat(absolute).catch(() => null);
804
+ if (judged === null || !judged.isFile()) return null;
805
+ const from = out.length;
806
+ for await (const file of search.files(scope, { only: [name], maxLinesPerFile: max })) {
807
+ for (const l of file.lines) out.push({ path, line: l.line, text: l.text.slice(0, GREP_LINE_CHARS) });
707
808
  }
708
- return 'searched';
809
+ if (out.length > from) return { kind: 'searched' };
810
+ const binary = await orNotFound(path, () => holdsNul(absolute, judged, search), 'Nothing to search');
811
+ return binary ? { kind: 'no-text' } : { kind: 'searched' };
812
+ }
813
+
814
+ /** Append the lines `hits` names (0-based indices into `text`'s lines) to `out`, as matches in `path`. */
815
+ function pushMatches(out: GrepMatch[], path: string, text: string, hits: readonly number[]): void {
816
+ if (hits.length === 0) return;
817
+ const lines = text.split('\n');
818
+ for (const i of hits) out.push({ path, line: i + 1, text: lines[i]!.slice(0, GREP_LINE_CHARS) });
709
819
  }
710
820
 
711
821
  /**
@@ -742,56 +852,151 @@ async function searchRootKind(
742
852
  }
743
853
  }
744
854
 
745
- /** JS grep over the workspace tree (read methods only) — bounded by match + depth caps. */
746
- async function grepWalk(
855
+ /**
856
+ * Where ripgrep searches the directory `root`: from the workspace's canonical
857
+ * directory, with the root's canonical path relative to it. The stat that
858
+ * called `root` a directory already refused one outside the workspace; this
859
+ * refuses it again where the search is started, so ripgrep, which follows the
860
+ * root it is given, is never started on anything outside.
861
+ */
862
+ async function treeScope(fs: LocalFilesystem, root: string): Promise<TreeScope> {
863
+ const cwd = await nodeFs.realpath(fs.basePath);
864
+ const absolute = fs.resolveAbsolutePath(root);
865
+ const rel = absolute === undefined ? '..' : relative(cwd, await nodeFs.realpath(absolute));
866
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
867
+ throw new ToolError(`"${displayPath(root)}" is outside the workspace.`, 403);
868
+ }
869
+ return { cwd, root: rel === '' ? '.' : rel.split(sep).join('/') };
870
+ }
871
+
872
+ /**
873
+ * grep over the directory `root`: the tree is listed, then searched in two
874
+ * passes, and no file is opened before it is admitted.
875
+ *
876
+ * The listing opens no file. Admission is the ONE step a file passes before
877
+ * either pass opens it: it is judged readable by the gate, and then — being a
878
+ * file the search will read, as `read_file` reads one — it is told to the
879
+ * read hook, which may refuse it, and with it the search. A file the caller
880
+ * may not read is never opened and never told of (there is no read to tell):
881
+ * what it holds can change neither the answer nor how long the search takes,
882
+ * and so not whether it ends within its budget. Judging and telling are both
883
+ * spent from that budget. A file that was not in the listing (it appeared
884
+ * since) is never admitted, so never searched.
885
+ *
886
+ * The text pass is ripgrep's own: every file whose reader greps its bytes as
887
+ * text (a TextReader) is searched by it, in path order, and read nowhere else.
888
+ * It admits a batch at a time, before ripgrep opens any of it, so the hook
889
+ * hears of each file of a batch whether or not a line of it is answered —
890
+ * and, past the line that fills the cap, of the rest of that batch, which the
891
+ * pass then never opens. Each file is answered as ripgrep finishes it, and
892
+ * the pass stops at the `max`-th line.
893
+ *
894
+ * The document pass reads the files every other reader owns through that
895
+ * reader, as `read_file` does — extractions under the per-call budget — and
896
+ * has their text matched by the same engine. A document that sorts after the
897
+ * text pass's last line cannot be among the first `max`, so it is not read.
898
+ *
899
+ * Answers what both found, unsorted: the caller merges, sorts and caps.
900
+ */
901
+ async function grepTree(
747
902
  fs: LocalFilesystem,
748
- dir: string,
749
- matcher: IPatternSearch,
750
- out: { path: string; line: number; text: string }[],
903
+ root: string,
904
+ search: IPatternSearch,
751
905
  max: number,
752
- depth: number,
753
906
  gate: ReadGate,
754
907
  notifyRead: (path: string) => Promise<void>,
755
908
  docs: DocGrepState,
756
- /** A file the walk leaves to its caller: never opened, never counted against `max`. */
909
+ /** A file the search leaves to its caller: never answered, never counted against `max`. */
757
910
  skip: (path: string) => boolean = () => false,
758
- ): Promise<void> {
759
- if (out.length >= max || depth > 12) return;
760
- let entries;
761
- try {
762
- entries = (await fs.readdir(dir || '.')) as DirEntry[];
763
- } catch {
764
- return;
911
+ ): Promise<GrepMatch[]> {
912
+ const scope = await treeScope(fs, root);
913
+ // A folder named with a trailing slash answers the paths it is spelled
914
+ // with everywhere else (`a/b.md`, never `a//b.md`), so the guide's own
915
+ // path and every verdict meet the one spelling.
916
+ const base = root.replace(/\/+$/, '');
917
+ const at = (p: string): string => (base === '' ? p : `${base}/${p}`);
918
+ // ripgrep already stays out of `.git`; the folder's every other spelling,
919
+ // and the folder placeholder, are left out as everywhere else.
920
+ const shown = (p: string): boolean => !hasGitInternalsSegment(p) && !isFolderPlaceholder(p) && !skip(p);
921
+ // Which reader owns an extension is the registry's answer, asked of a name
922
+ // carrying it; a TextReader greps the bytes themselves, which is ripgrep's
923
+ // pass. Extensions any other reader owns are left out of it.
924
+ const readerOf = (ext: string): FileReader => docs.readers.readerFor(`file${ext}`);
925
+ const notRaw = docs.readers.ownedExtensions().filter((ext) => !(readerOf(ext) instanceof TextReader));
926
+ const ownedBy = (p: string, exts: readonly string[]): boolean => {
927
+ const lower = p.toLowerCase();
928
+ return exts.some((ext) => lower.endsWith(ext));
929
+ };
930
+ const readerOwned = notRaw.filter((ext) => {
931
+ const reader = readerOf(ext);
932
+ return reader instanceof DocumentReader || reader.greppableText !== undefined;
933
+ });
934
+ const out: GrepMatch[] = [];
935
+ // Admission (see above): the gate's verdict on `paths`, then the hook told
936
+ // of each readable one in path order — a refusal ends the search here, as
937
+ // it ends a `read_file`, before anything is opened. Spent from the budget.
938
+ const admit = (paths: string[]): Promise<Set<string>> =>
939
+ search.charged(async () => {
940
+ const readable = await readableFiles(gate, paths);
941
+ for (const p of paths) {
942
+ if (readable.has(p)) await notifyRead(p);
943
+ }
944
+ return readable;
945
+ });
946
+
947
+ // Every file the walk covers, in path order. Only what is shown is ever
948
+ // admitted, so what is admitted is what may be answered. A name holding a
949
+ // control character is no file name to the platform — no tool reads or
950
+ // writes one — so it is not searched either.
951
+ const all = (await search.list(scope)).filter((p) => shown(at(p)) && !hasControlCharacter(p));
952
+ const raw = all.filter((p) => !ownedBy(p, notRaw));
953
+ // The text pass: a batch of files is admitted, then its admitted files, and
954
+ // only they, are searched. A batch is admitted before ripgrep opens any of
955
+ // it, and the pass stops at the `max`-th line, so the verdicts asked for are
956
+ // those of the files the answer reached — few for a common word — and the
957
+ // batches double so a rare one still walks the tree in a handful of runs.
958
+ let size = GREP_FIRST_BATCH;
959
+ for (let from = 0; from < raw.length && out.length < max; size = Math.min(size * 2, GREP_MAX_BATCH)) {
960
+ const batch: string[] = [];
961
+ for (let chars = 0; from < raw.length && batch.length < size && (batch.length === 0 || chars < GREP_BATCH_CHARS); from++) {
962
+ batch.push(raw[from]!);
963
+ chars += raw[from]!.length;
964
+ }
965
+ const readable = await admit(batch.map(at));
966
+ const only = batch.filter((p) => readable.has(at(p)));
967
+ for await (const file of search.files(scope, { only, maxLinesPerFile: max - out.length })) {
968
+ const p = at(file.path);
969
+ // Only what was admitted answers, whatever ripgrep printed.
970
+ if (!readable.has(p)) continue;
971
+ for (const l of file.lines.slice(0, max - out.length)) {
972
+ out.push({ path: p, line: l.line, text: l.text.slice(0, GREP_LINE_CHARS) });
973
+ }
974
+ // Enough: leaving the loop kills ripgrep at the file that filled the cap.
975
+ if (out.length >= max) break;
976
+ }
765
977
  }
766
- // Filter unreadable KB nodes out of the walk so grep never opens — or leaks
767
- // a line from — a file the caller may not read.
768
- entries = await filterReadableEntries(gate, dir, entries);
769
- for (const e of entries) {
770
- if (out.length >= max) return;
771
- if (hasGitInternalsSegment(e.name) || e.name === 'node_modules') continue;
772
- if (e.type !== 'directory' && isFolderPlaceholder(e.name)) continue;
773
- const p = dir ? `${dir}/${e.name}` : e.name;
774
- if (e.type === 'directory') {
775
- await grepWalk(fs, p, matcher, out, max, depth + 1, gate, notifyRead, docs, skip);
776
- } else if (skip(p)) {
978
+
979
+ const last = out.length >= max ? out[out.length - 1]!.path : undefined;
980
+ const candidates = all
981
+ .filter((p) => ownedBy(p, readerOwned))
982
+ .map(at)
983
+ .filter((p) => last === undefined || compareSearchPaths(p, last) <= 0);
984
+ const readable = await admit(candidates);
985
+ const texts: { path: string; text: string }[] = [];
986
+ for (const p of candidates) {
987
+ if (!readable.has(p)) continue;
988
+ let found: SearchableText;
989
+ try {
990
+ found = await searchableText(fileBytes(fs), p, docs);
991
+ } catch {
992
+ // One document that cannot be read must not fail a search over the tree.
777
993
  continue;
778
- } else {
779
- // Opening a file is a read of it, even when the walk started at a root
780
- // the read hook was already told about — so every file the walk opens
781
- // reaches the hook by name (closes the read-leak).
782
- await notifyRead(p);
783
- // A file the walk cannot read is silently skipped: one unreadable entry
784
- // must not fail a search over the whole tree.
785
- // A pattern too expensive to finish, or no worker free to run it, is not
786
- // an unreadable file: either ends the whole search.
787
- try {
788
- await grepOneFile(fs, p, matcher, out, max, docs);
789
- } catch (err) {
790
- if (endsTheSearch(err)) throw err;
791
- continue;
792
- }
793
994
  }
995
+ if (found.kind === 'text') texts.push({ path: p, text: found.text });
794
996
  }
997
+ const hits = await search.matchingLines(texts.map((t) => t.text));
998
+ texts.forEach((t, i) => pushMatches(out, t.path, t.text, hits[i]!));
999
+ return out;
795
1000
  }
796
1001
 
797
1002
  /** One `allowed-tools` entry a saved SKILL.md names that no visible tool matches. */
@@ -916,8 +1121,9 @@ export function registerWorkspaceTools(
916
1121
  */
917
1122
  uploads?: AgentUploadStore,
918
1123
  /**
919
- * The platform's agent guide (see `modules/agent-guide`), which `read_file`
920
- * and `file_stat` answer at the guide's name in the repository root — after
1124
+ * The start of the platform's agent guide and the list of its sections
1125
+ * (`guideStartText` in `modules/agent-guide`), which `read_file` and
1126
+ * `file_stat` answer at the guide's name in the repository root — after
921
1127
  * the knowledge base's own file of that name, when it has one. Optional for
922
1128
  * the harnesses that are about the file primitives; without it the two
923
1129
  * tools read the disk and nothing else.
@@ -1111,7 +1317,9 @@ export function registerWorkspaceTools(
1111
1317
  const assertToolPathsNotGitInternals = async (args: Record<string, unknown>, ctx: ToolContext): Promise<void> => {
1112
1318
  const paths = toolPathArgs(args);
1113
1319
  for (const p of paths) assertNoGitInternalsSegment(p);
1114
- const onDisk = paths.filter((p) => !spillStore.isSpillRef(p));
1320
+ // A spill ref belongs to no workspace, and a tmp ref to the folder beside
1321
+ // the checkout: neither is judged against the checkout's git folder.
1322
+ const onDisk = paths.filter((p) => !spillStore.isSpillRef(p) && !isTmpRef(p));
1115
1323
  if (onDisk.length === 0) return;
1116
1324
  const root = await gitCheckRootFor(args, ctx);
1117
1325
  if (root === null) return;
@@ -1168,15 +1376,20 @@ export function registerWorkspaceTools(
1168
1376
  * caller WILL have, never whether they may do it. The write verdicts that
1169
1377
  * gate the move are `writeBlocked` and the lock gate, both of which read
1170
1378
  * the tree as it is.
1379
+ *
1380
+ * `sourceContent` is the text of a source that is no repository path (a
1381
+ * tmp ref): it carries rules in exactly as a repository file would, so it
1382
+ * is previewed through the same relocation reading, never as "nothing
1383
+ * travels".
1171
1384
  */
1172
1385
  const accessAfter = async (
1173
1386
  branch: string,
1174
1387
  ctx: ToolContext,
1175
1388
  src: string,
1176
1389
  dest: string,
1177
- opts?: { sourceRemains?: boolean },
1390
+ opts?: { sourceRemains?: boolean; sourceContent?: string },
1178
1391
  ): Promise<AccessVerbs> => {
1179
- const from = toKbRelative(src, kbDirName);
1392
+ const from = opts?.sourceContent !== undefined ? src : toKbRelative(src, kbDirName);
1180
1393
  const to = toKbRelative(dest, kbDirName);
1181
1394
  // Outside the repository there are no rules to carry, and none to land
1182
1395
  // among — the same answer `accessAt` gives for such a path.
@@ -1190,6 +1403,18 @@ export function registerWorkspaceTools(
1190
1403
  );
1191
1404
  };
1192
1405
 
1406
+ /**
1407
+ * The text a tmp source carries into the repository, for the preview only:
1408
+ * read as a repository source is read there — a file that cannot be read
1409
+ * carries no rules — so the preview never answers before the write
1410
+ * verdict. Whether the file is there at all is `statTmpFile`'s to say.
1411
+ */
1412
+ const tmpSourceText = (branch: string, ctx: ToolContext, ref: string): Promise<string> =>
1413
+ ctx.workspaceService
1414
+ .readTmpFile(workspaceIdForBranch(branch), ctx.user.id, ref)
1415
+ .then((bytes) => bytes.toString('utf-8'))
1416
+ .catch(() => '');
1417
+
1193
1418
  /**
1194
1419
  * What `copy_file`'s dry run answers: the same impact shape `move_file`
1195
1420
  * previews, over a copy's own rules.
@@ -1217,10 +1442,16 @@ export function registerWorkspaceTools(
1217
1442
  * them nothing they could not ask `file_stat` for.
1218
1443
  */
1219
1444
  const copyImpact = async (branch: string, ctx: ToolContext, src: string, dest: string) => {
1220
- const [before, after, blocked] = await Promise.all([
1221
- accessAt(branch, ctx, src),
1222
- accessAfter(branch, ctx, src, dest, { sourceRemains: true }),
1223
- writeBlocked(branch, ctx, [dest]),
1445
+ const fromTmp = isTmpRef(src);
1446
+ // The write verdict FIRST, and alone: a staged file's bytes are read only
1447
+ // for a caller who may write the destination. A denied preview carries
1448
+ // no rules from the source ('' — what an unreadable source carries), so
1449
+ // its `access.after` is the destination's own rules for the caller.
1450
+ const blocked = await writeBlocked(branch, ctx, [dest]);
1451
+ const sourceContent = fromTmp ? (blocked.length > 0 ? '' : await tmpSourceText(branch, ctx, src)) : undefined;
1452
+ const [before, after] = await Promise.all([
1453
+ fromTmp ? TMP_ACCESS : accessAt(branch, ctx, src),
1454
+ accessAfter(branch, ctx, src, dest, { sourceRemains: true, sourceContent }),
1224
1455
  ]);
1225
1456
  const access = { before, after };
1226
1457
  const accessChanges = verbsDiffer(before, after);
@@ -1237,7 +1468,11 @@ export function registerWorkspaceTools(
1237
1468
  };
1238
1469
  }
1239
1470
  const fs = await ctx.getFilesystem(branch);
1240
- const kind = await kindOf(fs, src);
1471
+ // A tmp ref names one file by rule: resolving it is the whole look, and
1472
+ // a folder or nothing there is refused by the resolver itself.
1473
+ const kind = fromTmp
1474
+ ? (await orNotFound(src, () => ctx.workspaceService.statTmpFile(workspaceIdForBranch(branch), ctx.user.id, src), 'Nothing to copy'), 'file' as const)
1475
+ : await kindOf(fs, src);
1241
1476
  if (kind === null) throw notFound(src, 'Nothing to copy');
1242
1477
  const srcFiles = kind === 'folder' ? (await filesUnder(fs, src)).files : [src];
1243
1478
  const occupiedBy = await existingAt(await workspaceRoot(branch, ctx), dest);
@@ -1262,6 +1497,40 @@ export function registerWorkspaceTools(
1262
1497
  };
1263
1498
  };
1264
1499
 
1500
+ /**
1501
+ * `copy_file` out of the workspace's tmp folder: the bytes the ref names
1502
+ * (through the one tmp resolver), landed at `dest` by the ordinary write —
1503
+ * the lock, the pre-disk validation, the creator's read grant for a new
1504
+ * folder and the commit every write to the repository takes — and
1505
+ * exclusively, so a name taken since the handler's look is refused with
1506
+ * the same sentence rather than replaced. From then on the copy is a
1507
+ * repository file like any other; the staged file is left where it was.
1508
+ *
1509
+ * A missing source is the ordinary 404 naming the ref, asked only after
1510
+ * the write verdict (as for any copy); an absence met while landing is the
1511
+ * destination's.
1512
+ */
1513
+ const copyFromTmp = async (fs: LocalFilesystem, branch: string, ctx: ToolContext, src: string, dest: string): Promise<void> => {
1514
+ const bytes = await orNotFound(
1515
+ src,
1516
+ () => ctx.workspaceService.readTmpFile(workspaceIdForBranch(branch), ctx.user.id, src),
1517
+ 'Nothing to copy',
1518
+ );
1519
+ try {
1520
+ // Exclusive, under the destination's lock: a name taken since the look
1521
+ // is refused there as `DestinationTakenError` — releasing the lock
1522
+ // untouched, so the winner's queued file stays — and answered as a
1523
+ // repository copy's lost race is (`asEntryExists`).
1524
+ await asEntryExists(() => fs.writeFile(dest, bytes, { overwrite: false }));
1525
+ } catch (err) {
1526
+ if (err instanceof ToolError) throw err;
1527
+ const occupiedBy = await existingAt(await workspaceRoot(branch, ctx), dest);
1528
+ if (occupiedBy !== null) throw new ToolError(entryExistsMessage(occupiedBy, dest), 409);
1529
+ if (isAbsence(err)) throw notFound(dest, 'Nowhere to copy to');
1530
+ throw err;
1531
+ }
1532
+ };
1533
+
1265
1534
  /**
1266
1535
  * The paths among `paths` the caller may NOT write, judged exactly as the
1267
1536
  * lock gate judges them (`WorkflowService.acquireLock`): on a protected
@@ -1677,15 +1946,21 @@ export function registerWorkspaceTools(
1677
1946
  // spill ref; for any other, `__tool_chain_spill__/x` is an ordinary
1678
1947
  // path, and exempting it there would be a workspace-relative path that
1679
1948
  // never reached the repository — the whole bug, spelled with a prefix.
1949
+ //
1950
+ // The other is a tmp ref (`__tmp__/…`), left as written in exactly the
1951
+ // inputs `TMP_REF_INPUTS` names and refused in every other one first,
1952
+ // so no tool sees one it does not resolve itself.
1680
1953
  toolHandler(
1681
1954
  async (args, ctx) => {
1682
1955
  // BEFORE the normaliser: see `assertToolPathsNotGitInternals`.
1683
1956
  if (spec.fileTool !== false) await assertToolPathsNotGitInternals(args, ctx);
1684
- const normalized = normalizePathArgs(
1685
- args,
1686
- kbDirName,
1687
- spec.name === 'read_file' ? (v) => spillStore.isSpillRef(v) : undefined,
1688
- );
1957
+ // The normaliser admits a tmp ref in exactly the inputs
1958
+ // `TMP_REF_INPUTS` names and refuses it in every other one first.
1959
+ const normalized = normalizePathArgs(args, kbDirName, {
1960
+ tool: spec.name,
1961
+ tmpRefInputs: TMP_REF_INPUTS[spec.name],
1962
+ skip: spec.name === 'read_file' ? (v) => spillStore.isSpillRef(v) : undefined,
1963
+ });
1689
1964
  // The checkout's own name is reserved at the repository root on this
1690
1965
  // surface too. The routes meet that rule inside `WorkspaceService`;
1691
1966
  // these tools write through the locking filesystem, which never
@@ -1744,13 +2019,13 @@ export function registerWorkspaceTools(
1744
2019
  const startSessionDef = toolDef({
1745
2020
  name: 'start_session',
1746
2021
  description:
1747
- 'Mint this conversation\'s id: the `sessionId` KnowledgeBase tools take. Call this ONCE, at the start of your work — minting a new id mid-run starts a second conversation as far as the server is concerned. The id is also a chat session in the app, so you can hand the SAME id to the `ask` tool: your reads and your questions are then one conversation. Pass the returned id as `sessionId` on every later KnowledgeBase tool call, inside `call_tool_chain` too. RETRYING IS SAFE: a call that fails created nothing, so retry it. If a retry lands after a success you hold two independent ids, which is harmless: keep passing the one you already used and ignore the other. Returns `{ sessionId }`, plus `firstRun` while the knowledge base is still empty: a note to act on.',
2022
+ 'Mint this conversation\'s id: the `sessionId` KnowledgeBase tools take. Call this ONCE, at the start of your work — minting a new id mid-run starts a second conversation as far as the server is concerned. Pass the returned id as `sessionId` to every tool whose arguments list it, inside `call_tool_chain` too; the others refuse it. Where the deployment offers an `ask` tool, hand it the SAME id: your reads and your questions are then one conversation. RETRYING IS SAFE: a call that fails created nothing, so retry it. If a retry lands after a success you hold two independent ids, which is harmless: keep passing the one you already used and ignore the other. Returns `{ sessionId }`, plus `firstRun` while the knowledge base is still empty: a note to act on.',
1748
2023
  path: '/api/agent/tools/start_session',
1749
2024
  inputs: { type: 'object', properties: {}, additionalProperties: false },
1750
2025
  outputs: {
1751
2026
  type: 'object',
1752
2027
  properties: {
1753
- sessionId: str('The minted session id — pass it as `sessionId` on subsequent KnowledgeBase tool calls and to `ask`.'),
2028
+ sessionId: str('The minted session id — pass it as `sessionId` to the tools that take one, and to `ask` where there is one.'),
1754
2029
  firstRun: str(
1755
2030
  `Present only while the knowledge base holds nothing but its starter guide (and a starter pack's untouched pages): what to offer the person (the guide's \`${FIRST_RUN_SECTION_ID}\` section says how).`,
1756
2031
  ),
@@ -1853,6 +2128,63 @@ export function registerWorkspaceTools(
1853
2128
  */
1854
2129
  const readForTool: ReadForTool = async (branch, p, ctx) => (await readServed(branch, p, ctx)).result;
1855
2130
 
2131
+ /**
2132
+ * What a tmp ref (see `workspace-tmp.ts`) reads as: the bytes of the one
2133
+ * file it names in the CALLER's own folder of the branch workspace's `tmp/`
2134
+ * — through `readTmpFile`, the resolver a distribution's tool reads it with
2135
+ * — put through the SAME reader a repository file of that name gets. No
2136
+ * access gate and no read hook: the folder is outside the repository, so
2137
+ * no rule governs it and no knowledge-base path is read; what keeps one
2138
+ * person's staged file from another is that the ref resolves inside the
2139
+ * caller's folder alone. A missing file — somebody else's included — is the
2140
+ * ordinary 404, naming the ref.
2141
+ */
2142
+ const readTmp = async (branch: string, ref: string, ctx: ToolContext): Promise<ReadResult> => {
2143
+ const workspaceId = workspaceIdForBranch(branch);
2144
+ const bytes = await orNotFound(ref, () => ctx.workspaceService.readTmpFile(workspaceId, ctx.user.id, ref));
2145
+ return readers.readerFor(ref).read(bytes, ref, { workspaceId });
2146
+ };
2147
+
2148
+ /**
2149
+ * `file_stat` on a tmp ref: the shape a repository file's stat has, its
2150
+ * content fields decided by the same reader as {@link readTmp} reads with —
2151
+ * and nothing to DO with it but read and copy out. Not written, moved,
2152
+ * deleted or downloaded through these tools (`managed` is false: the
2153
+ * platform's own files are a repository notion), and so not text-editable
2154
+ * either, whatever its content.
2155
+ */
2156
+ const statTmp = async (branch: string, ref: string, ctx: ToolContext): Promise<Record<string, unknown>> => {
2157
+ const workspaceId = workspaceIdForBranch(branch);
2158
+ const stat = await orNotFound(ref, () => ctx.workspaceService.statTmpFile(workspaceId, ctx.user.id, ref));
2159
+ const reader = readers.readerFor(ref);
2160
+ // One read of the file at most: the type decision and the remembered
2161
+ // extraction below both want the bytes, and a staged document can be large.
2162
+ let bytes: Buffer | undefined;
2163
+ const bytesOf = async () =>
2164
+ (bytes ??= await orNotFound(ref, () => ctx.workspaceService.readTmpFile(workspaceId, ctx.user.id, ref)));
2165
+ const type = fileTypeOf(reader, ref, needsContent(reader) ? await bytesOf() : undefined);
2166
+ const out = {
2167
+ ...stat,
2168
+ path: ref,
2169
+ type: 'file',
2170
+ managed: false,
2171
+ movable: false,
2172
+ deletable: false,
2173
+ access: TMP_ACCESS,
2174
+ canWrite: false,
2175
+ ...type,
2176
+ textEditable: false,
2177
+ };
2178
+ // A document whose extraction failed answers that failure's notice, as a
2179
+ // repository document does: from what the cache remembers, never by
2180
+ // extracting.
2181
+ if (reader instanceof DocumentReader) {
2182
+ const remembered = await reader.cachedRead(await bytesOf(), ref);
2183
+ if (remembered?.kind === 'refusal') return { ...out, unreadable: remembered.message };
2184
+ }
2185
+ return out;
2186
+ };
2187
+
1856
2188
  /**
1857
2189
  * {@link readForTool}, also saying, at the guide's name, whether a file of
1858
2190
  * the knowledge base's own answered first on THIS branch (`guide`: `own`)
@@ -1911,7 +2243,7 @@ export function registerWorkspaceTools(
1911
2243
  'Read a workspace file as text. Returns `{ path, content, canWrite }`. Per the shared content rule: text as ' +
1912
2244
  'text, documents and email as extracted text, an image as the picture, anything else as a one-line ' +
1913
2245
  'description. Also reads a `__tool_chain_spill__/…` ref back from a truncated `call_tool_chain`: it ' +
1914
- 'belongs to no workspace, so `branch` is ignored and no `canWrite` comes back. `offset`/`limit` slice the ' +
2246
+ 'belongs to no workspace: any existing `branch` will do, and no `canWrite` comes back. `offset`/`limit` slice the ' +
1915
2247
  'content (characters; bytes for a spill ref; ignored for an image): page through large files and ' +
1916
2248
  'spills, never read multi-MB whole. `canWrite`: may you change this file directly on the default branch. ' +
1917
2249
  CAN_WRITE_CLAUSE,
@@ -1919,9 +2251,9 @@ export function registerWorkspaceTools(
1919
2251
  type: 'object',
1920
2252
  properties: {
1921
2253
  branch: BRANCH_INPUT,
1922
- path: str(`Path to read, under \`${kbDirName}/\` (e.g. \`${kbDirName}/KnowledgeBase/Foo.md\`), with or without a leading slash — a path without that prefix is placed under \`${kbDirName}/\` — or a \`__tool_chain_spill__/…\` ref from a truncated \`call_tool_chain\`.`),
1923
- offset: int('Start character index (default 0).'),
1924
- limit: int('Max characters to return from `offset`.'),
2254
+ path: str(`Path to read, under \`${kbDirName}/\` (e.g. \`${kbDirName}/KnowledgeBase/Foo.md\`), with or without a leading slash — a path without that prefix is placed under \`${kbDirName}/\` — or a \`__tool_chain_spill__/…\` ref from a truncated \`call_tool_chain\`, or a \`__tmp__/…\` ref to a file you staged in the workspace's tmp folder.`),
2255
+ offset: int('Start character index (bytes for a spill ref; default 0).'),
2256
+ limit: int('Max characters to return from `offset` (bytes for a spill ref).'),
1925
2257
  sessionId: SESSION_ID_INPUT,
1926
2258
  },
1927
2259
  required: ['branch', 'path'],
@@ -1956,7 +2288,11 @@ export function registerWorkspaceTools(
1956
2288
  : content;
1957
2289
  };
1958
2290
  const branch = a.branch as string;
1959
- const { result, guide } = await readServed(branch, p, ctx);
2291
+ // A tmp ref is no repository path, so the `canWrite` below is false for
2292
+ // it by the same rule as for any path outside the repository.
2293
+ const { result, guide } = isTmpRef(p)
2294
+ ? { result: await readTmp(branch, p, ctx), guide: null }
2295
+ : await readServed(branch, p, ctx);
1960
2296
  // Judged after the read, so a path the caller may not read is refused
1961
2297
  // before anything is said about writing it. At the guide's name this
1962
2298
  // answers for the knowledge base's own file there, as `file_stat` does:
@@ -2170,7 +2506,7 @@ export function registerWorkspaceTools(
2170
2506
  type: 'object',
2171
2507
  properties: {
2172
2508
  branch: BRANCH_INPUT,
2173
- path: wsPath(kbDirName, 'Path'),
2509
+ path: orTmpRef(wsPath(kbDirName, 'Path')),
2174
2510
  explainAccess: {
2175
2511
  type: 'boolean',
2176
2512
  description:
@@ -2247,6 +2583,7 @@ export function registerWorkspaceTools(
2247
2583
  handler: async (a, ctx: ToolContext) => {
2248
2584
  const p = a.path as string;
2249
2585
  const branch = a.branch as string;
2586
+ if (isTmpRef(p)) return statTmp(branch, p, ctx);
2250
2587
  // The guide's name with no file of the knowledge base's own under it —
2251
2588
  // or one the caller may not read, or a copy of the guide an earlier
2252
2589
  // release wrote, none of which `read_file` serves: what a read answers
@@ -2408,13 +2745,14 @@ export function registerWorkspaceTools(
2408
2745
  name: 'grep',
2409
2746
  gated: true,
2410
2747
  description:
2411
- 'Regex search across the workspace. Returns `{ matches: [{ path, line, text }] }` (capped). Use to find where something is defined/referenced. `path` may name a DIRECTORY (searches the subtree) or a single FILE (searches just that file); a path with nothing at it is an error, never an empty result. Searches INSIDE Office and OpenDocument files (.docx/.pptx/.xlsx, .odt/.odp/.ods), PDFs and email files (.eml/.msg) via their extracted text — matches there carry the extraction\'s line numbers, and the `[slide N]`/`[sheet: Name]`/`[page N]`/`[from]`/`[subject]` marker lines locate them; a bounded number of not-yet-extracted documents is extracted per call, and the result notes how many were skipped (re-run to cover them).',
2748
+ `Regex search across the workspace. ${PATTERN_DIALECT} Covers hidden and git-ignored files. ` +
2749
+ 'Returns `{ matches: [{ path, line, text }] }`, capped, in path then line order. `path` may name a DIRECTORY (its subtree) or a single FILE; a path with nothing at it is an error. Searches INSIDE Office/OpenDocument, PDF and email files (.eml/.msg) via their extracted text — matches carry the extraction\'s line numbers, and the `[slide N]`/`[sheet: Name]`/`[page N]`/`[from]`/`[subject]` marker lines locate them; a bounded number of not-yet-extracted documents is extracted per call; the result notes the rest (re-run to cover them).',
2412
2750
  inputs: {
2413
2751
  type: 'object',
2414
2752
  properties: {
2415
2753
  branch: BRANCH_INPUT,
2416
- pattern: str('JavaScript regular expression.'),
2417
- path: str(`Subtree to search, or a single file to search on its own, with or without a leading slash — a path without the \`${kbDirName}/\` prefix is placed under \`${kbDirName}/\` (default: the whole repository, \`${kbDirName}/\`).`),
2754
+ pattern: str('Regular expression, RE2-style (ripgrep): no look-around, no backreferences.'),
2755
+ path: str(`Subtree to search, or a single file to search on its own, with or without a leading slash — a path without the \`${kbDirName}/\` prefix is placed under \`${kbDirName}/\` (default: the whole repository, \`${kbDirName}/\`) — or a \`__tmp__/…\` ref to one staged file.`),
2418
2756
  ignore_case: { type: 'boolean', description: 'Case-insensitive match.' },
2419
2757
  max_results: { type: 'integer', minimum: 1, maximum: 1000, description: 'Cap on matches (default 200).' },
2420
2758
  sessionId: SESSION_ID_INPUT,
@@ -2441,9 +2779,14 @@ export function registerWorkspaceTools(
2441
2779
  },
2442
2780
  write: false,
2443
2781
  handler: async (a, ctx: ToolContext) => {
2444
- // The caller's pattern never runs on the request thread: a backtracking
2445
- // one would hold the event loop for everyone (see IPatternMatcher).
2446
- const matcher = patterns.search(a.pattern as string, a.ignore_case ? 'i' : '');
2782
+ // The caller's pattern runs in ripgrep, never on the request thread
2783
+ // (see IPatternMatcher); one the engine refuses is answered here,
2784
+ // before anything is searched. A caller that goes away takes the
2785
+ // search's runs with it.
2786
+ const search = await patterns.search(a.pattern as string, {
2787
+ ignoreCase: Boolean(a.ignore_case),
2788
+ signal: ctx.abortSignal,
2789
+ });
2447
2790
  // No path means the whole REPOSITORY, not the workspace directory above
2448
2791
  // it. The workspace root also holds whatever an older build left beside
2449
2792
  // the checkout, and the read gate has no rules for a path outside the
@@ -2457,7 +2800,7 @@ export function registerWorkspaceTools(
2457
2800
  const searchRoot = typeof a.path === 'string' && a.path.length > 0 ? a.path : kbDirName;
2458
2801
  const fs = await ctx.getFilesystem(a.branch as string);
2459
2802
  const gate = readGateFor(a.branch as string, ctx);
2460
- const out: { path: string; line: number; text: string }[] = [];
2803
+ const out: GrepMatch[] = [];
2461
2804
  const max = typeof a.max_results === 'number' ? Math.min(a.max_results, 1000) : 200;
2462
2805
  const docs: DocGrepState = {
2463
2806
  readers,
@@ -2469,8 +2812,13 @@ export function registerWorkspaceTools(
2469
2812
  // worth a stat.
2470
2813
  // A placeholder named on its own is searched as what it is to every
2471
2814
  // other tool: nothing.
2472
- const kind =
2473
- searchRoot === ''
2815
+ // A tmp ref names one file by rule (see `workspace-tmp.ts`), so it is
2816
+ // searched as a single file — never walked, and outside the repository
2817
+ // there is no read gate or read hook for it, as for `read_file`.
2818
+ const tmp = isTmpRef(searchRoot);
2819
+ const kind = tmp
2820
+ ? 'file'
2821
+ : searchRoot === ''
2474
2822
  ? 'directory'
2475
2823
  : isFolderPlaceholder(searchRoot)
2476
2824
  ? 'missing'
@@ -2496,42 +2844,25 @@ export function registerWorkspaceTools(
2496
2844
  // The guide's own path is not told here, because `guideAt` tells the
2497
2845
  // hook of the organisation's file there exactly as `read_file` does
2498
2846
  // (after the gate), and the platform's guide alone is nobody's file to
2499
- // note. Each file the walk opens goes to the hook per file below, so a
2500
- // hook sees every path a grep reached rather than only its root.
2501
- if (!(coversGuide && kind !== 'directory')) {
2847
+ // note. Each file the search opens goes to the hook per file below,
2848
+ // before it is opened and whether or not a line of it is answered
2849
+ // (`grepTree`'s admission), so a hook sees every file a grep reads
2850
+ // rather than only its root, and may refuse one as it refuses a
2851
+ // read_file. A tmp ref is no repository content, so there is nothing
2852
+ // to tell.
2853
+ if (!tmp && !(coversGuide && kind !== 'directory')) {
2502
2854
  await notifyAgentRead(agentAccessGate, ctx, a.branch as string, searchRoot);
2503
2855
  }
2856
+ const notifyRead = (p: string): Promise<void> => notifyAgentRead(agentAccessGate, ctx, a.branch as string, p);
2504
2857
  if (coversGuide) {
2505
2858
  if (kind === 'directory') {
2506
- await grepWalk(
2507
- fs,
2508
- searchRoot,
2509
- matcher,
2510
- out,
2511
- max,
2512
- 0,
2513
- gate,
2514
- (p) => notifyAgentRead(agentAccessGate, ctx, a.branch as string, p),
2515
- docs,
2516
- (p) => p === guidePath,
2517
- );
2518
- }
2519
- const lines = (await guideAt(a.branch as string, ctx, guidePath)).split('\n');
2520
- for (const i of await matcher.matchingLines(lines, max - out.length)) {
2521
- out.push({ path: guidePath, line: i + 1, text: lines[i]!.slice(0, 300) });
2859
+ out.push(...(await grepTree(fs, searchRoot, search, max, gate, notifyRead, docs, (p) => p === guidePath)));
2522
2860
  }
2861
+ const guide = await guideAt(a.branch as string, ctx, guidePath);
2862
+ const [hits] = await search.matchingLines([guide]);
2863
+ pushMatches(out, guidePath, guide, hits!);
2523
2864
  } else if (kind === 'directory') {
2524
- await grepWalk(
2525
- fs,
2526
- searchRoot,
2527
- matcher,
2528
- out,
2529
- max,
2530
- 0,
2531
- gate,
2532
- (p) => notifyAgentRead(agentAccessGate, ctx, a.branch as string, p),
2533
- docs,
2534
- );
2865
+ out.push(...(await grepTree(fs, searchRoot, search, max, gate, notifyRead, docs)));
2535
2866
  } else {
2536
2867
  // Not a directory: the permission verdict comes BEFORE every other
2537
2868
  // one, and it is `read_file`'s own gate on the same path — so grep
@@ -2541,15 +2872,28 @@ export function registerWorkspaceTools(
2541
2872
  // gets the 403, never the filesystem's complaint about it.
2542
2873
  await assertCanRead(gate, searchRoot);
2543
2874
  if (kind === 'missing') throw notFound(searchRoot, 'Nothing to search');
2544
- // A named FILE is searched directly: routing it through the walk would
2545
- // fail its readdir and answer an empty match list, which the caller
2546
- // cannot tell from "the pattern is not in this file".
2547
- const outcome = await orNotFound(
2548
- searchRoot,
2549
- () => grepOneFile(fs, searchRoot, matcher, out, max, docs),
2550
- 'Nothing to search',
2551
- );
2552
- if (outcome === 'no-text') {
2875
+ // A named text FILE is searched as a tree search searches it: by
2876
+ // ripgrep, from its folder, so it answers the same lines either way
2877
+ // (the valid UTF-8 lines of a file holding some that are not, and
2878
+ // nothing from one ripgrep finds binary). Any other file, and any
2879
+ // path that is not a regular file, is read through its reader,
2880
+ // exactly as read_file reads it, its text searched by the same
2881
+ // engine: a tree search would find nothing to walk and answer an
2882
+ // empty match list, which the caller cannot tell from "the pattern
2883
+ // is not in this file". A tmp ref is not in the checkout ripgrep
2884
+ // searches, so it is always read through its reader, its bytes from
2885
+ // the tmp resolver.
2886
+ const bytesOf = tmp
2887
+ ? (ref: string) => ctx.workspaceService.readTmpFile(docs.origin.workspaceId, ctx.user.id, ref)
2888
+ : fileBytes(fs);
2889
+ const found =
2890
+ (!tmp && readers.readerFor(searchRoot) instanceof TextReader
2891
+ ? await grepTextFile(fs, searchRoot, search, max, out)
2892
+ : null) ?? (await orNotFound(searchRoot, () => searchableText(bytesOf, searchRoot, docs), 'Nothing to search'));
2893
+ if (found.kind === 'text') {
2894
+ const [hits] = await search.matchingLines([found.text]);
2895
+ pushMatches(out, searchRoot, found.text, hits!);
2896
+ } else if (found.kind === 'no-text') {
2553
2897
  fileNote =
2554
2898
  `"${displayPath(searchRoot)}" has no searchable text — it is an image, binary content, or a document ` +
2555
2899
  'whose text could not be extracted. There were no matches because there was nothing to search, not ' +
@@ -2563,8 +2907,11 @@ export function registerWorkspaceTools(
2563
2907
  `extracted and this call's extraction budget (${UNCACHED_DOCS_PER_GREP}) ran out. Re-run the ` +
2564
2908
  'same grep to extract and search the next batch.'
2565
2909
  : undefined);
2910
+ // One order whatever found the lines, then the cap: the first
2911
+ // `max_results` by path, then line.
2912
+ out.sort((x, y) => compareSearchPaths(x.path, y.path) || x.line - y.line);
2566
2913
  return {
2567
- matches: out,
2914
+ matches: out.slice(0, max),
2568
2915
  truncated: out.length >= max,
2569
2916
  ...(note !== undefined ? { note } : {}),
2570
2917
  };
@@ -2581,6 +2928,7 @@ export function registerWorkspaceTools(
2581
2928
  description:
2582
2929
  'Write a workspace TEXT file. The change is committed + pushed as you. Returns `{ path, bytes, outcome }`, where `outcome` is ' +
2583
2930
  '`created`, `replaced` or `updated`.' +
2931
+ ASK_BEFORE_DEFAULT_BRANCH +
2584
2932
  UPLOAD_ROUTE_NOTE,
2585
2933
  inputs: {
2586
2934
  type: 'object',
@@ -2671,6 +3019,7 @@ export function registerWorkspaceTools(
2671
3019
  '`created` / `replaced` / `updated` for a path it wrote, or `refused` with `error` (the code) and `message` (why) for a ' +
2672
3020
  'path it could not. `count` is how many were written. A path it refuses — the mode said no, or the file is not text — ' +
2673
3021
  'does not stop the others; read `files` to see what landed.' +
3022
+ ASK_BEFORE_DEFAULT_BRANCH +
2674
3023
  UPLOAD_ROUTE_NOTE,
2675
3024
  inputs: {
2676
3025
  type: 'object',
@@ -2708,7 +3057,7 @@ export function registerWorkspaceTools(
2708
3057
  enum: ['created', 'replaced', 'updated', 'refused'],
2709
3058
  description: 'What happened at this path. `refused` means nothing was written there and the file is untouched.',
2710
3059
  },
2711
- error: str('Present when `outcome` is `refused`: the refusal code — `exists`, `missing` or `binary_not_writable`.'),
3060
+ error: str('Present when `outcome` is `refused`: the refusal code, such as `exists`, `missing`, `binary_not_writable` or a code of the deployment\'s own write check.'),
2712
3061
  message: str('Present when `outcome` is `refused`: the full refusal, the same one write_file would have given.'),
2713
3062
  },
2714
3063
  required: ['path', 'outcome'],
@@ -2843,6 +3192,7 @@ export function registerWorkspaceTools(
2843
3192
  gated: true,
2844
3193
  description:
2845
3194
  'Replace an exact string in a workspace TEXT file. `old_string` must appear exactly once unless `replace_all`. Committed + pushed as you.' +
3195
+ ASK_BEFORE_DEFAULT_BRANCH +
2846
3196
  UPLOAD_ROUTE_NOTE,
2847
3197
  inputs: {
2848
3198
  type: 'object',
@@ -2922,7 +3272,8 @@ export function registerWorkspaceTools(
2922
3272
  name: 'delete_file',
2923
3273
  gated: true,
2924
3274
  description:
2925
- 'Delete ONE workspace file. Committed + pushed as you. Its folder stays, even when this was its last file. Files only: a folder is refused with a pointer to `delete_folder`.',
3275
+ 'Delete ONE workspace file. Committed + pushed as you. Its folder stays, even when this was its last file. Files only: a folder is refused with a pointer to `delete_folder`.' +
3276
+ ASK_BEFORE_DEFAULT_BRANCH,
2926
3277
  inputs: {
2927
3278
  type: 'object',
2928
3279
  properties: {
@@ -3146,10 +3497,11 @@ export function registerWorkspaceTools(
3146
3497
  description:
3147
3498
  'Move or rename a workspace FILE or FOLDER; a folder moves recursively, with everything under it. `dest` is the full new path, not the folder to move into. Committed + pushed as you. ' +
3148
3499
  'The destination must not exist — a move never overwrites a file or merges into a folder. Access follows the ' +
3149
- 'DESTINATION folder, so a move can change what you (and others) may do with the file: the dry run answers ' +
3150
- '`{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }`, where `access` is your ' +
3151
- 'own `{ read, write, download, owner }` at the source and at the destination AS IT WILL BE once the move has landed, ' +
3152
- 'with every `access.md` inside a moved folder counted at its new place, and a move whose `accessChanges` is true wants `confirm: true`. ' +
3500
+ 'DESTINATION folder, so a move can change who may open or edit the file. You must be able to read what moves and the folder it lands in, ' +
3501
+ 'or it is refused as read_file refuses. The dry run answers ' +
3502
+ '`{ src, dest, kind, descendants, access: { before, after }, accessChanges, holderChanges?, allowed, reason? }`: your own ' +
3503
+ 'access at both ends, and who else loses or gains it. A move that changes either wants `confirm: true`; tell the person ' +
3504
+ 'who loses and who gains before you confirm. ' +
3153
3505
  'Links are rewritten by default, in one commit with the move: those in the moved files and those in other markdown files pointing at them (`links` reports them). `rewriteLinks: false` turns that off. Path mentions in plain prose or code are never changed.',
3154
3506
  inputs: {
3155
3507
  type: 'object',
@@ -3158,7 +3510,7 @@ export function registerWorkspaceTools(
3158
3510
  src: wsPath(kbDirName, 'Source path (file or folder)'),
3159
3511
  dest: wsPath(kbDirName, 'Destination path — the full new path; must not exist yet'),
3160
3512
  dryRun: { type: 'boolean', description: 'Answer with the impact and change nothing.' },
3161
- confirm: { type: 'boolean', description: 'Required when the move changes your access. Set it only after a dry run.' },
3513
+ confirm: { type: 'boolean', description: 'Required when the move changes your access or anyone else\'s. Set it only after a dry run.' },
3162
3514
  rewriteLinks: { type: 'boolean', description: 'Rewrite the links into, out of and between the moved files (default true). `false` moves without touching any link.' },
3163
3515
  sessionId: SESSION_ID_INPUT,
3164
3516
  },
@@ -3172,8 +3524,14 @@ export function registerWorkspaceTools(
3172
3524
  dest: str('Destination path (echoes the input).'),
3173
3525
  kind: str('`file` or `folder`.'),
3174
3526
  descendants: int('Files that move: 1 for a file, the file count under a folder.'),
3175
- access: { type: 'object', description: 'Your `{ read, write, download, owner }` at the source (`before`) and at the destination once the move has landed (`after`).' },
3527
+ access: { type: 'object', description: 'Your `{ read, write, download, owner }` at the source (`before`) and at the destination once the move has landed (`after`), with every `access.md` inside a moved folder counted at its new place.' },
3176
3528
  accessChanges: { type: 'boolean', description: 'True when any of your verdicts differs between `before` and `after`.' },
3529
+ holderChanges: {
3530
+ type: 'object',
3531
+ description:
3532
+ 'Who loses and who gains opening (`read`) or editing (`write`) it once the move has landed: `{ lose, gain }`, each ' +
3533
+ '`[{ kind, name, email?, verb }]` with `kind` `group`, `role`, `plugin` or `person`. Absent when nobody\'s access changes.',
3534
+ },
3177
3535
  allowed: { type: 'boolean', description: 'Whether the move may run.' },
3178
3536
  reason: str('Why it may not, when `allowed` is false.'),
3179
3537
  dryRun: { type: 'boolean', description: 'True on a dry run.' },
@@ -3203,6 +3561,23 @@ export function registerWorkspaceTools(
3203
3561
  // source already deleted.
3204
3562
  await assertAgentWriteAllowed(agentAccessGate, ctx, branch, src);
3205
3563
  await assertAgentWriteAllowed(agentAccessGate, ctx, branch, dest);
3564
+ // What moves and the folder it lands in must both be readable to the
3565
+ // caller, refused as read_file refuses them, on every branch: a move
3566
+ // preview reads both ends (the caller's own verdicts there, who else is
3567
+ // affected), and the folder's rules are what the names are made of. The
3568
+ // verdict comes BEFORE every other look, so neither end's existence is
3569
+ // told to someone who may not know it. The deployment's read hook is
3570
+ // asked first, as read_file asks it, and its refusal is the answer: a
3571
+ // move the hook would not let read is not previewed at all, since even
3572
+ // "this move asks for confirm" would tell what the comparison found. A
3573
+ // folder the caller may read moves with everything in it, files they
3574
+ // may not read included: those travel, they are not read.
3575
+ const destFolder = dest.slice(0, dest.lastIndexOf('/'));
3576
+ await notifyAgentRead(agentAccessGate, ctx, branch, src);
3577
+ await notifyAgentRead(agentAccessGate, ctx, branch, destFolder);
3578
+ const gate = readGateFor(branch, ctx);
3579
+ await assertCanRead(gate, src);
3580
+ await assertCanRead(gate, destFolder);
3206
3581
  const fs = await ctx.getFilesystem(branch);
3207
3582
  const root = await workspaceRoot(branch, ctx);
3208
3583
  // Before the kind check, which stats THROUGH a link: a dangling link at
@@ -3226,6 +3601,17 @@ export function registerWorkspaceTools(
3226
3601
  accessAfter(branch, ctx, src, dest),
3227
3602
  ]);
3228
3603
  const accessChanges = verbsDiffer(before, after);
3604
+ // Who ELSE gains or loses opening or editing it: the lists the app's move
3605
+ // confirmation shows people. The caller may read both ends (the hook and
3606
+ // the rules, gated above, as the app's route gates), so the names go to
3607
+ // them, and a change asks for `confirm`. Outside the repository no rules
3608
+ // travel.
3609
+ const fromRel = toKbRelative(src, kbDirName);
3610
+ const toRel = toKbRelative(dest, kbDirName);
3611
+ const changes = fromRel !== null && toRel !== null
3612
+ ? await accessControl.prospectiveHolderChanges(workspaceIdForBranch(branch), fromRel, toRel)
3613
+ : undefined;
3614
+ const othersChange = changes !== undefined && (changes.lose.length > 0 || changes.gain.length > 0);
3229
3615
  // The placeholder moves with its folder, but it is never content.
3230
3616
  const descendants = srcFiles.filter((f) => !isFolderPlaceholder(f)).length;
3231
3617
  // Neither end may be the platform's own: a move neither takes a platform
@@ -3333,6 +3719,7 @@ export function registerWorkspaceTools(
3333
3719
  descendants,
3334
3720
  access: { before, after },
3335
3721
  accessChanges,
3722
+ ...(othersChange ? { holderChanges: changes } : {}),
3336
3723
  allowed: moveReason === undefined,
3337
3724
  ...(moveReason !== undefined ? { reason: moveReason } : {}),
3338
3725
  ...(linkPlan ? { links: linkPlan.report } : {}),
@@ -3348,12 +3735,16 @@ export function registerWorkspaceTools(
3348
3735
  }
3349
3736
  if (collision) throw new ToolError(reason!, 409);
3350
3737
  if (linkPlan?.overCap !== undefined) throw new ToolError(linkPlan.overCap, 400);
3351
- if (accessChanges && a.confirm !== true) {
3738
+ if ((accessChanges || othersChange) && a.confirm !== true) {
3739
+ const why = [
3740
+ ...(accessChanges ? [`your access at "${dest}" differs from "${src}"`] : []),
3741
+ ...(othersChange ? ['it changes who else may open or edit it (`holderChanges`)'] : []),
3742
+ ];
3352
3743
  return {
3353
3744
  ...impact,
3354
3745
  confirmationRequired: true,
3355
3746
  moved: false,
3356
- message: `Nothing was moved: your access at "${dest}" differs from "${src}", so this move requires confirm: true.`,
3747
+ message: `Nothing was moved: ${why.join(', and ')}, so this move requires confirm: true.`,
3357
3748
  };
3358
3749
  }
3359
3750
  // The look above is what produces the sentence; the filesystem's own
@@ -3402,14 +3793,14 @@ export function registerWorkspaceTools(
3402
3793
  name: 'copy_file',
3403
3794
  gated: true,
3404
3795
  description:
3405
- 'Copy a workspace FILE to a new path. The destination must not exist — like a move, a copy never overwrites a file or a folder; to change what is in a file that already exists, write it. Committed + pushed as you. ' +
3406
- 'Preflight first: `dryRun: true` changes nothing and answers `{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }` — `access` is your own `{ read, write, download, owner }` at the source and at the destination AS IT WILL BE once the copy has landed, with every `access.md` inside a copied folder counted at its new place. ' +
3407
- 'One exception to that shape: when the destination is one you may not write, the answer is the refusal alone — `allowed: false` with `reason`, and no `kind` and no `descendants`, because nothing about the source is read before that verdict.',
3796
+ 'Copy a workspace FILE to a new path. The destination must not exist — like a move, a copy never overwrites a file or a folder; to change what is in a file that already exists, write it. Committed + pushed as you. `src` may be a `__tmp__/<path>` ref to a file you staged in the workspace\'s tmp folder: the copy is how such a file is kept, as a normal repository file. ' +
3797
+ 'Preflight first: `dryRun: true` changes nothing and answers `{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }` — `access` is your own `{ read, write, download, owner }` at the source and at the destination AS IT WILL BE once the copy has landed, with the copy\'s own rules counted at its new place. ' +
3798
+ 'One exception to that shape: when the destination is one you may not write, the answer is the refusal (`allowed: false` with `reason`, and the access figures) with no `kind` and no `descendants`, because nothing about the source is read before that verdict.',
3408
3799
  inputs: {
3409
3800
  type: 'object',
3410
3801
  properties: {
3411
3802
  branch: BRANCH_INPUT,
3412
- src: wsPath(kbDirName, 'Source path'),
3803
+ src: orTmpRef(wsPath(kbDirName, 'Source path')),
3413
3804
  dest: wsPath(kbDirName, 'Destination path — must not exist yet'),
3414
3805
  dryRun: { type: 'boolean', description: 'Answer with the impact and change nothing.' },
3415
3806
  sessionId: SESSION_ID_INPUT,
@@ -3437,11 +3828,14 @@ export function registerWorkspaceTools(
3437
3828
  proposable: true,
3438
3829
  handler: async (a, ctx: ToolContext) => {
3439
3830
  // A copy CARRIES the source content into the destination, so BOTH ends
3440
- // go to the write hook. Ask about both before touching disk.
3441
- writePolicy.assertPathWritable(ctx.sessionId, a.src as string);
3831
+ // go to the write hook. Ask about both before touching disk. A tmp ref
3832
+ // is not a knowledge-base path, so only the destination is one to ask
3833
+ // about when the copy is out of the workspace's tmp folder.
3834
+ const fromTmp = isTmpRef(a.src);
3835
+ if (!fromTmp) writePolicy.assertPathWritable(ctx.sessionId, a.src as string);
3442
3836
  writePolicy.assertPathWritable(ctx.sessionId, a.dest as string);
3443
3837
  const branch = a.branch as string;
3444
- await assertAgentWriteAllowed(agentAccessGate, ctx, branch, a.src as string);
3838
+ if (!fromTmp) await assertAgentWriteAllowed(agentAccessGate, ctx, branch, a.src as string);
3445
3839
  await assertAgentWriteAllowed(agentAccessGate, ctx, branch, a.dest as string);
3446
3840
  const src = a.src as string;
3447
3841
  const dest = a.dest as string;
@@ -3488,6 +3882,10 @@ export function registerWorkspaceTools(
3488
3882
  // the server's own absolute path. Anything that is not absence travels
3489
3883
  // on as it always did.
3490
3884
  const fs = await ctx.getFilesystem(branch);
3885
+ if (fromTmp) {
3886
+ await copyFromTmp(fs, branch, ctx, src, dest);
3887
+ return { src, dest, copied: true };
3888
+ }
3491
3889
  try {
3492
3890
  await asEntryExists(() => fs.copyFile(src, dest));
3493
3891
  } catch (err) {
@@ -3858,7 +4256,7 @@ export function registerWorkspaceTools(
3858
4256
  '`replaced` / `updated`, or `refused` with `error` (the code) and `message` (why). `count` is how many landed and ' +
3859
4257
  '`total` how many paths there were; `files` is cut to the first 25 unless you pass `all: true`. ' +
3860
4258
  'Every path is judged one by one — by your write access, the platform-file rules and what is already there — ' +
3861
- 'exactly as `write_file` judges it, and a refused path does not stop the others. ' +
4259
+ 'as `write_file` judges it, except that platform files are refused and binary content is fine; a refused path does not stop the others. ' +
3862
4260
  'The token is single-use: it is spent by the apply that lands it, and refused if you use it twice, let it ' +
3863
4261
  'expire, or present one issued to somebody else. `mode` means what it means on `write_file`.',
3864
4262
  inputs: {
@@ -3942,7 +4340,7 @@ export function registerWorkspaceTools(
3942
4340
  'each one inside a folder too, is included only if you may read AND download that file. Returns ' +
3943
4341
  '`{ expiresAt, expiresInSeconds, files: [{ path, bytes, sha256, downloadUrl }], folders: [{ path, bytes, ' +
3944
4342
  'downloadUrl, files }], refused: [{ path, reason }] }`: a link per file, with its own content type, and per ' +
3945
- `folder a zip at full repository paths (\`apply_file_upload\` it at \`${kbDirName}/\` to put every file back). ` +
4343
+ `folder a zip at full repository paths (\`apply_file_upload\` it at \`${kbDirName}/\` to put them back). ` +
3946
4344
  'Fetch with any HTTP client (`curl -o <name> "<downloadUrl>"`, or the address without its last segment and an ' +
3947
4345
  '`x-download-token` header). Each link works ONCE, for 15 minutes, and serves the files as they are now. ' +
3948
4346
  'Refused: `not found` (missing or named-but-unreadable; folders omit unreadable files), ' +